Snavigation/src/components/Suggestions.vue
2023-07-30 18:39:43 +08:00

272 lines
7.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<Transition name="fadeDown" mode="out-in">
<div
v-if="
set.showSuggestions &&
status.siteStatus === 'focus' &&
searchKeyword !== null
"
class="suggestions"
:style="{ height: `${suggestionsHeights}px` }"
>
<!-- 快捷操作 -->
<Transition
name="fade"
mode="out-in"
@after-enter="changeSuggestionsHeights"
@after-leave="changeSuggestionsHeights"
>
<div
v-if="searchKeyword !== null"
class="special-result"
ref="specialallResultsRef"
>
<!-- 快捷翻译 -->
<div
v-if="searchKeywordType === 'text'"
class="s-result"
@click="toSearch(keyWord, 2)"
>
<SvgIcon iconName="icon-translation-two" />
<span class="text">快捷翻译:{{ keyWord }}</span>
</div>
<!-- 直接访问 -->
<div
v-if="searchKeywordType !== 'text'"
class="s-result"
@click="
toSearch(searchKeyword, searchKeywordType === 'email' ? 3 : 4)
"
>
<SvgIcon
:iconName="`icon-${
searchKeywordType === 'email' ? 'email' : 'link'
}`"
/>
<span class="text">
{{
searchKeywordType === "email" ? "发送邮件至" : "直接访问"
}}{{ searchKeyword }}
</span>
</div>
</div>
</Transition>
<!-- 搜索建议 -->
<Transition
name="fade"
mode="out-in"
@after-enter="changeSuggestionsHeights"
@after-leave="changeSuggestionsHeights"
>
<div
v-if="
searchKeyword !== null &&
searchKeywordType === 'text' &&
searchSuggestionsData[0]
"
class="all-result"
ref="allResultsRef"
>
<div
v-for="item in searchSuggestionsData"
class="s-result"
:key="item"
@click="toSearch(item, 1)"
>
<SvgIcon iconName="icon-search" className="search" />
<span class="text">{{ item }}</span>
</div>
</div>
</Transition>
</div>
</Transition>
</template>
<script setup>
import { nextTick, ref, watch } from "vue";
import { statusStore, setStore } from "@/stores";
import { getSearchSuggestions } from "@/api";
import debounce from "@/utils/debounce";
import identifyInput from "@/utils/identifyInput";
const set = setStore();
const status = statusStore();
const emit = defineEmits(["toSearch"]);
// 搜索关键字
const searchKeyword = ref(null);
// 搜索关键字类别
const searchKeywordType = ref("text");
// 搜索建议数据
const searchSuggestionsData = ref([]);
// 搜索建议元素
const specialallResultsRef = ref(null);
const allResultsRef = ref(null);
// 搜索建议高度
const suggestionsHeights = ref(0);
// 接收搜索框内容
const props = defineProps({
// 搜索关键字
keyWord: {
type: String,
required: true,
},
});
// 搜索框联想
const keywordsSearch = debounce((val) => {
const searchValue = val?.trim();
// 是否为空
if (!searchValue || searchValue === "") {
searchKeyword.value = null;
return false;
}
// 赋值关键字
searchKeyword.value = searchValue;
// 若为文字
if (searchKeyword.value && searchKeywordType.value === "text") {
console.log(val + "的搜索建议");
// 调用搜索建议
getSearchSuggestions(searchValue)
.then((res) => {
console.log(res);
// 写入结果
searchSuggestionsData.value = Array.from(res);
// 计算高度
nextTick(() => {
changeSuggestionsHeights();
});
})
.catch((error) => {
// 清空结果
searchSuggestionsData.value = [];
console.error("处理搜索建议发生错误:", error);
});
}
}, 300);
// 响应键盘事件
const keyboardEvents = (keyCode, event) => {
try {
// 获取元素
const mainInput = document.getElementById("main-input");
// 38 上 / 40 下
if (keyCode === 38 || keyCode === 40) {
// 阻止默认事件
event.preventDefault();
if (mainInput && allResultsRef.value && searchSuggestionsData.value[0]) {
const suggestionItems =
allResultsRef.value.querySelectorAll(".s-result");
if (suggestionItems.length > 0) {
// 获取当前已聚焦的元素
const focusedItem = document.querySelector(".s-result.focus");
// 确定当前聚焦的元素在列表中的索引
const currentIndex = Array.from(suggestionItems).indexOf(focusedItem);
// 移除所有元素的选中状态
suggestionItems.forEach((item) =>
item.classList.toggle("focus", false)
);
// 计算下一个要聚焦的元素的索引
let nextIndex = keyCode === 38 ? currentIndex - 1 : currentIndex + 1;
// 确保索引不越界
nextIndex = Math.max(
0,
Math.min(nextIndex, suggestionItems.length - 1)
);
// 操作元素
if (nextIndex !== -1) {
suggestionItems[nextIndex].classList.toggle("focus", true);
mainInput.value =
suggestionItems[nextIndex].querySelector(".text").textContent;
}
}
}
}
// 13 回车
if (keyCode === 13) {
toSearch(mainInput.value, 1);
}
} catch (error) {
console.error("键盘事件出现错误:" + error);
}
};
// 计算元素高度并改变
const changeSuggestionsHeights = () => {
try {
const allResultsHeight = allResultsRef.value?.offsetHeight;
const specialallResultsHeight = specialallResultsRef.value?.offsetHeight;
suggestionsHeights.value =
(specialallResultsHeight || 0) + (allResultsHeight || 0);
} catch (error) {
console.error("计算高度时出现错误:" + error);
}
};
// 触发父组件搜索事件
const toSearch = (val, type = 1) => {
emit("toSearch", val, type);
};
// 监听搜索框变化
watch(
() => props.keyWord,
(val) => {
// 清空结果
searchSuggestionsData.value = [];
// 判断类型
searchKeywordType.value = identifyInput(val);
// 调用搜索结果
keywordsSearch(val);
}
);
// 暴露方法
defineExpose({ keyboardEvents });
</script>
<style lang="scss" scoped>
.suggestions {
position: absolute;
top: 0;
left: 0;
width: 100%;
max-height: 44vh;
overflow-y: auto;
color: var(--main-text-color);
background-color: var(--main-background-light-color);
backdrop-filter: blur(30px) saturate(1.25);
border-radius: 16px;
transition: height 0.2s ease, opacity 0.3s ease, transform 0.3s ease;
z-index: 1;
.all-result,
.special-result {
.s-result {
cursor: pointer;
box-sizing: border-box;
display: flex;
flex-direction: row;
align-items: center;
padding: 6px 12px;
font-size: 14px;
transition: background-color 0.3s, padding-left 0.3s;
.i-icon {
opacity: 0.8;
margin-right: 8px;
}
.text {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&:hover,
&.focus {
background-color: var(--main-background-light-color);
padding-left: 18px;
}
}
}
}
</style>