This commit is contained in:
KirisameVanilla 2025-09-09 15:18:36 +08:00
parent 556c371eef
commit 5e45e039b3
No known key found for this signature in database
GPG Key ID: A68EE6C617D68238
2 changed files with 56 additions and 157 deletions

View File

@ -1,9 +1,10 @@
<template> <template>
<section class="flex justify-center items-center bg-white h-[88px]"> <section class="flex justify-center items-center bg-white h-[88px]">
<div class="relative w-[804px]"> <div class="relative w-[804px]" :class="{ 'focused': isInputFocused }">
<select <select
v-model="selectedLang" v-model="selectedLang"
class="top-1/2 left-0 z-10 absolute bg-blue-700 px-8 py-3 border-none rounded-full outline-none text-white -translate-y-1/2 appearance-none cursor-pointer" class="top-1/2 left-0 z-10 absolute px-8 py-3 border-none rounded-full outline-none text-white transition-colors -translate-y-1/2 duration-200 appearance-none cursor-pointer"
:style="{ backgroundColor: isInputFocused ? '#3b82f6' : '#1d4ed8' }"
> >
<option value="jp">日语</option> <option value="jp">日语</option>
<option value="fr">法语</option> <option value="fr">法语</option>
@ -12,15 +13,19 @@
v-model="searchQuery" v-model="searchQuery"
@keyup.enter="handleSearch" @keyup.enter="handleSearch"
@input="handleInputChange" @input="handleInputChange"
@focus="showSuggestions = true" @focus="handleInputFocus"
@blur="handleInputBlur" @blur="handleInputBlur"
type="text" type="text"
placeholder="请输入单词" placeholder="请输入单词"
class="pr-[70px] pl-[207px] border-[5px] border-blue-700 focus:border-blue-500 rounded-full outline-none w-full h-[56px] text-xl" class="pr-[70px] pl-[160px] border-[5px] border-blue-700 focus:border-blue-500 rounded-full outline-none w-full h-[56px] text-xl transition-colors duration-200"
/> />
<button <button
@click="handleSearch" @click="handleSearch"
class="top-1/2 right-0 absolute bg-[length:60%] bg-[url('/images/search.png')] bg-blue-700 hover:bg-blue-600 bg-no-repeat bg-center rounded-full w-[56px] h-[56px] -translate-y-1/2" @mouseenter="handleButtonMouseEnter"
@mouseleave="handleButtonMouseLeave"
ref="searchButton"
class="top-1/2 right-0 absolute bg-[length:60%] bg-[url('/images/search.png')] bg-no-repeat bg-center rounded-full w-[56px] h-[56px] transition-colors -translate-y-1/2 duration-200"
:style="{ backgroundColor: buttonBgColor }"
/> />
<!-- 搜索推荐下拉框 --> <!-- 搜索推荐下拉框 -->
@ -42,7 +47,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from 'vue' import { ref, watch, computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { searchSuggest } from '../api/dict' import { searchSuggest } from '../api/dict'
@ -51,9 +56,19 @@ const searchQuery = ref('')
const selectedLang = ref('jp') const selectedLang = ref('jp')
const suggestions = ref<string[]>([]) const suggestions = ref<string[]>([])
const showSuggestions = ref(false) const showSuggestions = ref(false)
const isInputFocused = ref(false)
const isButtonHovered = ref(false)
const debounceTimer = ref<number | null>(null) const debounceTimer = ref<number | null>(null)
const isLoading = ref(false) const isLoading = ref(false)
//
const buttonBgColor = computed(() => {
if (isButtonHovered.value) {
return isInputFocused.value ? '#60a5fa' : '#2563eb' // hover
}
return isInputFocused.value ? '#3b82f6' : '#1d4ed8' //
})
// //
const debounce = (fn: Function, delay: number) => { const debounce = (fn: Function, delay: number) => {
return (...args: any[]) => { return (...args: any[]) => {
@ -102,14 +117,31 @@ const handleInputChange = () => {
} }
} }
//
const handleInputFocus = () => {
isInputFocused.value = true
showSuggestions.value = true
}
// //
const handleInputBlur = () => { const handleInputBlur = () => {
isInputFocused.value = false
// //
setTimeout(() => { setTimeout(() => {
showSuggestions.value = false showSuggestions.value = false
}, 200) }, 200)
} }
//
const handleButtonMouseEnter = () => {
isButtonHovered.value = true
}
//
const handleButtonMouseLeave = () => {
isButtonHovered.value = false
}
// //
const selectSuggestion = (suggestion: string) => { const selectSuggestion = (suggestion: string) => {
searchQuery.value = suggestion searchQuery.value = suggestion

View File

@ -2,62 +2,7 @@
<div> <div>
<AppHeader active="dict" /> <AppHeader active="dict" />
<!-- 搜索区域 --> <SearchBar />
<section class="bg-white py-12">
<div class="mx-auto px-4 max-w-[1030px]">
<div class="mb-8 text-center">
<h1 class="mb-4 font-deserta text-blue-700 text-4xl">多语言词典查询</h1>
<p class="font-inter text-gray-600">输入单词获取详细释义和例句</p>
</div>
<div class="mx-auto max-w-[600px]">
<div class="relative flex gap-4">
<div class="relative flex-1">
<input
v-model="searchQuery"
@keyup.enter="handleSearch"
@input="handleInputChange"
@focus="showSuggestions = true"
@blur="handleInputBlur"
type="text"
placeholder="请输入单词..."
class="px-6 border-2 border-blue-700 focus:border-blue-500 rounded-full outline-none w-full h-[60px] text-xl"
/>
<!-- 搜索推荐下拉框 -->
<div
v-if="showSuggestions && suggestions.length > 0"
class="top-full left-0 z-20 absolute bg-white shadow-lg mt-1 border border-gray-200 rounded-lg w-full max-h-60 overflow-y-auto"
>
<div
v-for="(suggestion, index) in suggestions"
:key="index"
@mousedown="selectSuggestion(suggestion)"
class="hover:bg-gray-100 px-4 py-2 text-left cursor-pointer"
>
{{ suggestion }}
</div>
</div>
</div>
<button
@click="handleSearch"
:disabled="loading || !searchQuery.trim()"
class="bg-blue-700 hover:bg-blue-600 disabled:opacity-50 rounded-full w-[120px] h-[60px] font-inter text-white disabled:cursor-not-allowed"
>
{{ loading ? '搜索中...' : '搜索' }}
</button>
</div>
<div class="flex justify-center mt-4">
<select v-model="selectedLang" class="px-4 py-2 border border-gray-300 rounded">
<option value="jp">日语</option>
<option value="fr">法语</option>
</select>
</div>
</div>
</div>
</section>
<!-- 搜索结果 --> <!-- 搜索结果 -->
<section class="bg-gray-50 py-12 min-h-[400px]"> <section class="bg-gray-50 py-12 min-h-[400px]">
@ -122,103 +67,22 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, watch } from 'vue' import { ref, watch } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import AppHeader from '../components/AppHeader.vue' import AppHeader from '../components/AppHeader.vue'
import AppFooter from '../components/AppFooter.vue' import AppFooter from '../components/AppFooter.vue'
import { searchWord, searchSuggest, type WordDefinition } from '../api/dict' import SearchBar from '../components/SearchBar.vue'
import { searchWord, type WordDefinition } from '../api/dict'
const route = useRoute() const route = useRoute()
const searchQuery = ref('')
const selectedLang = ref('jp')
const searchResult = ref<WordDefinition | null>(null) const searchResult = ref<WordDefinition | null>(null)
const loading = ref(false) const loading = ref(false)
const error = ref('') const error = ref('')
const hasSearched = ref(false) const hasSearched = ref(false)
const suggestions = ref<string[]>([])
const showSuggestions = ref(false)
const debounceTimer = ref<number | null>(null)
// //
const debounce = (fn: Function, delay: number) => { const handleSearch = async (query: string, lang: string) => {
return (...args: any[]) => { if (!query.trim()) return
if (debounceTimer.value) {
clearTimeout(debounceTimer.value)
}
debounceTimer.value = window.setTimeout(() => {
fn(...args)
}, delay)
}
}
//
const fetchSuggestions = async (query: string) => {
if (!query || query.length < 1) {
suggestions.value = []
return
}
try {
const response = await searchSuggest({
query: query.trim(),
language: selectedLang.value
})
suggestions.value = response.list || []
} catch (error) {
console.error('获取搜索建议失败:', error)
suggestions.value = []
}
}
//
const debouncedFetchSuggestions = debounce(fetchSuggestions, 300)
//
const handleInputChange = () => {
if (searchQuery.value.trim()) {
debouncedFetchSuggestions(searchQuery.value)
showSuggestions.value = true
} else {
suggestions.value = []
showSuggestions.value = false
}
}
//
const handleInputBlur = () => {
//
setTimeout(() => {
showSuggestions.value = false
}, 200)
}
//
const selectSuggestion = (suggestion: string) => {
searchQuery.value = suggestion
showSuggestions.value = false
handleSearch()
}
//
onMounted(() => {
const q = route.query.q as string
const lang = route.query.lang as string
if (q) {
searchQuery.value = q
}
if (lang) {
selectedLang.value = lang
}
//
if (q) {
handleSearch()
}
})
const handleSearch = async () => {
if (!searchQuery.value.trim()) return
loading.value = true loading.value = true
error.value = '' error.value = ''
@ -226,8 +90,8 @@ const handleSearch = async () => {
try { try {
const result = await searchWord({ const result = await searchWord({
lang_pref: selectedLang.value, lang_pref: lang,
query_word: searchQuery.value.trim() query_word: query.trim()
}) })
searchResult.value = result searchResult.value = result
} catch (err: any) { } catch (err: any) {
@ -239,10 +103,13 @@ const handleSearch = async () => {
} }
} }
// //
watch(selectedLang, () => { watch(() => route.query, (newQuery) => {
if (searchQuery.value.trim()) { const q = newQuery.q as string
debouncedFetchSuggestions(searchQuery.value) const lang = newQuery.lang as string || 'jp'
if (q) {
handleSearch(q, lang)
} }
}) }, { immediate: true })
</script> </script>