-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(components): add GlobalSearch components
- Loading branch information
Showing
8 changed files
with
255 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
30 changes: 30 additions & 0 deletions
30
src/layouts/modules/global-search/components/search-footer.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
<script lang="ts" setup> | ||
defineOptions({ name: 'SearchFooter' }); | ||
</script> | ||
|
||
<template> | ||
<div class="h-44px flex-y-center px-24px"> | ||
<span class="mr-14px flex-y-center"> | ||
<icon-mdi-keyboard-return class="icon mr-6px p-2px text-20px" /> | ||
<span>确认</span> | ||
</span> | ||
<span class="mr-14px flex-y-center"> | ||
<icon-mdi-arrow-up-thin class="icon mr-5px p-2px text-20px" /> | ||
<icon-mdi-arrow-down-thin class="icon mr-6px p-2px text-20px" /> | ||
<span>切换</span> | ||
</span> | ||
<span class="flex-y-center"> | ||
<icon-mdi-keyboard-esc class="icon mr-6px p-2px text-20px" /> | ||
<span>关闭</span> | ||
</span> | ||
</div> | ||
</template> | ||
|
||
<style lang="scss" scoped> | ||
.icon { | ||
box-shadow: | ||
inset 0 -2px #cdcde6, | ||
inset 0 0 1px 1px #fff, | ||
0 1px 2px 1px #1e235a66; | ||
} | ||
</style> |
115 changes: 115 additions & 0 deletions
115
src/layouts/modules/global-search/components/search-modal.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
<script lang="ts" setup> | ||
import { computed, ref, shallowRef } from 'vue'; | ||
import { useRouter } from 'vue-router'; | ||
import { onKeyStroke, useDebounceFn } from '@vueuse/core'; | ||
import { useRouteStore } from '@/store/modules/route'; | ||
import { useAppStore } from '@/store/modules/app'; | ||
import { $t } from '@/locales'; | ||
import SearchResult from './search-result.vue'; | ||
import SearchFooter from './search-footer.vue'; | ||
defineOptions({ name: 'SearchModal' }); | ||
const appStore = useAppStore(); | ||
const isMobile = computed(() => appStore.isMobile); | ||
const router = useRouter(); | ||
const routeStore = useRouteStore(); | ||
const keyword = ref(''); | ||
const activePath = ref(''); | ||
const resultOptions = shallowRef<App.Global.Menu[]>([]); | ||
const handleSearch = useDebounceFn(search, 300); | ||
const modelShow = defineModel<boolean>('show', { required: true }); | ||
/** 查询 */ | ||
function search() { | ||
resultOptions.value = routeStore.searchMenus.filter(menu => { | ||
const trimKeyword = keyword.value.toLocaleLowerCase().trim(); | ||
const title = (menu.i18nKey ? $t(menu.i18nKey) : menu.label).toLocaleLowerCase(); | ||
return trimKeyword && title.includes(trimKeyword); | ||
}); | ||
activePath.value = resultOptions.value[0]?.routePath ?? ''; | ||
} | ||
function handleClose() { | ||
modelShow.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.routePath === activePath.value); | ||
if (index === 0) { | ||
activePath.value = resultOptions.value[length - 1].routePath; | ||
} else { | ||
activePath.value = resultOptions.value[index - 1].routePath; | ||
} | ||
} | ||
/** key down */ | ||
function handleDown() { | ||
const { length } = resultOptions.value; | ||
if (length === 0) return; | ||
const index = resultOptions.value.findIndex(item => item.routePath === activePath.value); | ||
if (index + 1 === length) { | ||
activePath.value = resultOptions.value[0].routePath; | ||
} else { | ||
activePath.value = resultOptions.value[index + 1].routePath; | ||
} | ||
} | ||
/** key enter */ | ||
function handleEnter(e: Event | undefined) { | ||
const { length } = resultOptions.value; | ||
if (length === 0 || activePath.value === '') return; | ||
e?.preventDefault(); | ||
handleClose(); | ||
router.push(activePath.value); | ||
} | ||
onKeyStroke('Escape', handleClose); | ||
onKeyStroke('Enter', handleEnter); | ||
onKeyStroke('ArrowUp', handleUp); | ||
onKeyStroke('ArrowDown', handleDown); | ||
</script> | ||
|
||
<template> | ||
<n-modal | ||
v-model:show="modelShow" | ||
:segmented="{ footer: 'soft' }" | ||
:closable="false" | ||
preset="card" | ||
auto-focus | ||
footer-style="padding: 0; margin: 0" | ||
class="fixed left-0 right-0" | ||
:class="[isMobile ? 'size-full top-0px rounded-0' : 'w-630px top-50px']" | ||
@after-leave="handleClose" | ||
> | ||
<n-input-group> | ||
<n-input v-model:value="keyword" clearable placeholder="请输入关键词搜索" @input="handleSearch"> | ||
<template #prefix> | ||
<icon-uil-search class="text-15px text-#c2c2c2" /> | ||
</template> | ||
</n-input> | ||
<n-button v-if="isMobile" type="primary" ghost @click="handleClose">取消</n-button> | ||
</n-input-group> | ||
|
||
<div class="mt-20px"> | ||
<n-empty v-if="resultOptions.length === 0" description="暂无搜索结果" /> | ||
<SearchResult v-else v-model:path="activePath" :options="resultOptions" @enter="handleEnter" /> | ||
</div> | ||
<template #footer> | ||
<SearchFooter v-if="!isMobile" /> | ||
</template> | ||
</n-modal> | ||
</template> | ||
|
||
<style lang="scss" scoped></style> |
57 changes: 57 additions & 0 deletions
57
src/layouts/modules/global-search/components/search-result.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
<script lang="ts" setup> | ||
import { useThemeStore } from '@/store/modules/theme'; | ||
import { $t } from '@/locales'; | ||
defineOptions({ name: 'SearchResult' }); | ||
interface Props { | ||
options: App.Global.Menu[]; | ||
} | ||
defineProps<Props>(); | ||
interface Emits { | ||
(e: 'enter'): void; | ||
} | ||
const emit = defineEmits<Emits>(); | ||
const theme = useThemeStore(); | ||
const active = defineModel<string>('path', { required: true }); | ||
/** 鼠标移入 */ | ||
async function handleMouse(item: App.Global.Menu) { | ||
active.value = item.routePath; | ||
} | ||
function handleTo() { | ||
emit('enter'); | ||
} | ||
</script> | ||
|
||
<template> | ||
<n-scrollbar> | ||
<div class="pb-12px"> | ||
<template v-for="item in options" :key="item.routePath"> | ||
<div | ||
class="mt-8px h-56px flex-y-center cursor-pointer justify-between rounded-4px bg-#e5e7eb px-14px dark:bg-dark" | ||
:style="{ | ||
background: item.routePath === active ? theme.themeColor : '', | ||
color: item.routePath === active ? '#fff' : '' | ||
}" | ||
@click="handleTo" | ||
@mouseenter="handleMouse(item)" | ||
> | ||
<component :is="item.icon" /> | ||
<span class="ml-5px flex-1"> | ||
{{ (item.i18nKey && $t(item.i18nKey)) || item.label }} | ||
</span> | ||
<icon-ant-design-enter-outlined class="icon mr-3px p-2px text-20px" /> | ||
</div> | ||
</template> | ||
</div> | ||
</n-scrollbar> | ||
</template> | ||
|
||
<style lang="scss" scoped></style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
<script lang="ts" setup> | ||
import { useBoolean } from '@sa/hooks'; | ||
import SearchModal from './components/search-modal.vue'; | ||
defineOptions({ name: 'GlobalSearch' }); | ||
const { bool: show, toggle } = useBoolean(); | ||
function handleSearch() { | ||
toggle(); | ||
} | ||
</script> | ||
|
||
<template> | ||
<ButtonIcon tooltip-content="搜索" @click="handleSearch"> | ||
<icon-uil-search class="text-20px" /> | ||
</ButtonIcon> | ||
<SearchModal v-model:show="show" /> | ||
</template> | ||
|
||
<style lang="scss" scoped></style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters