修改了搜索提示功能
This commit is contained in:
parent
9e91195bf0
commit
a2e6660ec7
@ -1,2 +1,2 @@
|
|||||||
# 生产环境变量
|
# 生产环境变量
|
||||||
VITE_API_BASE_URL=https://lux.llvho.com/ssapi
|
VITE_API_BASE_URL=/api
|
@ -95,10 +95,34 @@ export default defineComponent({
|
|||||||
selectNext() {
|
selectNext() {
|
||||||
if (this.suggestions.length === 0) return;
|
if (this.suggestions.length === 0) return;
|
||||||
this.selectedIndex = Math.min(this.selectedIndex + 1, this.suggestions.length - 1);
|
this.selectedIndex = Math.min(this.selectedIndex + 1, this.suggestions.length - 1);
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.scrollToSelected();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
selectPrev() {
|
selectPrev() {
|
||||||
if (this.suggestions.length === 0) return;
|
if (this.suggestions.length === 0) return;
|
||||||
this.selectedIndex = Math.max(this.selectedIndex - 1, -1);
|
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() {
|
fetchSuggestions: _.debounce(async function() {
|
||||||
// 双重检查确保空值时立即清除建议
|
// 双重检查确保空值时立即清除建议
|
||||||
@ -110,7 +134,11 @@ export default defineComponent({
|
|||||||
|
|
||||||
this.loading = true
|
this.loading = true
|
||||||
try {
|
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()
|
const data = await response.json()
|
||||||
// 确保当前searchQuery与请求时一致
|
// 确保当前searchQuery与请求时一致
|
||||||
if (this.searchQuery.trim()) {
|
if (this.searchQuery.trim()) {
|
||||||
|
@ -9,22 +9,40 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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 class="search-results">
|
||||||
<div v-if="loading" class="loading">加载中...</div>
|
<div v-if="loading" class="loading">加载中...</div>
|
||||||
@ -78,6 +96,7 @@
|
|||||||
import $ from 'jquery'
|
import $ from 'jquery'
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
import { useThemeStore } from '../stores/theme'
|
import { useThemeStore } from '../stores/theme'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'ResultsPage',
|
name: 'ResultsPage',
|
||||||
@ -113,7 +132,10 @@ export default defineComponent({
|
|||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
results: [],
|
results: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null
|
suggestionLoading: false,
|
||||||
|
error: null,
|
||||||
|
suggestions: [],
|
||||||
|
selectedIndex: -1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
@ -137,6 +159,15 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
deep: true
|
deep: true
|
||||||
|
},
|
||||||
|
newSearchQuery: {
|
||||||
|
handler() {
|
||||||
|
this.selectedIndex = -1;
|
||||||
|
this.fetchSuggestions();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
suggestions() {
|
||||||
|
this.selectedIndex = -1;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -184,15 +215,82 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleSearch() {
|
handleSearch() {
|
||||||
if (!this.newSearchQuery.trim()) return
|
const query = this.selectedIndex >= 0
|
||||||
|
? this.suggestions[this.selectedIndex]
|
||||||
|
: this.newSearchQuery.trim();
|
||||||
|
|
||||||
|
if (!query) return
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
path: '/search',
|
path: '/search',
|
||||||
query: {
|
query: {
|
||||||
q: this.newSearchQuery,
|
q: query,
|
||||||
page: 1
|
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) {
|
goToPage(page) {
|
||||||
if (page < 1 || page > this.totalPages) return
|
if (page < 1 || page > this.totalPages) return
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
@ -211,10 +309,15 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.search-box {
|
.search-box-wrapper {
|
||||||
display: flex;
|
position: relative;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
margin: 20px auto;
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@ -246,6 +349,7 @@ export default defineComponent({
|
|||||||
border: 1px solid #d9d9d9;
|
border: 1px solid #d9d9d9;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark-mode .search-box input {
|
.dark-mode .search-box input {
|
||||||
@ -289,6 +393,14 @@ export default defineComponent({
|
|||||||
color: var(--uv-styles-color-text-default);
|
color: var(--uv-styles-color-text-default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark-mode .about a {
|
||||||
|
color: #e6a23c
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode .about a:visited {
|
||||||
|
color: #f3f03f;
|
||||||
|
}
|
||||||
|
|
||||||
.search-title {
|
.search-title {
|
||||||
margin-top: 80px;
|
margin-top: 80px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@ -327,14 +439,59 @@ export default defineComponent({
|
|||||||
color: #aaa;
|
color: #aaa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark-mode .result-item h3 {
|
.dark-mode .result-item h3 a {
|
||||||
color: #80a0c2;
|
color: #e6a23c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode .result-item h3 a:visited {
|
||||||
|
color: #f3f03f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-item p {
|
.result-item p {
|
||||||
margin-bottom: 0;
|
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 {
|
.loading, .error, .no-results {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user