Files
ai-dimension/client/src/pages/search/search.vue
T
txai-dev 2a111d21c0 feat: v1.3.0 差异化重构——星图点亮体系
P0 修复:
- 详情页正文渲染(content 分段),71 条知识不再空白
- 问答页去登录墙:游客每日 3 次(IP 限流),登录后每日 5 次
- 修正问答配额为真正的每日重置语义

点亮闭环:
- KnowledgeLit 模型 + 服务端判题(POST /:id/quiz,答案不下发)
- /api/knowledge/stats 星域统计 + 点亮进度 + streak + litIds
- AI 批量生成 71 条读后检测题(scripts/generate-quiz.js)

设计系统 2.0:
- 70 个统一线性图标替换全部 emoji 图标
- 主题 token mixin 化,修复 H5 暗色级联 bug,MP 跟随系统(darkmode)
- 首页星域导航(进度环)、我的页学习者档案、星域列表页重写
- 硬编码色清理,误植 map 页移除

服务端加固:
- trust proxy 真实 IP、错误信息脱敏、ask 独立限流、无效 ObjectId 404
- seo.js ESM→CJS(修复容器启动)、AI 导师"维度精灵"人格化
2026-09-12 23:39:05 +08:00

399 lines
8.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>
<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>
<text class="empty-hint">支持按标题、摘要、标签搜索</text>
</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 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);
}
</style>