Skip to content

Commit

Permalink
feat: perfect search modal
Browse files Browse the repository at this point in the history
  • Loading branch information
chansee97 committed May 5, 2024
1 parent 61bbded commit 1ccc3f3
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 24 deletions.
3 changes: 2 additions & 1 deletion locales/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"confirm": "Confirm",
"close": "Closure",
"reload": "Refresh",
"choose": "Choose"
"choose": "Choose",
"navigate": "Navigate"
},
"app": {
"loginOut": "Login out",
Expand Down
3 changes: 2 additions & 1 deletion locales/zh_CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"cancel": "取消",
"reload": "刷新",
"close": "关闭",
"choose": "选择"
"choose": "选择",
"navigate": "切换"
},
"app": {
"loginOut": "退出登录",
Expand Down
184 changes: 162 additions & 22 deletions src/layouts/components/header/Search.vue
Original file line number Diff line number Diff line change
@@ -1,20 +1,44 @@
<script setup lang="ts">
import { NFlex, NTag, NText } from 'naive-ui'
import { useRouteStore } from '@/store'
import { renderIcon } from '@/utils'
import { useBoolean } from '@/hooks'
const routeStore = useRouteStore()
// 搜索值
const searchValue = ref('')
// 选中索引
const selectedIndex = ref<number>(0)
const { bool: showModal, setTrue: openModal, setFalse: closeModal, toggle: toggleModal } = useBoolean(false)
const { ctrl_k, arrowup, arrowdown, enter/* keys you want to monitor */ } = useMagicKeys({
passive: false,
onEventFired(e) {
if (e.ctrlKey && e.key === 'k' && e.type === 'keydown')
e.preventDefault()
},
})
// 监听全局热键
watchEffect(() => {
if (ctrl_k.value)
toggleModal()
})
const { t } = useI18n()
// 计算符合条件的菜单选项
const options = computed(() => {
if (!searchValue.value)
return []
return routeStore.rowRoutes.filter((item) => {
const conditions = [
t(`route.${String(item.name)}`, item['meta.title'] || item.name)?.includes(searchValue.value),
item.path?.includes(searchValue.value),
]
return conditions.some(condition => condition)
return conditions.some(condition => !item['meta.hide'] && condition)
}).map((item) => {
return {
label: t(`route.${String(item.name)}`, item['meta.title'] || item.name),
Expand All @@ -24,36 +48,152 @@ const options = computed(() => {
})
})
function renderLabel(option: any) {
return h(NFlex, {}, {
default: () => [
h(NTag, { size: 'small', type: 'primary', bordered: false }, { icon: renderIcon(option.icon), default: () => option.label }),
h(NText, { depth: 3 }, { default: () => option.value }),
],
})
const router = useRouter()
// 关闭回调
function handleClose() {
searchValue.value = ''
selectedIndex.value = 0
closeModal()
}
const router = useRouter()
// 输入框改变,索引重置
function handleInputChange() {
selectedIndex.value = 0
}
// 选择菜单选项
function handleSelect(value: string) {
handleClose()
router.push(value)
nextTick(() => {
searchValue.value = ''
})
}
watchEffect(() => {
// 没有打开弹窗或没有搜索结果时,不操作
if (!showModal.value || !options.value.length)
return
if (arrowup.value)
handleArrowup()
if (arrowdown.value)
handleArrowdown()
if (enter.value)
handleEnter()
})
const scrollbarRef = ref()
// 上箭头操作
function handleArrowup() {
if (selectedIndex.value === 0)
selectedIndex.value = options.value.length - 1
else
selectedIndex.value--
nextTick(() => {
scrollbarRef.value?.scrollTo({
top: selectedIndex.value * 70,
})
})
}
// 下箭头操作
function handleArrowdown() {
if (selectedIndex.value === options.value.length - 1)
selectedIndex.value = 0
else
selectedIndex.value++
nextTick(() => {
scrollbarRef.value?.scrollTo({
top: selectedIndex.value * 70,
})
})
}
// 回车键操作
function handleEnter() {
const target = options.value[selectedIndex.value]
if (target)
handleSelect(target.value)
}
</script>

<template>
<n-auto-complete
v-model:value="searchValue" class="w-20em m-r-1em" :input-props="{
autocomplete: 'disabled',
}" :options="options" :render-label="renderLabel" :placeholder="$t('app.searchPlaceholder')" clearable @select="handleSelect"
<CommonWrapper @click="openModal">
<icon-park-outline-search /><n-tag round size="small" class="font-mono cursor-pointer">
CtrlK
</n-tag>
</CommonWrapper>
<n-modal
v-model:show="showModal"
class="w-560px fixed top-100px inset-x-0"
size="small"
preset="card"
:segmented="{
content: true,
footer: true,
}"
:closable="false"
@after-leave="handleClose"
>
<template #prefix>
<n-icon>
<icon-park-outline-search />
</n-icon>
<template #header>
<n-input v-model:value="searchValue" :placeholder="$t('app.searchPlaceholder')" clearable size="large" @input="handleInputChange">
<template #prefix>
<n-icon>
<icon-park-outline-search />
</n-icon>
</template>
</n-input>
</template>
</n-auto-complete>
</template>
<n-scrollbar ref="scrollbarRef" class="h-600px">
<ul
v-if="options.length"
class="flex flex-col gap-8px p-1 p-r-3"
>
<n-el
v-for="(option, index) in options"
:key="option.value" tag="li" role="option"
class="cursor-pointer shadow h-62px"
:class="{ 'text-[var(--base-color)] bg-[var(--primary-color-hover)]': index === selectedIndex }"
@click="handleSelect(option.value)"
@mouseover.stop="selectedIndex = index"
>
<div class="grid grid-rows-2 grid-cols-[40px_1fr_30px] h-full p-2">
<nova-icon :icon="option.icon" class="row-span-2 place-self-center" />
<span>{{ option.label }}</span>
<icon-park-outline-right class="row-span-2 place-self-center" />
<span class="op-70">{{ option.value }}</span>
</div>
</n-el>
</ul>

<n-empty v-else size="large" class="h-600px flex-center" />
</n-scrollbar>

<style scoped></style>
<template #footer>
<n-flex>
<div class="flex-y-center gap-1">
<svg width="15" height="15" aria-label="Enter key" role="img"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2"><path d="M12 3.53088v3c0 1-1 2-2 2H4M7 11.53088l-3-3 3-3" /></g></svg>
<span>{{ $t('common.choose') }}</span>
</div>
<div class="flex-y-center gap-1">
<svg width="15" height="15" aria-label="Arrow down" role="img"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2"><path d="M7.5 3.5v8M10.5 8.5l-3 3-3-3" /></g></svg>
<svg width="15" height="15" aria-label="Arrow up" role="img"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2"><path d="M7.5 11.5v-8M10.5 6.5l-3-3-3 3" /></g></svg>
<span>{{ $t('common.navigate') }}</span>
</div>
<div class="flex-y-center gap-1">
<svg width="15" height="15" aria-label="Escape key" role="img"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2"><path d="M13.6167 8.936c-.1065.3583-.6883.962-1.4875.962-.7993 0-1.653-.9165-1.653-2.1258v-.5678c0-1.2548.7896-2.1016 1.653-2.1016.8634 0 1.3601.4778 1.4875 1.0724M9 6c-.1352-.4735-.7506-.9219-1.46-.8972-.7092.0246-1.344.57-1.344 1.2166s.4198.8812 1.3445.9805C8.465 7.3992 8.968 7.9337 9 8.5c.032.5663-.454 1.398-1.4595 1.398C6.6593 9.898 6 9 5.963 8.4851m-1.4748.5368c-.2635.5941-.8099.876-1.5443.876s-1.7073-.6248-1.7073-2.204v-.4603c0-1.0416.721-2.131 1.7073-2.131.9864 0 1.6425 1.031 1.5443 2.2492h-2.956" /></g></svg>
<span>{{ $t('common.close') }}</span>
</div>
</n-flex>
</template>
</n-modal>
</template>
1 change: 1 addition & 0 deletions src/styles/index.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@import './reset.css';
@import './transition.css';
@import './navie.css';

html,
body,
Expand Down
3 changes: 3 additions & 0 deletions src/styles/navie.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.n-modal-mask {
backdrop-filter: blur(2px);
}

0 comments on commit 1ccc3f3

Please sign in to comment.