421 lines
9.3 KiB
Vue
421 lines
9.3 KiB
Vue
<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> |