修改了搜索提示功能

This commit is contained in:
Yakumo Hokori 2025-04-27 03:02:04 +08:00
parent 9e91195bf0
commit a2e6660ec7
3 changed files with 209 additions and 24 deletions

View File

@ -1,2 +1,2 @@
# 生产环境变量
VITE_API_BASE_URL=https://lux.llvho.com/ssapi
VITE_API_BASE_URL=/api

View File

@ -95,10 +95,34 @@ export default defineComponent({
selectNext() {
if (this.suggestions.length === 0) return;
this.selectedIndex = Math.min(this.selectedIndex + 1, this.suggestions.length - 1);
this.$nextTick(() => {
this.scrollToSelected();
});
},
selectPrev() {
if (this.suggestions.length === 0) return;
this.selectedIndex = Math.max(this.selectedIndex - 1, -1);
this.$nextTick(() => {
this.scrollToSelected();
});
},
scrollToSelected() {
if (this.selectedIndex < 0) return;
const container = document.querySelector('.suggestions-container');
const selectedItem = document.querySelector('.selected-item');
if (!container || !selectedItem) return;
const containerRect = container.getBoundingClientRect();
const selectedRect = selectedItem.getBoundingClientRect();
//
if (selectedRect.bottom > containerRect.bottom) {
//
container.scrollTop += selectedRect.bottom - containerRect.bottom;
} else if (selectedRect.top < containerRect.top) {
//
container.scrollTop -= containerRect.top - selectedRect.top;
}
},
fetchSuggestions: _.debounce(async function() {
//
@ -110,7 +134,11 @@ export default defineComponent({
this.loading = true
try {
const response = await fetch(`/api/completion?q=${encodeURIComponent(this.searchQuery)}`)
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL
const url = new URL(`${apiBaseUrl}/completion`, window.location.origin)
url.searchParams.set('q', this.searchQuery)
const response = await fetch(url)
const data = await response.json()
// searchQuery
if (this.searchQuery.trim()) {

View File

@ -9,22 +9,40 @@
/>
</div>
<div class="search-box">
<router-link to="/" class="home-logo">
<div class="logo">
<h1>Hokori Search</h1>
</div>
</router-link>
<input
type="text"
v-model="newSearchQuery"
placeholder="输入搜索关键词"
@keyup.enter="handleSearch"
/>
<el-button @click="handleSearch" type="warning">搜索</el-button>
</div>
<h1 class="search-title">搜索: "{{ searchQuery }}</h1>
<h1 class="search-title">搜索: {{ searchQuery }}</h1>
<div class="search-box-wrapper">
<div class="search-box">
<router-link to="/" class="home-logo">
<div class="logo">
<h1>Hokori Search</h1>
</div>
</router-link>
<input
type="text"
v-model="newSearchQuery"
placeholder="输入搜索关键词"
@keyup.enter="handleSearch"
@keydown.down.prevent="selectNext"
@keydown.up.prevent="selectPrev"
/>
<el-button @click="handleSearch" type="warning">搜索</el-button>
</div>
<div v-if="suggestions.length > 0" class="suggestions-container">
<div
v-for="(item, index) in suggestions"
:key="index"
class="suggestion-item"
:class="{'selected-item': index === selectedIndex}"
@click="selectSuggestion(item)"
>
{{ item }}
</div>
</div>
</div>
<div class="search-results">
<div v-if="loading" class="loading">加载中...</div>
@ -78,6 +96,7 @@
import $ from 'jquery'
import { defineComponent } from 'vue'
import { useThemeStore } from '../stores/theme'
import _ from 'lodash'
export default defineComponent({
name: 'ResultsPage',
@ -113,7 +132,10 @@ export default defineComponent({
totalPages: 1,
results: [],
loading: false,
error: null
suggestionLoading: false,
error: null,
suggestions: [],
selectedIndex: -1
}
},
created() {
@ -137,6 +159,15 @@ export default defineComponent({
}
},
deep: true
},
newSearchQuery: {
handler() {
this.selectedIndex = -1;
this.fetchSuggestions();
}
},
suggestions() {
this.selectedIndex = -1;
}
},
methods: {
@ -184,15 +215,82 @@ export default defineComponent({
}
},
handleSearch() {
if (!this.newSearchQuery.trim()) return
const query = this.selectedIndex >= 0
? this.suggestions[this.selectedIndex]
: this.newSearchQuery.trim();
if (!query) return
this.$router.push({
path: '/search',
query: {
q: this.newSearchQuery,
q: query,
page: 1
}
})
},
selectSuggestion(item) {
this.newSearchQuery = item;
this.handleSearch();
},
selectNext() {
if (this.suggestions.length === 0) return;
this.selectedIndex = Math.min(this.selectedIndex + 1, this.suggestions.length - 1);
this.$nextTick(() => {
this.scrollToSelected();
});
},
selectPrev() {
if (this.suggestions.length === 0) return;
this.selectedIndex = Math.max(this.selectedIndex - 1, -1);
this.$nextTick(() => {
this.scrollToSelected();
});
},
scrollToSelected() {
if (this.selectedIndex < 0) return;
const container = document.querySelector('.suggestions-container');
const selectedItem = document.querySelector('.selected-item');
if (!container || !selectedItem) return;
const containerRect = container.getBoundingClientRect();
const selectedRect = selectedItem.getBoundingClientRect();
//
if (selectedRect.bottom > containerRect.bottom) {
//
container.scrollTop += selectedRect.bottom - containerRect.bottom;
} else if (selectedRect.top < containerRect.top) {
//
container.scrollTop -= containerRect.top - selectedRect.top;
}
},
fetchSuggestions: _.debounce(async function() {
//
const query = this.newSearchQuery.trim()
if (!query) {
this.suggestions = []
return
}
this.suggestionLoading = true
try {
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL
const url = new URL(`${apiBaseUrl}/completion`, window.location.origin)
url.searchParams.set('q', this.newSearchQuery)
const response = await fetch(url)
const data = await response.json()
// searchQuery
if (this.newSearchQuery.trim()) {
this.suggestions = data.suggestions || []
}
} catch (error) {
console.error('获取搜索建议失败:', error)
this.suggestions = []
} finally {
this.suggestionLoading = false
}
}, 300),
goToPage(page) {
if (page < 1 || page > this.totalPages) return
this.$router.push({
@ -211,10 +309,15 @@ export default defineComponent({
</script>
<style scoped>
.search-box {
display: flex;
.search-box-wrapper {
position: relative;
max-width: 600px;
margin: 20px auto;
}
.search-box {
display: flex;
width: 100%;
gap: 10px;
align-items: center;
}
@ -246,6 +349,7 @@ export default defineComponent({
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 16px;
position: relative;
}
.dark-mode .search-box input {
@ -289,6 +393,14 @@ export default defineComponent({
color: var(--uv-styles-color-text-default);
}
.dark-mode .about a {
color: #e6a23c
}
.dark-mode .about a:visited {
color: #f3f03f;
}
.search-title {
margin-top: 80px;
text-align: center;
@ -327,14 +439,59 @@ export default defineComponent({
color: #aaa;
}
.dark-mode .result-item h3 {
color: #80a0c2;
.dark-mode .result-item h3 a {
color: #e6a23c;
}
.dark-mode .result-item h3 a:visited {
color: #f3f03f;
}
.result-item p {
margin-bottom: 0;
}
.suggestions-container {
position: absolute;
width: calc(100% - 220px); /* 调整宽度以匹配输入框 */
max-height: 300px;
overflow-y: auto;
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
z-index: 1000;
top: 100%; /* 位于搜索框正下方 */
left: 200px; /* 与输入框左侧对齐 */
margin-top: 5px;
}
.suggestion-item {
padding: 12px 20px;
cursor: pointer;
transition: background-color 0.3s;
}
.suggestion-item:hover {
background-color: #f5f5f5;
}
.dark-mode .suggestions-container {
background-color: #2d2d2d;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.3);
}
.dark-mode .suggestion-item:hover {
background-color: #3d3d3d;
}
.selected-item {
background-color: #f0f0f0 !important;
}
.dark-mode .selected-item {
background-color: #4d4d4d !important;
}
.loading, .error, .no-results {
text-align: center;
padding: 20px;