feat: search
This commit is contained in:
parent
452672dedc
commit
556c371eef
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
Loading…
Reference in New Issue