feat: search

This commit is contained in:
KirisameVanilla 2025-09-09 15:08:39 +08:00
parent 452672dedc
commit 556c371eef
No known key found for this signature in database
GPG Key ID: A68EE6C617D68238
3 changed files with 269 additions and 46 deletions

View File

@ -1,24 +1,48 @@
import apiClient from './client' import apiClient from './client'
export interface WordDefinition { export interface WordContent {
word: string chi_exp: string
part_of_speech: string
meaning: string
example: string example: string
} }
export interface WordDefinition {
query: string
pos: string[]
contents: WordContent[]
}
export interface SearchParams { export interface SearchParams {
lang_pref: string lang_pref: string
query_word: string query_word: string
} }
export interface SearchSuggestParams {
query: string
language: string
}
export interface SearchSuggestResponse {
list: string[]
}
// 搜索单词 // 搜索单词
export const searchWord = async (params: SearchParams): Promise<WordDefinition[]> => { export const searchWord = async (params: SearchParams): Promise<WordDefinition> => {
const response = await apiClient.get('/search', { const packet = {
params: { language: params.lang_pref,
lang_pref: params.lang_pref, query: params.query_word,
query_word: params.query_word sort: "relevance",
order: "asc"
} }
const response = await apiClient.post('/search', packet)
console.log(response)
return response.data
}
// 搜索推荐
export const searchSuggest = async (params: SearchSuggestParams): Promise<SearchSuggestResponse> => {
const response = await apiClient.post('/search/list', {
query: params.query,
language: params.language
}) })
return response.data return response.data
} }

View File

@ -5,12 +5,15 @@
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 bg-blue-700 px-8 py-3 border-none rounded-full outline-none text-white -translate-y-1/2 appearance-none cursor-pointer"
> >
<option value="jp">日语</option>
<option value="fr">法语</option> <option value="fr">法语</option>
<option value="en">英语</option>
</select> </select>
<input <input
v-model="searchQuery" v-model="searchQuery"
@keyup.enter="handleSearch" @keyup.enter="handleSearch"
@input="handleInputChange"
@focus="showSuggestions = true"
@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-[207px] border-[5px] border-blue-700 focus:border-blue-500 rounded-full outline-none w-full h-[56px] text-xl"
@ -19,18 +22,102 @@
@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" 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"
/> />
<!-- 搜索推荐下拉框 -->
<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> </div>
</section> </section>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, watch } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { searchSuggest } from '../api/dict'
const router = useRouter() const router = useRouter()
const searchQuery = ref('') const searchQuery = ref('')
const selectedLang = ref('fr') const selectedLang = ref('jp')
const suggestions = ref<string[]>([])
const showSuggestions = ref(false)
const debounceTimer = ref<number | null>(null)
const isLoading = ref(false)
//
const debounce = (fn: Function, delay: number) => {
return (...args: any[]) => {
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 {
isLoading.value = true
const response = await searchSuggest({
query: query.trim(),
language: selectedLang.value
})
suggestions.value = response.list || []
} catch (error) {
console.error('获取搜索建议失败:', error)
suggestions.value = []
} finally {
isLoading.value = false
}
}
//
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()
}
//
const handleSearch = () => { const handleSearch = () => {
if (searchQuery.value.trim()) { if (searchQuery.value.trim()) {
// //
@ -43,4 +130,11 @@ const handleSearch = () => {
}) })
} }
} }
//
watch(selectedLang, () => {
if (searchQuery.value.trim()) {
debouncedFetchSuggestions(searchQuery.value)
}
})
</script> </script>

View File

@ -6,19 +6,40 @@
<section class="bg-white py-12"> <section class="bg-white py-12">
<div class="mx-auto px-4 max-w-[1030px]"> <div class="mx-auto px-4 max-w-[1030px]">
<div class="mb-8 text-center"> <div class="mb-8 text-center">
<h1 class="mb-4 font-deserta text-blue-700 text-4xl">法语词典查询</h1> <h1 class="mb-4 font-deserta text-blue-700 text-4xl">多语言词典查询</h1>
<p class="font-inter text-gray-600">输入法语单词获取详细释义和例句</p> <p class="font-inter text-gray-600">输入单词获取详细释义和例句</p>
</div> </div>
<div class="mx-auto max-w-[600px]"> <div class="mx-auto max-w-[600px]">
<div class="flex gap-4"> <div class="relative flex gap-4">
<input <div class="relative flex-1">
v-model="searchQuery" <input
@keyup.enter="handleSearch" v-model="searchQuery"
type="text" @keyup.enter="handleSearch"
placeholder="请输入法语单词..." @input="handleInputChange"
class="flex-1 px-6 border-2 border-blue-700 focus:border-blue-500 rounded-full outline-none h-[60px] text-xl" @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 <button
@click="handleSearch" @click="handleSearch"
:disabled="loading || !searchQuery.trim()" :disabled="loading || !searchQuery.trim()"
@ -30,8 +51,8 @@
<div class="flex justify-center mt-4"> <div class="flex justify-center mt-4">
<select v-model="selectedLang" class="px-4 py-2 border border-gray-300 rounded"> <select v-model="selectedLang" class="px-4 py-2 border border-gray-300 rounded">
<option value="jp">日语</option>
<option value="fr">法语</option> <option value="fr">法语</option>
<option value="en">英语</option>
</select> </select>
</div> </div>
</div> </div>
@ -45,25 +66,39 @@
{{ error }} {{ error }}
</div> </div>
<div v-if="searchResults.length > 0" class="space-y-6"> <div v-if="searchResult" class="space-y-6">
<h2 class="mb-6 font-deserta text-blue-700 text-2xl">搜索结果</h2> <h2 class="mb-6 font-deserta text-blue-700 text-2xl">搜索结果</h2>
<div v-for="(result, index) in searchResults" :key="index" class="bg-white shadow-md p-6 rounded-lg"> <div class="bg-white shadow-md p-6 rounded-lg">
<div class="flex justify-between items-start mb-4"> <div class="flex justify-between items-start mb-4">
<h3 class="font-bold text-blue-700 text-2xl">{{ result.word }}</h3> <h3 class="font-bold text-blue-700 text-2xl">{{ searchResult.query }}</h3>
<span class="bg-blue-100 px-3 py-1 rounded-full text-blue-700 text-sm"> <div class="flex gap-2">
{{ result.part_of_speech }} <span
</span> v-for="(pos, index) in searchResult.pos"
:key="index"
class="bg-blue-100 px-3 py-1 rounded-full text-blue-700 text-sm"
>
{{ pos }}
</span>
</div>
</div> </div>
<div class="mb-4"> <div class="space-y-4">
<h4 class="mb-2 font-semibold text-gray-700">释义</h4> <div
<p class="text-gray-800 leading-relaxed">{{ result.meaning }}</p> v-for="(content, index) in searchResult.contents"
</div> :key="index"
class="pl-4 border-blue-200 border-l-4"
<div v-if="result.example"> >
<h4 class="mb-2 font-semibold text-gray-700">例句</h4> <div class="mb-2">
<p class="bg-gray-50 p-3 rounded text-gray-600 italic">{{ result.example }}</p> <h4 class="mb-2 font-semibold text-gray-700">释义</h4>
<p class="text-gray-800 leading-relaxed">{{ content.chi_exp }}</p>
</div>
<div v-if="content.example">
<h4 class="mb-2 font-semibold text-gray-700">例句</h4>
<p class="bg-gray-50 p-3 rounded text-gray-600 italic">{{ content.example }}</p>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -76,8 +111,8 @@
<div v-else-if="!hasSearched" class="py-12 text-gray-400 text-center"> <div v-else-if="!hasSearched" class="py-12 text-gray-400 text-center">
<div class="mb-4 text-6xl">🔍</div> <div class="mb-4 text-6xl">🔍</div>
<p class="text-xl">开始您的法语词汇探索之旅</p> <p class="text-xl">开始您的词汇探索之旅</p>
<p class="mt-2">在上方输入框中输入法语单词进行查询</p> <p class="mt-2">在上方输入框中输入单词进行查询</p>
</div> </div>
</div> </div>
</section> </section>
@ -87,19 +122,82 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted, 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, type WordDefinition } from '../api/dict' import { searchWord, searchSuggest, type WordDefinition } from '../api/dict'
const route = useRoute() const route = useRoute()
const searchQuery = ref('') const searchQuery = ref('')
const selectedLang = ref('fr') const selectedLang = ref('jp')
const searchResults = ref<WordDefinition[]>([]) 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) => {
return (...args: any[]) => {
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(() => { onMounted(() => {
@ -127,17 +225,24 @@ const handleSearch = async () => {
hasSearched.value = true hasSearched.value = true
try { try {
const results = await searchWord({ const result = await searchWord({
lang_pref: selectedLang.value, lang_pref: selectedLang.value,
query_word: searchQuery.value.trim() query_word: searchQuery.value.trim()
}) })
searchResults.value = results searchResult.value = result
} catch (err: any) { } catch (err: any) {
console.error('Search error:', err) console.error('Search error:', err)
error.value = err.response?.data?.detail || '搜索失败,请稍后重试' error.value = err.response?.data?.detail || '搜索失败,请稍后重试'
searchResults.value = [] searchResult.value = null
} finally { } finally {
loading.value = false loading.value = false
} }
} }
//
watch(selectedLang, () => {
if (searchQuery.value.trim()) {
debouncedFetchSuggestions(searchQuery.value)
}
})
</script> </script>