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

* Update SearchModal.vue

* chore: 优化菜单搜索

---------

Co-authored-by: xiaoxian521 <1923740402@qq.com>
This commit is contained in:
guanrui Lu 2023-05-16 12:53:06 +08:00 committed by GitHub
parent 6776e85641
commit 0e632ac4ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 127 additions and 35 deletions

View File

@ -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>

View File

@ -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">
<SearchResult <el-empty
v-else v-if="resultOptions.length === 0"
v-model:value="activePath" description="暂无搜索结果"
:options="resultOptions" />
@click="handleEnter" <SearchResult
/> v-else
ref="resultRef"
v-model:value="activePath"
:options="resultOptions"
@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>

View File

@ -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,22 +58,33 @@ 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-item56px+56px=112pxmargin8px+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"
class="result-item dark:bg-[#1d1d1d]" :key="item.path"
:style="itemStyle(item)" :ref="'resultItemRef' + index"
@click="handleTo" class="result-item dark:bg-[#1d1d1d]"
@mouseenter="handleMouse(item)" :style="itemStyle(item)"
> @click="handleTo"
<component :is="useRenderIcon(item.meta?.icon ?? Bookmark2Line)" /> @mouseenter="handleMouse(item)"
<span class="result-item-title">{{ t(item.meta?.title) }}</span> >
<enterOutlined /> <component :is="useRenderIcon(item.meta?.icon ?? Bookmark2Line)" />
</div> <span class="result-item-title">{{ t(item.meta?.title) }}</span>
</template> <enterOutlined />
</div>
</div> </div>
</template> </template>

View File

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

View File

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