mirror of
https://github.com/caojiezi2003/Snavigation.git
synced 2025-04-05 07:16:57 +00:00
272 lines
7.4 KiB
Vue
272 lines
7.4 KiB
Vue
<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>
|