2025-10-24 23:10:22 +08:00

373 lines
7.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="search-results">
<!-- 搜索头部 -->
<div class="search-header">
<div class="search-info">
<h1 class="search-title">
<Icon icon="tabler:search" class="title-icon" />
{{ t('Search Results') }}
</h1>
<p class="search-subtitle" v-if="query">
{{ t('Search keyword') }}: "{{ query }}"
</p>
<p class="search-subtitle" v-if="!loading && results.length > 0">
{{ t('Found') }} {{ results.length }} {{ t('relevant results') }}
</p>
</div>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<n-spin size="large">
<div class="loading-text">{{ t('Searching...') }}</div>
</n-spin>
</div>
<!-- 搜索结果 -->
<div v-else-if="results.length > 0" class="results-container">
<div class="results-grid">
<div
v-for="result in results"
:key="result.id"
class="result-card"
@click="handleResultClick(result)"
>
<div class="result-header">
<div class="result-meta">
<span class="result-type">{{ result.payload.pagetype }}</span>
<span class="result-score">{{ t('Similarity') }}: {{ Math.round(result.score * 100) }}%</span>
</div>
<div class="result-title">
{{ result.payload.title || result.payload.name }}
</div>
</div>
<div class="result-content">
<p class="result-summary" v-if="result.payload.content">
{{ truncateText(result.payload.content, 200) }}
</p>
<div class="result-tags" v-if="result.payload.category">
<n-tag size="small" type="info">{{ result.payload.category }}</n-tag>
</div>
</div>
<div class="result-footer">
<span class="result-date" v-if="result.payload.modified">
{{ formatDate(result.payload.modified) }}
</span>
<Icon icon="tabler:arrow-right" class="result-arrow" />
</div>
</div>
</div>
</div>
<!-- 无结果状态 -->
<div v-else-if="!loading && query" class="empty-state">
<div class="empty-icon">
<Icon icon="tabler:search-off" />
</div>
<h3 class="empty-title">{{ t('No relevant results found') }}</h3>
<p class="empty-description">
{{ t('Try using different keywords or check spelling') }}
</p>
</div>
<!-- 初始状态 -->
<div v-else class="initial-state">
<div class="initial-icon">
<Icon icon="tabler:search" />
</div>
<h3 class="initial-title">{{ t('Start searching') }}</h3>
<p class="initial-description">
{{ t('Enter keywords in the search box above to start searching') }}
</p>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { NSpin, NTag, useMessage } from 'naive-ui'
import { Icon } from '@iconify/vue'
import { searchText } from '../shared/api/embedding'
import { t, locale } from '../shared/i18n'
const route = useRoute()
const router = useRouter()
const message = useMessage()
// 响应式数据
const query = ref('')
const results = ref<Array<{
id: string
score: number
payload: Record<string, any>
}>>([])
const loading = ref(false)
// 执行搜索
const performSearch = async (searchQuery: string) => {
if (!searchQuery.trim()) {
results.value = []
return
}
loading.value = true
try {
const response = await searchText(searchQuery, 'knowledge_base', 20, 0.5)
if (response.success && response.data) {
results.value = response.data
} else {
message.error(response.message || t('Search failed'))
results.value = []
}
} catch (error: any) {
console.error('搜索错误:', error)
message.error(t('An error occurred during search'))
results.value = []
} finally {
loading.value = false
}
}
// 监听路由变化
watch(() => route.query.q, (newQuery) => {
if (newQuery && typeof newQuery === 'string') {
query.value = newQuery
performSearch(newQuery)
}
}, { immediate: true })
// 处理结果点击
const handleResultClick = (result: any) => {
const { pagetype, name } = result.payload
if (pagetype && name) {
router.push(`/app/${pagetype}/${name}`)
}
}
// 文本截断
const truncateText = (text: string, maxLength: number): string => {
if (!text) return ''
if (text.length <= maxLength) return text
return text.substring(0, maxLength) + '...'
}
// 日期格式化
const formatDate = (dateString: string): string => {
try {
const date = new Date(dateString)
if (locale.value === 'zh-CN') {
// 中文格式2025-10-08
return date.toISOString().split('T')[0]
} else {
// 英文格式Oct 8, 2025
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
})
}
} catch {
return dateString
}
}
</script>
<style scoped>
.search-results {
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.search-header {
margin-bottom: 32px;
}
.search-title {
display: flex;
align-items: center;
gap: 12px;
font-size: 28px;
font-weight: 600;
color: #1f2937;
margin: 0 0 8px 0;
}
.title-icon {
color: #3b82f6;
}
.search-subtitle {
color: #6b7280;
font-size: 16px;
margin: 0;
}
.loading-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
}
.loading-text {
margin-top: 16px;
color: #6b7280;
font-size: 16px;
}
.results-container {
margin-top: 24px;
}
.results-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: 24px;
}
.result-card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 24px;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.result-card:hover {
border-color: #3b82f6;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15);
transform: translateY(-2px);
}
.result-header {
margin-bottom: 16px;
}
.result-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.result-type {
background: #f3f4f6;
color: #374151;
padding: 4px 8px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
}
.result-score {
color: #10b981;
font-size: 12px;
font-weight: 500;
}
.result-title {
font-size: 18px;
font-weight: 600;
color: #1f2937;
line-height: 1.4;
margin: 0;
}
.result-content {
margin-bottom: 16px;
}
.result-summary {
color: #6b7280;
font-size: 14px;
line-height: 1.6;
margin: 0 0 12px 0;
}
.result-tags {
display: flex;
gap: 8px;
}
.result-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.result-date {
color: #9ca3af;
font-size: 12px;
}
.result-arrow {
color: #d1d5db;
transition: color 0.2s ease;
}
.result-card:hover .result-arrow {
color: #3b82f6;
}
.empty-state,
.initial-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 400px;
text-align: center;
}
.empty-icon,
.initial-icon {
font-size: 64px;
color: #d1d5db;
margin-bottom: 24px;
}
.empty-title,
.initial-title {
font-size: 24px;
font-weight: 600;
color: #374151;
margin: 0 0 12px 0;
}
.empty-description,
.initial-description {
color: #6b7280;
font-size: 16px;
margin: 0;
max-width: 400px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.search-results {
padding: 16px;
}
.results-grid {
grid-template-columns: 1fr;
gap: 16px;
}
.result-card {
padding: 16px;
}
.search-title {
font-size: 24px;
}
}
</style>