Files
ai-dimension/client/src/pages/search/search.vue
T

421 lines
9.3 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>
<view class="search-page page-layout">
<!-- 状态栏占位 -->
<view class="status-bar-placeholder"></view>
<!-- 顶部导航 -->
<view class="nav-bar">
<view class="nav-back" @click="goBack">‹</view>
<text class="nav-title">搜索</text>
<view class="nav-spacer"></view>
</view>
<!-- 搜索框 -->
<view class="search-box">
<input
class="search-input"
type="text"
:value="searchText"
:placeholder="placeholderText"
placeholder-style="color: var(--text-secondary); font-size: 14px;"
@input="onInput"
@confirm="doSearch"
confirm-type="search"
/>
<view class="search-icon"></view>
<view v-if="searchText" class="clear-btn" @click="clearSearch">✕</view>
</view>
<!-- 加载状态 -->
<view class="loading-state" v-if="loading">
<view class="loading-spinner"></view>
<text class="loading-text">搜索中...</text>
</view>
<!-- 未搜索:热门词快捷入口 -->
<view class="empty-state" v-else-if="!hasSearched">
<text class="empty-icon">🔍</text>
<text class="empty-text">输入关键词搜索 AI 知识</text>
<view class="hot-words">
<text v-for="w in HOT_WORDS" :key="w" class="hot-word" @click="quickSearch(w)">{{ w }}</text>
</view>
</view>
<!-- 无结果提示 -->
<view class="empty-state" v-else-if="results.length === 0 && !loading">
<text class="empty-icon">📭</text>
<text class="empty-text">没有找到相关内容</text>
<text class="empty-hint">试试换个关键词搜索</text>
</view>
<!-- 搜索结果列表 -->
<scroll-view class="results-scroll" scroll-y v-else>
<view
class="result-card"
v-for="item in results"
:key="item._id || item.id"
@click="goDetail(item._id || item.id)"
>
<!-- 维度标签 -->
<view
class="dim-badge"
:style="{ background: dimColor(item.dim) }"
>
<text class="dim-name">D{{ item.dim }}</text>
</view>
<text class="result-title">{{ item.title }}</text>
<!-- 摘要 -->
<text class="result-summary" v-if="item.content">
{{ item.content }}
</text>
<!-- 标签与分类 -->
<view class="result-meta">
<view class="meta-tags" v-if="item.tags?.length">
<text class="meta-tag" v-for="tag in item.tags" :key="tag">
#{{ tag }}
</text>
</view>
<text class="result-category" v-if="item.category">
{{ categoryLabel(item.category) }}
</text>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { knowledgeApi } from '@/utils/api'
import { DIMENSIONS } from '@/utils/dimensions'
const searchText = ref('')
const HOT_WORDS = ['大模型', 'Transformer', 'DeepSeek', '提示词', 'AI绘画', '智能体', 'RAG', '具身智能']
function quickSearch(w) {
searchText.value = w
doSearch()
}
const results = ref([])
const loading = ref(false)
const hasSearched = ref(false)
let debounceTimer = null
let page = 1
// 维度颜色
function dimColor(dim) {
if (!dim) return 'var(--dim1-color)'
const dimInfo = DIMENSIONS.find(d => d.id === dim)
return dimInfo ? dimInfo.color : 'var(--dim1-color)'
}
// 分类标签
function categoryLabel(cat) {
const map = {
concept: '📚 概念',
history: '📜 历史',
application: '🛠️ 应用',
question: '❓ 问答'
}
return map[cat] || cat
}
// 占位符提示
const placeholderText = computed(() =>
searchText.value ? '' : '输入关键词搜索 AI 知识'
)
// 输入处理(debounced)
function onInput(e) {
const val = e.detail?.value || e.target?.value || ''
searchText.value = val
if (debounceTimer) clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
if (val.trim()) {
doSearch()
}
}, 500)
}
// 执行搜索
async function doSearch() {
const q = searchText.value.trim()
if (!q) return
loading.value = true
hasSearched.value = true
try {
const data = await knowledgeApi.search(q, { page, limit: 20 })
results.value = data.list || data.items || data || []
} catch (err) {
console.error('[search] 搜索失败:', err)
uni.showToast({ title: '搜索失败,请重试', icon: 'none' })
} finally {
loading.value = false
}
}
// 清空搜索
function clearSearch() {
searchText.value = ''
results.value = []
hasSearched.value = false
page = 1
if (debounceTimer) {
clearTimeout(debounceTimer)
debounceTimer = null
}
}
// 跳转到详情
function goDetail(id) {
if (!id) return
uni.navigateTo({ url: `/pages/detail/detail?id=${id}` })
}
// 返回
function goBack() {
uni.navigateBack()
}
onMounted(() => {
// 检查是否有路由传入的搜索词
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const query = currentPage.$page?.query || {}
if (query.q) {
searchText.value = query.q
doSearch()
}
})
onUnmounted(() => {
if (debounceTimer) clearTimeout(debounceTimer)
})
// 微信小程序分享
// #ifdef MP-WEIXIN
onShareAppMessage(() => ({
title: '搜一搜想了解的AI知识 · 宇之然AI维度',
path: '/pages/index/index'
}))
// #endif
</script>
<style lang="scss" scoped>
.search-page {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
background: var(--bg-primary);
}
// 顶部导航
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 50px 20px 12px;
flex-shrink: 0;
.nav-back {
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
color: var(--text-primary);
}
.nav-title {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
}
.nav-spacer { width: 30px; }
}
// 搜索框
.search-box {
position: relative;
margin: 0 20px 16px;
flex-shrink: 0;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 14px;
padding: 10px 44px 10px 14px;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
.search-input {
width: 100%;
height: 32px;
font-size: 14px;
color: var(--text-primary);
background: transparent;
outline: none;
}
.search-icon {
position: absolute;
right: 14px;
top: 50%;
transform: translateY(-50%);
font-size: 16px;
color: var(--text-secondary);
}
.clear-btn {
position: absolute;
right: 14px;
top: 50%;
transform: translateY(-50%);
width: 20px;
height: 20px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
color: var(--text-secondary);
}
}
// 加载状态
.loading-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid rgba(179, 136, 255, 0.2);
border-top-color: var(--dim1-color);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.loading-text {
font-size: 14px;
color: var(--text-secondary);
}
}
@keyframes spin {
to { transform: rotate(360deg); }
}
// 空状态
.empty-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
.empty-icon { font-size: 48px; opacity: 0.5; }
.empty-text { font-size: 16px; color: var(--text-secondary); }
.empty-hint { font-size: 13px; color: var(--text-muted); }
}
// 结果列表
.results-scroll {
flex: 1;
padding: 0 20px;
overflow-y: auto;
padding-bottom: 40px;
}
.result-card {
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 14px;
padding: 14px 16px;
margin-bottom: 10px;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
transition: all 0.3s;
}
.dim-badge {
display: inline-block;
padding: 2px 10px;
border-radius: 8px;
font-size: 11px;
font-weight: 600;
color: white;
margin-bottom: 8px;
opacity: 0.85;
}
.result-title {
display: block;
font-size: 15px;
font-weight: 600;
color: var(--text-primary);
line-height: 1.5;
margin-bottom: 6px;
}
.result-summary {
display: block;
font-size: 13px;
color: var(--text-tertiary);
line-height: 1.6;
margin-bottom: 10px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.result-meta {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.meta-tags {
display: flex;
gap: 6px;
flex-wrap: wrap;
.meta-tag {
font-size: 11px;
color: var(--dim1-color);
background: rgba(179, 136, 255, 0.1);
padding: 2px 8px;
border-radius: 8px;
}
}
.result-category {
font-size: 11px;
color: var(--text-secondary);
}
.hot-words {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
margin-top: 16px;
padding: 0 20px;
}
.hot-word {
padding: 6px 16px;
border-radius: 999px;
background: var(--glass-bg, #f3f4fa);
color: var(--text-secondary, #4a4f63);
font-size: 13px;
}
</style>