mirror of
https://github.com/pure-admin/vue-pure-admin.git
synced 2025-06-07 08:57:19 +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">
|
||||||
<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>
|
||||||
|
@ -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-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"
|
||||||
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>
|
||||||
|
|
||||||
|
@ -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