فهرست منبع

chore: 优化菜单搜索 (#551)

* Update SearchModal.vue

* chore: 优化菜单搜索

---------

Co-authored-by: xiaoxian521 <1923740402@qq.com>
guanrui Lu 2 سال پیش
والد
کامیت
0e632ac4ab

+ 25 - 7
src/layout/components/search/components/SearchFooter.vue

@@ -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>
   <div class="search-footer text-[#333] dark:text-white">
     <span class="search-footer-item">
@@ -13,16 +27,15 @@
       <mdiKeyboardEsc class="icon" />
       关闭
     </span>
+    <p
+      v-if="device !== 'mobile' && props.total > 0"
+      class="search-footer-total"
+    >
+      共{{ props.total }}项
+    </p>
   </div>
 </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>
 .search-footer {
   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,
       0 1px 2px 1px #1e235a66;
   }
+
+  .search-footer-total {
+    position: absolute;
+    right: 20px;
+  }
 }
 </style>

+ 39 - 15
src/layout/components/search/components/SearchModal.vue

@@ -8,7 +8,7 @@ import { transformI18n } from "@/plugins/i18n";
 import { ref, computed, shallowRef } from "vue";
 import { useDebounceFn, onKeyStroke } from "@vueuse/core";
 import { usePermissionStoreHook } from "@/store/modules/permission";
-import Search from "@iconify-icons/ep/search";
+import Search from "@iconify-icons/ri/search-line";
 
 interface Props {
   /** 弹窗显隐 */
@@ -25,6 +25,8 @@ const props = withDefaults(defineProps<Props>(), {});
 const router = useRouter();
 
 const keyword = ref("");
+const scrollbarRef = ref();
+const resultRef = ref();
 const activePath = ref("");
 const inputRef = ref<HTMLInputElement | null>(null);
 const resultOptions = shallowRef([]);
@@ -83,6 +85,11 @@ function handleClose() {
   }, 200);
 }
 
+function scrollTo(index) {
+  const scrollTop = resultRef.value.handleScroll(index);
+  scrollbarRef.value.setScrollTop(scrollTop);
+}
+
 /** key up */
 function handleUp() {
   const { length } = resultOptions.value;
@@ -92,8 +99,10 @@ function handleUp() {
   );
   if (index === 0) {
     activePath.value = resultOptions.value[length - 1].path;
+    scrollTo(resultOptions.value.length - 1);
   } else {
     activePath.value = resultOptions.value[index - 1].path;
+    scrollTo(index - 1);
   }
 }
 
@@ -109,6 +118,7 @@ function handleDown() {
   } else {
     activePath.value = resultOptions.value[index + 1].path;
   }
+  scrollTo(index + 1);
 }
 
 /** key enter */
@@ -127,41 +137,55 @@ onKeyStroke("ArrowDown", handleDown);
 <template>
   <el-dialog
     top="5vh"
+    class="pure-search-dialog"
     v-model="show"
-    :width="device === 'mobile' ? '80vw' : '50vw'"
+    :show-close="false"
+    :width="device === 'mobile' ? '80vw' : '40vw'"
     :before-close="handleClose"
+    :style="{
+      borderRadius: '6px'
+    }"
     @opened="inputRef.focus()"
     @closed="inputRef.blur()"
   >
     <el-input
       ref="inputRef"
+      size="large"
       v-model="keyword"
       clearable
-      placeholder="请输入关键词搜索"
+      placeholder="搜索菜单"
       @input="handleSearch"
     >
       <template #prefix>
-        <span class="el-input__icon">
-          <IconifyIconOffline :icon="Search" />
-        </span>
+        <IconifyIconOffline
+          :icon="Search"
+          class="text-primary w-[24px] h-[24px]"
+        />
       </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"
-      />
+      <el-scrollbar ref="scrollbarRef" max-height="600px">
+        <el-empty
+          v-if="resultOptions.length === 0"
+          description="暂无搜索结果"
+        />
+        <SearchResult
+          v-else
+          ref="resultRef"
+          v-model:value="activePath"
+          :options="resultOptions"
+          @click="handleEnter"
+        />
+      </el-scrollbar>
     </div>
     <template #footer>
-      <SearchFooter />
+      <SearchFooter :total="resultOptions.length" />
     </template>
   </el-dialog>
 </template>
+
 <style lang="scss" scoped>
 .search-result-container {
-  margin-top: 20px;
+  margin-top: 12px;
 }
 </style>

+ 25 - 13
src/layout/components/search/components/SearchResult.vue

@@ -1,6 +1,6 @@
 <script setup lang="ts">
-import { computed } from "vue";
 import { useI18n } from "vue-i18n";
+import { computed, getCurrentInstance } from "vue";
 import { useEpThemeStoreHook } from "@/store/modules/epTheme";
 import { useRenderIcon } from "@/components/ReIcon/src/hooks";
 import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
@@ -28,6 +28,7 @@ interface Emits {
 
 const props = withDefaults(defineProps<Props>(), {});
 const emit = defineEmits<Emits>();
+const instance = getCurrentInstance()!;
 
 const itemStyle = computed(() => {
   return item => {
@@ -57,22 +58,33 @@ async function handleMouse(item) {
 function handleTo() {
   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>
 
 <template>
   <div class="result">
-    <template v-for="item in options" :key="item.path">
-      <div
-        class="result-item dark:bg-[#1d1d1d]"
-        :style="itemStyle(item)"
-        @click="handleTo"
-        @mouseenter="handleMouse(item)"
-      >
-        <component :is="useRenderIcon(item.meta?.icon ?? Bookmark2Line)" />
-        <span class="result-item-title">{{ t(item.meta?.title) }}</span>
-        <enterOutlined />
-      </div>
-    </template>
+    <div
+      v-for="(item, index) in options"
+      :key="item.path"
+      :ref="'resultItemRef' + index"
+      class="result-item dark:bg-[#1d1d1d]"
+      :style="itemStyle(item)"
+      @click="handleTo"
+      @mouseenter="handleMouse(item)"
+    >
+      <component :is="useRenderIcon(item.meta?.icon ?? Bookmark2Line)" />
+      <span class="result-item-title">{{ t(item.meta?.title) }}</span>
+      <enterOutlined />
+    </div>
   </div>
 </template>
 

+ 17 - 0
src/style/dark.scss

@@ -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 组件 */
   .pure-segmented {
     color: rgb(255 255 255 / 65%);

+ 21 - 0
src/style/element-plus.scss

@@ -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%);
+  }
+}