mirror of
				https://github.com/pure-admin/vue-pure-admin.git
				synced 2025-11-03 13:44:47 +08:00 
			
		
		
		
	chore: 优化菜单搜索 (#551)
* Update SearchModal.vue * chore: 优化菜单搜索 --------- Co-authored-by: xiaoxian521 <1923740402@qq.com>
This commit is contained in:
		
							parent
							
								
									6776e85641
								
							
						
					
					
						commit
						0e632ac4ab
					
				@ -1,3 +1,17 @@
 | 
				
			|||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import ArrowUpLine from "@iconify-icons/ri/arrow-up-line";
 | 
				
			||||||
 | 
					import ArrowDownLine from "@iconify-icons/ri/arrow-down-line";
 | 
				
			||||||
 | 
					import { useNav } from "@/layout/hooks/useNav";
 | 
				
			||||||
 | 
					import mdiKeyboardEsc from "@/assets/svg/keyboard_esc.svg?component";
 | 
				
			||||||
 | 
					import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = withDefaults(defineProps<{ total: number }>(), {
 | 
				
			||||||
 | 
					  total: 0
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { device } = useNav();
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div class="search-footer text-[#333] dark:text-white">
 | 
					  <div class="search-footer text-[#333] dark:text-white">
 | 
				
			||||||
    <span class="search-footer-item">
 | 
					    <span class="search-footer-item">
 | 
				
			||||||
@ -13,16 +27,15 @@
 | 
				
			|||||||
      <mdiKeyboardEsc class="icon" />
 | 
					      <mdiKeyboardEsc class="icon" />
 | 
				
			||||||
      关闭
 | 
					      关闭
 | 
				
			||||||
    </span>
 | 
					    </span>
 | 
				
			||||||
 | 
					    <p
 | 
				
			||||||
 | 
					      v-if="device !== 'mobile' && props.total > 0"
 | 
				
			||||||
 | 
					      class="search-footer-total"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      共{{ props.total }}项
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script setup lang="ts">
 | 
					 | 
				
			||||||
import ArrowUpLine from "@iconify-icons/ri/arrow-up-line";
 | 
					 | 
				
			||||||
import ArrowDownLine from "@iconify-icons/ri/arrow-down-line";
 | 
					 | 
				
			||||||
import mdiKeyboardEsc from "@/assets/svg/keyboard_esc.svg?component";
 | 
					 | 
				
			||||||
import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style lang="scss" scoped>
 | 
					<style lang="scss" scoped>
 | 
				
			||||||
.search-footer {
 | 
					.search-footer {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
@ -40,5 +53,10 @@ import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
 | 
				
			|||||||
    box-shadow: inset 0 -2px #cdcde6, inset 0 0 1px 1px #fff,
 | 
					    box-shadow: inset 0 -2px #cdcde6, inset 0 0 1px 1px #fff,
 | 
				
			||||||
      0 1px 2px 1px #1e235a66;
 | 
					      0 1px 2px 1px #1e235a66;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .search-footer-total {
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    right: 20px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
 | 
				
			|||||||
@ -8,7 +8,7 @@ import { transformI18n } from "@/plugins/i18n";
 | 
				
			|||||||
import { ref, computed, shallowRef } from "vue";
 | 
					import { ref, computed, shallowRef } from "vue";
 | 
				
			||||||
import { useDebounceFn, onKeyStroke } from "@vueuse/core";
 | 
					import { useDebounceFn, onKeyStroke } from "@vueuse/core";
 | 
				
			||||||
import { usePermissionStoreHook } from "@/store/modules/permission";
 | 
					import { usePermissionStoreHook } from "@/store/modules/permission";
 | 
				
			||||||
import Search from "@iconify-icons/ep/search";
 | 
					import Search from "@iconify-icons/ri/search-line";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface Props {
 | 
					interface Props {
 | 
				
			||||||
  /** 弹窗显隐 */
 | 
					  /** 弹窗显隐 */
 | 
				
			||||||
@ -25,6 +25,8 @@ const props = withDefaults(defineProps<Props>(), {});
 | 
				
			|||||||
const router = useRouter();
 | 
					const router = useRouter();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const keyword = ref("");
 | 
					const keyword = ref("");
 | 
				
			||||||
 | 
					const scrollbarRef = ref();
 | 
				
			||||||
 | 
					const resultRef = ref();
 | 
				
			||||||
const activePath = ref("");
 | 
					const activePath = ref("");
 | 
				
			||||||
const inputRef = ref<HTMLInputElement | null>(null);
 | 
					const inputRef = ref<HTMLInputElement | null>(null);
 | 
				
			||||||
const resultOptions = shallowRef([]);
 | 
					const resultOptions = shallowRef([]);
 | 
				
			||||||
@ -83,6 +85,11 @@ function handleClose() {
 | 
				
			|||||||
  }, 200);
 | 
					  }, 200);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function scrollTo(index) {
 | 
				
			||||||
 | 
					  const scrollTop = resultRef.value.handleScroll(index);
 | 
				
			||||||
 | 
					  scrollbarRef.value.setScrollTop(scrollTop);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/** key up */
 | 
					/** key up */
 | 
				
			||||||
function handleUp() {
 | 
					function handleUp() {
 | 
				
			||||||
  const { length } = resultOptions.value;
 | 
					  const { length } = resultOptions.value;
 | 
				
			||||||
@ -92,8 +99,10 @@ function handleUp() {
 | 
				
			|||||||
  );
 | 
					  );
 | 
				
			||||||
  if (index === 0) {
 | 
					  if (index === 0) {
 | 
				
			||||||
    activePath.value = resultOptions.value[length - 1].path;
 | 
					    activePath.value = resultOptions.value[length - 1].path;
 | 
				
			||||||
 | 
					    scrollTo(resultOptions.value.length - 1);
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    activePath.value = resultOptions.value[index - 1].path;
 | 
					    activePath.value = resultOptions.value[index - 1].path;
 | 
				
			||||||
 | 
					    scrollTo(index - 1);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -109,6 +118,7 @@ function handleDown() {
 | 
				
			|||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    activePath.value = resultOptions.value[index + 1].path;
 | 
					    activePath.value = resultOptions.value[index + 1].path;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  scrollTo(index + 1);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/** key enter */
 | 
					/** key enter */
 | 
				
			||||||
@ -127,41 +137,55 @@ onKeyStroke("ArrowDown", handleDown);
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <el-dialog
 | 
					  <el-dialog
 | 
				
			||||||
    top="5vh"
 | 
					    top="5vh"
 | 
				
			||||||
 | 
					    class="pure-search-dialog"
 | 
				
			||||||
    v-model="show"
 | 
					    v-model="show"
 | 
				
			||||||
    :width="device === 'mobile' ? '80vw' : '50vw'"
 | 
					    :show-close="false"
 | 
				
			||||||
 | 
					    :width="device === 'mobile' ? '80vw' : '40vw'"
 | 
				
			||||||
    :before-close="handleClose"
 | 
					    :before-close="handleClose"
 | 
				
			||||||
 | 
					    :style="{
 | 
				
			||||||
 | 
					      borderRadius: '6px'
 | 
				
			||||||
 | 
					    }"
 | 
				
			||||||
    @opened="inputRef.focus()"
 | 
					    @opened="inputRef.focus()"
 | 
				
			||||||
    @closed="inputRef.blur()"
 | 
					    @closed="inputRef.blur()"
 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    <el-input
 | 
					    <el-input
 | 
				
			||||||
      ref="inputRef"
 | 
					      ref="inputRef"
 | 
				
			||||||
 | 
					      size="large"
 | 
				
			||||||
      v-model="keyword"
 | 
					      v-model="keyword"
 | 
				
			||||||
      clearable
 | 
					      clearable
 | 
				
			||||||
      placeholder="请输入关键词搜索"
 | 
					      placeholder="搜索菜单"
 | 
				
			||||||
      @input="handleSearch"
 | 
					      @input="handleSearch"
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <template #prefix>
 | 
					      <template #prefix>
 | 
				
			||||||
        <span class="el-input__icon">
 | 
					        <IconifyIconOffline
 | 
				
			||||||
          <IconifyIconOffline :icon="Search" />
 | 
					          :icon="Search"
 | 
				
			||||||
        </span>
 | 
					          class="text-primary w-[24px] h-[24px]"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
      </template>
 | 
					      </template>
 | 
				
			||||||
    </el-input>
 | 
					    </el-input>
 | 
				
			||||||
    <div class="search-result-container">
 | 
					    <div class="search-result-container">
 | 
				
			||||||
      <el-empty v-if="resultOptions.length === 0" description="暂无搜索结果" />
 | 
					      <el-scrollbar ref="scrollbarRef" max-height="600px">
 | 
				
			||||||
 | 
					        <el-empty
 | 
				
			||||||
 | 
					          v-if="resultOptions.length === 0"
 | 
				
			||||||
 | 
					          description="暂无搜索结果"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
        <SearchResult
 | 
					        <SearchResult
 | 
				
			||||||
          v-else
 | 
					          v-else
 | 
				
			||||||
 | 
					          ref="resultRef"
 | 
				
			||||||
          v-model:value="activePath"
 | 
					          v-model:value="activePath"
 | 
				
			||||||
          :options="resultOptions"
 | 
					          :options="resultOptions"
 | 
				
			||||||
          @click="handleEnter"
 | 
					          @click="handleEnter"
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
 | 
					      </el-scrollbar>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <template #footer>
 | 
					    <template #footer>
 | 
				
			||||||
      <SearchFooter />
 | 
					      <SearchFooter :total="resultOptions.length" />
 | 
				
			||||||
    </template>
 | 
					    </template>
 | 
				
			||||||
  </el-dialog>
 | 
					  </el-dialog>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="scss" scoped>
 | 
					<style lang="scss" scoped>
 | 
				
			||||||
.search-result-container {
 | 
					.search-result-container {
 | 
				
			||||||
  margin-top: 20px;
 | 
					  margin-top: 12px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import { computed } from "vue";
 | 
					 | 
				
			||||||
import { useI18n } from "vue-i18n";
 | 
					import { useI18n } from "vue-i18n";
 | 
				
			||||||
 | 
					import { computed, getCurrentInstance } from "vue";
 | 
				
			||||||
import { useEpThemeStoreHook } from "@/store/modules/epTheme";
 | 
					import { useEpThemeStoreHook } from "@/store/modules/epTheme";
 | 
				
			||||||
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
 | 
					import { useRenderIcon } from "@/components/ReIcon/src/hooks";
 | 
				
			||||||
import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
 | 
					import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
 | 
				
			||||||
@ -28,6 +28,7 @@ interface Emits {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const props = withDefaults(defineProps<Props>(), {});
 | 
					const props = withDefaults(defineProps<Props>(), {});
 | 
				
			||||||
const emit = defineEmits<Emits>();
 | 
					const emit = defineEmits<Emits>();
 | 
				
			||||||
 | 
					const instance = getCurrentInstance()!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const itemStyle = computed(() => {
 | 
					const itemStyle = computed(() => {
 | 
				
			||||||
  return item => {
 | 
					  return item => {
 | 
				
			||||||
@ -57,12 +58,24 @@ async function handleMouse(item) {
 | 
				
			|||||||
function handleTo() {
 | 
					function handleTo() {
 | 
				
			||||||
  emit("enter");
 | 
					  emit("enter");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function handleScroll(index: number) {
 | 
				
			||||||
 | 
					  const curInstance = instance?.proxy?.$refs[`resultItemRef${index}`];
 | 
				
			||||||
 | 
					  if (!curInstance) return 0;
 | 
				
			||||||
 | 
					  const curRef = curInstance[0] as ElRef;
 | 
				
			||||||
 | 
					  const scrollTop = curRef.offsetTop + 128; // 128 两个result-item(56px+56px=112px)高度加上下margin(8px+8px=16px)
 | 
				
			||||||
 | 
					  return scrollTop > 600 ? scrollTop - 600 : 0; // 600 el-scrollbar max-height="600px"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					defineExpose({ handleScroll });
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div class="result">
 | 
					  <div class="result">
 | 
				
			||||||
    <template v-for="item in options" :key="item.path">
 | 
					 | 
				
			||||||
    <div
 | 
					    <div
 | 
				
			||||||
 | 
					      v-for="(item, index) in options"
 | 
				
			||||||
 | 
					      :key="item.path"
 | 
				
			||||||
 | 
					      :ref="'resultItemRef' + index"
 | 
				
			||||||
      class="result-item dark:bg-[#1d1d1d]"
 | 
					      class="result-item dark:bg-[#1d1d1d]"
 | 
				
			||||||
      :style="itemStyle(item)"
 | 
					      :style="itemStyle(item)"
 | 
				
			||||||
      @click="handleTo"
 | 
					      @click="handleTo"
 | 
				
			||||||
@ -72,7 +85,6 @@ function handleTo() {
 | 
				
			|||||||
      <span class="result-item-title">{{ t(item.meta?.title) }}</span>
 | 
					      <span class="result-item-title">{{ t(item.meta?.title) }}</span>
 | 
				
			||||||
      <enterOutlined />
 | 
					      <enterOutlined />
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    </template>
 | 
					 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -139,6 +139,23 @@ html.dark {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /* 自定义菜单搜索样式 */
 | 
				
			||||||
 | 
					  .pure-search-dialog {
 | 
				
			||||||
 | 
					    .el-dialog__footer {
 | 
				
			||||||
 | 
					      box-shadow: 0 -1px 0 0 #555a64, 0 -3px 6px 0 rgb(69 98 155 / 12%);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .search-footer {
 | 
				
			||||||
 | 
					      .search-footer-item {
 | 
				
			||||||
 | 
					        color: rgb(235 235 235 / 60%);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .icon {
 | 
				
			||||||
 | 
					          box-shadow: none;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /* ReSegmented 组件 */
 | 
					  /* ReSegmented 组件 */
 | 
				
			||||||
  .pure-segmented {
 | 
					  .pure-segmented {
 | 
				
			||||||
    color: rgb(255 255 255 / 65%);
 | 
					    color: rgb(255 255 255 / 65%);
 | 
				
			||||||
 | 
				
			|||||||
@ -148,3 +148,24 @@
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 自定义菜单搜索样式 */
 | 
				
			||||||
 | 
					.pure-search-dialog {
 | 
				
			||||||
 | 
					  .el-dialog__header {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .el-dialog__body {
 | 
				
			||||||
 | 
					    padding-top: 12px;
 | 
				
			||||||
 | 
					    padding-bottom: 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .el-input__inner {
 | 
				
			||||||
 | 
					    font-size: 1.2em;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .el-dialog__footer {
 | 
				
			||||||
 | 
					    padding-bottom: 10px;
 | 
				
			||||||
 | 
					    box-shadow: 0 -1px 0 0 #e0e3e8, 0 -3px 6px 0 rgb(69 98 155 / 12%);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user