1397 lines
32 KiB
Plaintext
1397 lines
32 KiB
Plaintext
<!-- 内容搜索页面 - UTSJSONObject 优化版本 -->
|
||
<template>
|
||
<scroll-view direction="vertical" class="search-page" :scroll-y="true" :enable-back-to-top="true">
|
||
<!-- 搜索头部 -->
|
||
<view class="search-header">
|
||
<view class="search-bar">
|
||
<view class="back-btn" @click="goBack">
|
||
<text class="back-icon">←</text>
|
||
</view>
|
||
<view class="search-input-container">
|
||
<input class="search-input" :value="searchKeyword" @input="onSearchInput" @confirm="performSearch"
|
||
:placeholder="$t('mt.search.placeholder')" confirm-type="search" :focus="true" />
|
||
<view class="clear-btn" v-if="searchKeyword !== ''" @click="clearSearch">
|
||
<text class="clear-icon">✕</text>
|
||
</view>
|
||
</view>
|
||
<view class="search-btn" @click="performSearch">
|
||
<text class="search-icon">🔍</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 筛选条件 -->
|
||
<view class="filter-section">
|
||
<scroll-view direction="horizontal" class="filter-scroll" :scroll-x="true">
|
||
<view class="filter-tabs">
|
||
<view class="filter-tab" @click="showFilterModal">
|
||
<text class="filter-text">{{ $t('mt.search.filter') }}</text>
|
||
<text class="filter-icon">⚙</text>
|
||
</view>
|
||
<view class="filter-tab" :class="{ active: activeFilters.category !== null }"
|
||
@click="clearCategoryFilter" v-if="activeFilters.category !== null">
|
||
<text class="filter-text">{{ getCategoryDisplayNameById(activeFilters.category) }}</text>
|
||
<text class="filter-remove">✕</text>
|
||
</view>
|
||
<view class="filter-tab" :class="{ active: activeFilters.language !== null }"
|
||
@click="clearLanguageFilter" v-if="activeFilters.language !== null">
|
||
<text class="filter-text">{{ activeFilters.language }}</text>
|
||
<text class="filter-remove">✕</text>
|
||
</view>
|
||
<view class="filter-tab" :class="{ active: activeFilters.dateRange !== null }"
|
||
@click="clearDateFilter" v-if="activeFilters.dateRange !== null">
|
||
<text class="filter-text">{{ activeFilters.dateRange }}</text>
|
||
<text class="filter-remove">✕</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 搜索建议 -->
|
||
<view class="suggestions-section" v-if="showSuggestions && searchSuggestions.length > 0">
|
||
<view class="suggestions-header">
|
||
<text class="suggestions-title">{{ $t('mt.search.suggestions') }}</text>
|
||
</view>
|
||
<view class="suggestions-list">
|
||
<view v-for="suggestion in searchSuggestions" :key="suggestion" class="suggestion-item"
|
||
@click="selectSuggestion(suggestion)">
|
||
<text class="suggestion-icon"></text>
|
||
<text class="suggestion-text">{{ suggestion }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 热门搜索 -->
|
||
<view class="hot-searches-section" v-if="searchKeyword === '' && hotSearches.length > 0">
|
||
<view class="section-header">
|
||
<text class="section-title">{{ $t('mt.search.hot') }}</text>
|
||
</view>
|
||
<view class="hot-tags">
|
||
<view v-for="hotSearch in hotSearches" :key="hotSearch" class="hot-tag"
|
||
@click="selectHotSearch(hotSearch)">
|
||
<text class="hot-text">{{ hotSearch }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 搜索历史 -->
|
||
<view class="history-section" v-if="searchKeyword === '' && searchHistory.length > 0">
|
||
<view class="section-header">
|
||
<text class="section-title">{{ $t('mt.search.history') }}</text>
|
||
<view class="clear-history-btn" @click="clearSearchHistory">
|
||
<text class="clear-history-icon">🧹</text>
|
||
</view>
|
||
</view>
|
||
<view class="history-list">
|
||
<view v-for="history in searchHistory" :key="history" class="history-item"
|
||
@click="selectHistory(history)">
|
||
<text class="history-icon">⏰</text>
|
||
<text class="history-text">{{ history }}</text>
|
||
<view class="history-remove" @click.stop="removeHistory(history)">
|
||
<text class="remove-icon">✕</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 搜索结果 -->
|
||
<view class="results-section" v-if="hasSearched">
|
||
<!-- 结果统计 -->
|
||
<view class="results-header">
|
||
<text class="results-count">{{ $t('mt.search.found', { count: pageState.total }) }}</text>
|
||
<view class="sort-options">
|
||
<view class="sort-btn" @click="showSortModal">
|
||
<text class="sort-text">{{ currentSortText }}</text>
|
||
<text class="sort-icon">↕</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 加载状态 -->
|
||
<view class="loading-section" v-if="pageState.loading">
|
||
<text class="loading-text">{{ $t('mt.search.loading') }}</text>
|
||
</view>
|
||
|
||
<!-- 错误状态 -->
|
||
<view class="error-section" v-if="pageState.error !== null">
|
||
<text class="error-text">{{ pageState.error }}</text>
|
||
<view class="retry-btn" @click="retrySearch">
|
||
<text class="retry-text">{{ $t('mt.action.retry') }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 结果列表 -->
|
||
<view class="results-list" v-if="searchResults.length > 0">
|
||
<view v-for="content in searchResults" :key="content.id" class="result-item"
|
||
@click="navigateToDetail(content)">
|
||
<view class="result-header">
|
||
<text class="result-title">{{highlightSearchText(content.title) }}</text>
|
||
<view class="result-meta">
|
||
<view class="quality-indicator"
|
||
:style="{ backgroundColor: getQualityScoreColor(content.quality_score) }">
|
||
</view>
|
||
<text class="result-language">{{ content.original_language }}</text>
|
||
</view>
|
||
</view>
|
||
<text class="result-summary">{{ highlightSearchText(content.summary as string) }}</text>
|
||
<view class="result-footer">
|
||
<text class="result-author">{{ content.author }}</text>
|
||
<text class="result-time">{{ formatRelativeTimeKey(content.published_at) }}</text>
|
||
<view class="result-stats">
|
||
<text class="stat-text"> {{ content.view_count }}</text>
|
||
<text class="stat-text"> {{ content.like_count }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 空结果状态 -->
|
||
<view class="empty-results"
|
||
v-if="!pageState.loading && pageState.error === null && searchResults.length === 0">
|
||
<text class="empty-icon"></text>
|
||
<text class="empty-title">{{ $t('mt.search.empty') }}</text>
|
||
<text class="empty-text">{{ $t('mt.search.emptyTip') }}</text>
|
||
</view>
|
||
|
||
<!-- 加载更多 -->
|
||
<view class="load-more-section" v-if="searchResults.length > 0 && searchResults.length < pageState.total">
|
||
<view class="load-more-btn" @click="loadMore">
|
||
<text class="load-more-text">{{ $t('mt.button.loadMore') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 筛选弹窗 -->
|
||
<view class="filter-modal" v-if="showFilterPanel" @click="hideFilterModal">
|
||
<view class="filter-content" @click.stop>
|
||
<view class="modal-header">
|
||
<text class="modal-title">{{ $t('mt.search.filterTitle') }}</text>
|
||
<view class="close-btn" @click="hideFilterModal">
|
||
<text class="close-text">✕</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="filter-form">
|
||
<!-- 分类筛选 -->
|
||
<view class="filter-group">
|
||
<text class="group-title">内容分类</text>
|
||
<view class="category-options">
|
||
<view v-for="category in categoriesList" :key="category.id" class="category-option"
|
||
:class="{ active: filterForm.category_id === category.id }"
|
||
@click="selectFilterCategory(category)">
|
||
<text class="option-text">{{ getCategoryDisplayName(category) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 语言筛选 -->
|
||
<view class="filter-group">
|
||
<text class="group-title">语言</text>
|
||
<view class="language-options">
|
||
<view v-for="language in languagesList" :key="language.code" class="language-option"
|
||
:class="{ active: filterForm.language === language.code }"
|
||
@click="selectFilterLanguage(language)">
|
||
<text class="option-text">{{ $t(getLanguageDisplayNameKey(language.code)) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 质量筛选 -->
|
||
<view class="filter-group">
|
||
<text class="group-title">内容质量</text>
|
||
<view class="quality-slider">
|
||
<text class="slider-label">最低质量分:{{ filterForm.quality_min ?? 0 }}</text>
|
||
<!-- 使用官方 UTS <slider /> 组件 -->
|
||
<slider
|
||
:min="0"
|
||
:max="100"
|
||
:step="1"
|
||
:value="filterForm.quality_min ?? 0"
|
||
@change="onQualitySliderChange"
|
||
show-value
|
||
/>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 时间筛选 -->
|
||
<view class="filter-group">
|
||
<text class="group-title">发布时间</text>
|
||
<view class="date-options">
|
||
<view v-for="option in dateRangeOptions" :key="option.value" class="date-option"
|
||
:class="{ active: filterForm.date_range === option.value }"
|
||
@click="selectDateRange(option.value)">
|
||
<text class="option-text">{{ option.text }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="modal-actions">
|
||
<view class="action-btn secondary" @click="resetFilters">
|
||
<text class="action-text">重置</text>
|
||
</view>
|
||
<view class="action-btn primary" @click="applyFilters">
|
||
<text class="action-text">应用</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 排序弹窗 -->
|
||
<view class="sort-modal" v-if="showSortPanel" @click="hideSortModal">
|
||
<view class="sort-content" @click.stop>
|
||
<view class="modal-header">
|
||
<text class="modal-title">排序方式</text>
|
||
<view class="close-btn" @click="hideSortModal">
|
||
<text class="close-text">✕</text>
|
||
</view>
|
||
</view>
|
||
<view class="sort-list">
|
||
<view v-for="option in sortOptions" :key="option.value" class="sort-item"
|
||
:class="{ active: currentSort === option.value }" @click="selectSort(option.value)">
|
||
<text class="sort-name">{{ option.text }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||
import type {
|
||
InfoContent,
|
||
CategoryData,
|
||
LanguageData,
|
||
PageState,
|
||
PickerOption,
|
||
ContentFilterData
|
||
} from './types.uts'
|
||
import {
|
||
formatRelativeTimeKey,
|
||
getLanguageDisplayName,
|
||
getQualityScoreColor
|
||
} from './types.uts'
|
||
import supa from '@/components/supadb/aksupainstance.uts'
|
||
|
||
// 页面状态
|
||
const pageState = ref<PageState>({
|
||
loading: false,
|
||
error: null,
|
||
currentPage: 1,
|
||
pageSize: 20,
|
||
total: 0
|
||
})
|
||
|
||
// 搜索状态
|
||
const searchKeyword = ref<string>('')
|
||
const hasSearched = ref<boolean>(false)
|
||
const showSuggestions = ref<boolean>(false)
|
||
|
||
// 数据列表
|
||
const searchResults = ref<Array<InfoContent>>([])
|
||
const searchSuggestions = ref<Array<string>>([])
|
||
const searchHistory = ref<Array<string>>([])
|
||
const hotSearches = ref<Array<string>>([])
|
||
const categoriesList = ref<Array<CategoryData>>([])
|
||
const languagesList = ref<Array<LanguageData>>([])
|
||
|
||
// 筛选和排序
|
||
const showFilterPanel = ref<boolean>(false)
|
||
const showSortPanel = ref<boolean>(false)
|
||
const currentSort = ref<string>('relevance')
|
||
|
||
const filterForm = ref<ContentFilterData>({
|
||
category_id: null,
|
||
language: null,
|
||
status: 'published',
|
||
quality_min: null,
|
||
date_from: null,
|
||
date_to: null,
|
||
search_text: null
|
||
})
|
||
type ActiveFilters = {
|
||
category : string | null
|
||
language : string | null
|
||
dateRange : string | null
|
||
}
|
||
const activeFilters = ref<ActiveFilters>({
|
||
category: null,
|
||
language: null,
|
||
dateRange: null
|
||
})
|
||
|
||
// Supabase组件引用
|
||
const searchRef = ref<SupadbComponentPublicInstance | null>(null)
|
||
|
||
// 计算属性
|
||
const searchFilter = computed(() : UTSJSONObject => {
|
||
const filter : UTSJSONObject = {}
|
||
filter.set('status', 'eq.published')
|
||
|
||
if (searchKeyword.value !== '') {
|
||
// 全文搜索 - 简化实现
|
||
filter.set('or', `title.ilike.*${searchKeyword.value}*,content.ilike.*${searchKeyword.value}*,summary.ilike.*${searchKeyword.value}*`)
|
||
}
|
||
|
||
if (filterForm.value.category_id !== null) {
|
||
filter.set('category_id', `eq.${filterForm.value.category_id}`)
|
||
}
|
||
|
||
if (filterForm.value.language !== null) {
|
||
filter.set('original_language', `eq.${filterForm.value.language}`)
|
||
}
|
||
|
||
if (filterForm.value.quality_min !== null) {
|
||
filter.set('quality_score', `gte.${filterForm.value.quality_min}`)
|
||
}
|
||
|
||
if (filterForm.value.date_from !== null && filterForm.value.date_to !== null) {
|
||
filter.set('published_at', `gte.${filterForm.value.date_from}`)
|
||
filter.set('published_at', `lte.${filterForm.value.date_to}`)
|
||
}
|
||
|
||
// 排序
|
||
let orderBy = 'published_at.desc'
|
||
switch (currentSort.value) {
|
||
case 'relevance':
|
||
orderBy = 'quality_score.desc'
|
||
break
|
||
case 'newest':
|
||
orderBy = 'published_at.desc'
|
||
break
|
||
case 'oldest':
|
||
orderBy = 'published_at.asc'
|
||
break
|
||
case 'quality':
|
||
orderBy = 'quality_score.desc'
|
||
break
|
||
case 'popular':
|
||
orderBy = 'view_count.desc'
|
||
break
|
||
}
|
||
filter.set('order', orderBy)
|
||
|
||
return filter
|
||
})
|
||
|
||
const sortOptions = ref<Array<PickerOption>>([
|
||
{ value: 'relevance', text: '相关性' },
|
||
{ value: 'newest', text: '最新发布' },
|
||
{ value: 'oldest', text: '最早发布' },
|
||
{ value: 'quality', text: '质量最高' },
|
||
{ value: 'popular', text: '最多浏览' }
|
||
])
|
||
|
||
const currentSortText = computed(() : string => {
|
||
const option = sortOptions.value.find(opt => opt.value === currentSort.value)
|
||
return option != null ? option.text : '相关性'
|
||
})
|
||
|
||
const dateRangeOptions = ref<Array<PickerOption>>([
|
||
{ value: 'today', text: '今天' },
|
||
{ value: 'week', text: '本周' },
|
||
{ value: 'month', text: '本月' },
|
||
{ value: 'year', text: '本年' },
|
||
{ value: 'all', text: '全部' }
|
||
])
|
||
|
||
|
||
// 根据category对象获取显示名称(优先translations,否则name_key)
|
||
const getCategoryDisplayName = (category : CategoryData) : string => {
|
||
const translations = category.translations
|
||
// UTS: translations may be null or undefined, so use safe call
|
||
if (translations != null && translations.length > 0) {
|
||
const first = translations[0]
|
||
if (first != null && typeof first.name === 'string') {
|
||
return first.name
|
||
}
|
||
}
|
||
// fallback: use name_key
|
||
return category.name_key
|
||
}
|
||
|
||
// 根据category_id获取显示名称
|
||
const getCategoryDisplayNameById = (categoryId : string | null) : string => {
|
||
if (categoryId == null || categoryId === '') return ''
|
||
const category = categoriesList.value.find(cat => cat.id === categoryId)
|
||
return category != null ? getCategoryDisplayName(category) : ''
|
||
}
|
||
|
||
// 搜索相关函数
|
||
const loadSearchSuggestions = (keyword : string) => {
|
||
// 模拟搜索建议
|
||
const suggestions = [
|
||
'人工智能发展趋势',
|
||
'人工智能应用场景',
|
||
'人工智能技术突破',
|
||
'人工智能行业分析'
|
||
].filter(s => s.includes(keyword))
|
||
|
||
searchSuggestions.value = suggestions
|
||
}
|
||
|
||
const onSearchInput = (event : InputEvent) => {
|
||
const value = event.detail.value
|
||
searchKeyword.value = value
|
||
|
||
if (value.length > 0) {
|
||
showSuggestions.value = true
|
||
loadSearchSuggestions(value)
|
||
} else {
|
||
showSuggestions.value = false
|
||
}
|
||
|
||
}
|
||
|
||
|
||
const executeSearch = async () => {
|
||
pageState.value.loading = true
|
||
pageState.value.error = null
|
||
|
||
const keyword = searchKeyword.value.trim()
|
||
|
||
try {
|
||
// 只查 ak_contents_translation,按当前语言过滤
|
||
const result = await supa
|
||
.from('ak_contents')
|
||
.select('*', { count: 'exact' })
|
||
.or(`title.ilike.%${keyword}%,content.ilike.%${keyword}%`)
|
||
//.eq('language_id', lang)
|
||
.range((pageState.value.currentPage - 1) * pageState.value.pageSize, pageState.value.currentPage * pageState.value.pageSize - 1)
|
||
.executeAs<InfoContent>()
|
||
|
||
if (result.error === null && Array.isArray(result.data)) {
|
||
const resdata= result.data as Array<InfoContent>
|
||
if (pageState.value.currentPage === 1) {
|
||
searchResults.value =resdata
|
||
} else {
|
||
searchResults.value = searchResults.value.concat(result.data!!)
|
||
}
|
||
pageState.value.total = result.total ?? resdata.length
|
||
pageState.value.loading = false
|
||
pageState.value.error = null
|
||
} else {
|
||
pageState.value.loading = false
|
||
pageState.value.error = '搜索失败,请重试'
|
||
}
|
||
} catch (error) {
|
||
console.error('Search error:', error)
|
||
pageState.value.loading = false
|
||
pageState.value.error = '搜索失败,请重试'
|
||
}
|
||
}
|
||
|
||
const retrySearch = () => {
|
||
executeSearch()
|
||
}
|
||
|
||
const clearSearch = () => {
|
||
searchKeyword.value = ''
|
||
showSuggestions.value = false
|
||
hasSearched.value = false
|
||
searchResults.value = []
|
||
pageState.value.total = 0
|
||
}
|
||
const addToSearchHistory = (keyword : string) => {
|
||
// 移除重复项
|
||
const filteredHistory = searchHistory.value.filter(item => item !== keyword)
|
||
// 添加到开头
|
||
searchHistory.value = [keyword, ...filteredHistory].slice(0, 10)
|
||
// 保存到本地存储
|
||
try {
|
||
uni.setStorageSync('search_history', searchHistory.value)
|
||
} catch (error) {
|
||
console.error('保存搜索历史失败:', error)
|
||
}
|
||
}
|
||
const performSearch = () => {
|
||
if (searchKeyword.value.trim() === '') return
|
||
|
||
showSuggestions.value = false
|
||
hasSearched.value = true
|
||
pageState.value.currentPage = 1
|
||
|
||
// 添加到搜索历史
|
||
addToSearchHistory(searchKeyword.value.trim())
|
||
|
||
// 执行搜索
|
||
executeSearch()
|
||
}
|
||
const selectSuggestion = (suggestion : string) => {
|
||
searchKeyword.value = suggestion
|
||
performSearch()
|
||
}
|
||
|
||
const selectHotSearch = (hotSearch : string) => {
|
||
searchKeyword.value = hotSearch
|
||
performSearch()
|
||
}
|
||
|
||
const selectHistory = (history : string) => {
|
||
searchKeyword.value = history
|
||
performSearch()
|
||
}
|
||
|
||
|
||
|
||
const loadSearchHistory = () => {
|
||
try {
|
||
const history = uni.getStorageSync('search_history')
|
||
if (history != null && Array.isArray(history)) {
|
||
searchHistory.value = history as Array<string>
|
||
}
|
||
} catch (error) {
|
||
console.error('加载搜索历史失败:', error)
|
||
}
|
||
}
|
||
|
||
const clearSearchHistory = () => {
|
||
searchHistory.value = []
|
||
try {
|
||
uni.removeStorageSync('search_history')
|
||
} catch (error) {
|
||
console.error('清空搜索历史失败:', error)
|
||
}
|
||
}
|
||
|
||
const removeHistory = (history : string) => {
|
||
searchHistory.value = searchHistory.value.filter(item => item !== history)
|
||
try {
|
||
uni.setStorageSync('search_history', searchHistory.value)
|
||
} catch (error) {
|
||
console.error('删除搜索历史失败:', error)
|
||
}
|
||
}
|
||
|
||
// 筛选相关函数
|
||
const showFilterModal = () => {
|
||
showFilterPanel.value = true
|
||
}
|
||
|
||
const hideFilterModal = () => {
|
||
showFilterPanel.value = false
|
||
}
|
||
|
||
const selectFilterCategory = (category : CategoryData) => {
|
||
const categoryId = category.id
|
||
filterForm.value.category_id = filterForm.value.category_id === categoryId ? null : categoryId
|
||
}
|
||
|
||
const selectFilterLanguage = (language : LanguageData) => {
|
||
const languageCode = language.code
|
||
filterForm.value.language = filterForm.value.language === languageCode ? null : languageCode
|
||
}
|
||
|
||
const selectDateRange = (range : string) => {
|
||
filterForm.value.date_range = filterForm.value.date_range === range ? null : range
|
||
|
||
// 计算日期范围
|
||
const now = new Date()
|
||
let startDate : Date | null = null
|
||
|
||
switch (range) {
|
||
case 'today':
|
||
startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||
break
|
||
case 'week':
|
||
startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
|
||
break
|
||
case 'month':
|
||
startDate = new Date(now.getFullYear(), now.getMonth(), 1)
|
||
break
|
||
case 'year':
|
||
startDate = new Date(now.getFullYear(), 0, 1)
|
||
break
|
||
case 'all':
|
||
startDate = null
|
||
break
|
||
}
|
||
|
||
if (startDate !== null) {
|
||
filterForm.value.date_from = startDate.toISOString()
|
||
filterForm.value.date_to = now.toISOString()
|
||
} else {
|
||
filterForm.value.date_from = null
|
||
filterForm.value.date_to = null
|
||
}
|
||
}
|
||
|
||
const resetFilters = () => {
|
||
filterForm.value = {
|
||
category_id: null,
|
||
language: null,
|
||
status: 'published',
|
||
quality_min: null,
|
||
date_from: null,
|
||
date_to: null,
|
||
search_text: searchKeyword.value.trim(),
|
||
date_range: null
|
||
} as ContentFilterData
|
||
activeFilters.value = {
|
||
category: null,
|
||
language: null,
|
||
dateRange: null
|
||
} as ActiveFilters
|
||
}
|
||
|
||
const applyFilters = () => {
|
||
// 更新活跃筛选器显示
|
||
activeFilters.value.category = filterForm.value.category_id
|
||
activeFilters.value.language = filterForm.value.language
|
||
activeFilters.value.dateRange = filterForm.value.date_range
|
||
|
||
hideFilterModal()
|
||
|
||
if (hasSearched.value) {
|
||
pageState.value.currentPage = 1
|
||
executeSearch()
|
||
}
|
||
}
|
||
|
||
const clearCategoryFilter = () => {
|
||
filterForm.value.category_id = null
|
||
activeFilters.value.category = null
|
||
if (hasSearched.value) {
|
||
executeSearch()
|
||
}
|
||
}
|
||
|
||
const clearLanguageFilter = () => {
|
||
filterForm.value.language = null
|
||
activeFilters.value.language = null
|
||
if (hasSearched.value) {
|
||
executeSearch()
|
||
}
|
||
}
|
||
|
||
const clearDateFilter = () => {
|
||
filterForm.value.date_from = null
|
||
filterForm.value.date_to = null
|
||
filterForm.value.date_range = null
|
||
activeFilters.value.dateRange = null
|
||
if (hasSearched.value) {
|
||
executeSearch()
|
||
}
|
||
}
|
||
|
||
// 排序相关函数
|
||
const showSortModal = () => {
|
||
showSortPanel.value = true
|
||
}
|
||
|
||
const hideSortModal = () => {
|
||
showSortPanel.value = false
|
||
}
|
||
|
||
const selectSort = (sortValue : string) => {
|
||
currentSort.value = sortValue
|
||
hideSortModal()
|
||
|
||
if (hasSearched.value) {
|
||
pageState.value.currentPage = 1
|
||
executeSearch()
|
||
}
|
||
}
|
||
|
||
// 数据处理函数
|
||
const handleSearchResults = (result : UTSJSONObject) => {
|
||
const data = result.get('data')
|
||
const count = result.get('count')
|
||
|
||
if (data != null && Array.isArray(data)) {
|
||
if (pageState.value.currentPage === 1) {
|
||
searchResults.value = data as Array<InfoContent>
|
||
} else {
|
||
searchResults.value = searchResults.value.concat(data as Array<InfoContent>)
|
||
}
|
||
}
|
||
|
||
if (count != null) {
|
||
pageState.value.total = count as number
|
||
}
|
||
|
||
pageState.value.loading = false
|
||
pageState.value.error = null
|
||
}
|
||
|
||
const handleError = (error : any) => {
|
||
console.error('Search error:', error)
|
||
pageState.value.loading = false
|
||
pageState.value.error = '搜索失败,请重试'
|
||
|
||
uni.showToast({
|
||
title: '搜索失败',
|
||
icon: 'error'
|
||
})
|
||
}
|
||
|
||
const loadMore = () => {
|
||
pageState.value.currentPage += 1
|
||
executeSearch()
|
||
}
|
||
|
||
// 高亮搜索关键词
|
||
|
||
const highlightSearchText = (text: string | null): string => {
|
||
if (typeof text !== 'string' || searchKeyword.value.trim() === '') return text ?? ''
|
||
const keyword = searchKeyword.value.trim()
|
||
const regex = new RegExp(keyword, 'gi')
|
||
return text?.replace(regex, `**${keyword}**`)??''
|
||
}
|
||
|
||
// 质量滑块变更事件
|
||
|
||
const onQualitySliderChange = (event:UniSliderChangeEvent) => {
|
||
const value = event.detail.value
|
||
filterForm.value.quality_min = value.toString()
|
||
}
|
||
|
||
// 导航函数
|
||
const navigateToDetail = (content : InfoContent) => {
|
||
const contentId = content.id
|
||
try {
|
||
uni.navigateTo({
|
||
url: `/pages/info/detail?id=${contentId}`
|
||
})
|
||
} catch (error) {
|
||
console.error('导航异常:', error)
|
||
}
|
||
}
|
||
|
||
const goBack = () => {
|
||
try {
|
||
uni.navigateBack({
|
||
delta: 1
|
||
})
|
||
} catch (error) {
|
||
console.error('返回异常:', error)
|
||
}
|
||
}
|
||
|
||
// 加载语言数据,直接使用强类型常量
|
||
import { LANGUAGE_OPTIONS, getLanguageDisplayNameKey } from './types.uts'
|
||
const loadLanguages = () => {
|
||
languagesList.value = LANGUAGE_OPTIONS
|
||
}
|
||
|
||
|
||
// 加载热门搜索
|
||
const loadHotSearches = () => {
|
||
hotSearches.value = [
|
||
'人工智能',
|
||
'机器学习',
|
||
'深度学习',
|
||
'自然语言处理',
|
||
'计算机视觉',
|
||
'大数据',
|
||
'云计算',
|
||
'区块链',
|
||
'物联网',
|
||
'5G技术'
|
||
]
|
||
}
|
||
|
||
// 加载分类数据
|
||
// 动态加载分类,强类型 CategoryData[],与 index.uvue 保持一致
|
||
const loadCategories = async () => {
|
||
if (supa === null) return
|
||
try {
|
||
// 默认使用中文,或可根据当前语言调整
|
||
const lang = 'zh-CN'
|
||
const result = await supa
|
||
.from('ak_content_categories')
|
||
.select('*,translations:ak_content_category_translations(name)', {})
|
||
.eq('translations.language_code', lang)
|
||
.order('sort_order', { ascending: true })
|
||
.executeAs<CategoryData>()
|
||
if (result.data !== null && Array.isArray(result.data)) {
|
||
categoriesList.value = result.data as CategoryData[]
|
||
} else {
|
||
categoriesList.value = []
|
||
}
|
||
} catch (e) {
|
||
console.error('加载分类失败:', e)
|
||
categoriesList.value = []
|
||
}
|
||
}
|
||
// 生命周期
|
||
|
||
onMounted(() => {
|
||
loadSearchHistory()
|
||
loadHotSearches()
|
||
loadCategories()
|
||
loadLanguages()
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
// 清理工作
|
||
})
|
||
</script>
|
||
|
||
<style>
|
||
.search-page {
|
||
flex: 1;
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
.search-header {
|
||
background-color: #ffffff;
|
||
border-bottom: 1px solid #e5e5e5;
|
||
}
|
||
|
||
.search-bar {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
padding: 12px 16px;
|
||
}
|
||
|
||
.back-btn {
|
||
width: 40px;
|
||
height: 40px;
|
||
justify-content: center;
|
||
align-items: center;
|
||
border-radius: 20px;
|
||
background-color: #f3f4f6;
|
||
margin-right: 12px;
|
||
}
|
||
|
||
.back-icon {
|
||
font-size: 18px;
|
||
color: #374151;
|
||
}
|
||
|
||
.search-input-container {
|
||
flex: 1;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
background-color: #f3f4f6;
|
||
border-radius: 20px;
|
||
padding: 0 16px;
|
||
margin-right: 12px;
|
||
}
|
||
|
||
.search-input {
|
||
flex: 1;
|
||
height: 40px;
|
||
font-size: 16px;
|
||
color: #374151;
|
||
}
|
||
|
||
.clear-btn {
|
||
width: 20px;
|
||
height: 20px;
|
||
justify-content: center;
|
||
align-items: center;
|
||
border-radius: 10px;
|
||
background-color: #d1d5db;
|
||
}
|
||
|
||
.clear-icon {
|
||
font-size: 12px;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.search-btn {
|
||
padding: 10px 16px;
|
||
background-color: #3b82f6;
|
||
border-radius: 20px;
|
||
}
|
||
|
||
.search-text {
|
||
font-size: 14px;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.filter-section {
|
||
border-top: 1px solid #f3f4f6;
|
||
}
|
||
|
||
.filter-scroll {
|
||
height: 50px;
|
||
}
|
||
|
||
.filter-tabs {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
padding: 0 16px;
|
||
}
|
||
|
||
.filter-tab {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
margin-right: 12px;
|
||
padding: 6px 12px;
|
||
border-radius: 16px;
|
||
background-color: #f3f4f6;
|
||
}
|
||
|
||
.filter-tab.active {
|
||
background-color: #eff6ff;
|
||
border: 1px solid #3b82f6;
|
||
}
|
||
|
||
.filter-text {
|
||
font-size: 14px;
|
||
color: #374151;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.filter-tab.active .filter-text {
|
||
color: #3b82f6;
|
||
}
|
||
|
||
.filter-icon,
|
||
.filter-remove {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
margin-left: 4px;
|
||
}
|
||
|
||
.filter-tab.active .filter-remove {
|
||
color: #3b82f6;
|
||
}
|
||
|
||
.suggestions-section,
|
||
.hot-searches-section,
|
||
.history-section {
|
||
background-color: #ffffff;
|
||
margin: 12px;
|
||
border-radius: 12px;
|
||
}
|
||
|
||
.suggestions-header,
|
||
.section-header {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 16px;
|
||
border-bottom: 1px solid #e5e5e5;
|
||
}
|
||
|
||
.suggestions-title,
|
||
.section-title {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.clear-history-btn {
|
||
padding: 4px 12px;
|
||
background-color: #f3f4f6;
|
||
border-radius: 12px;
|
||
}
|
||
|
||
.clear-history-text {
|
||
font-size: 14px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.suggestions-list,
|
||
.history-list {
|
||
padding: 0 16px;
|
||
}
|
||
|
||
.suggestion-item,
|
||
.history-item {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
padding: 12px 0;
|
||
border-bottom: 1px solid #f3f4f6;
|
||
}
|
||
|
||
.suggestion-icon,
|
||
.history-icon {
|
||
font-size: 16px;
|
||
color: #6b7280;
|
||
margin-right: 12px;
|
||
}
|
||
|
||
.suggestion-text,
|
||
.history-text {
|
||
flex: 1;
|
||
font-size: 16px;
|
||
color: #374151;
|
||
}
|
||
|
||
.history-remove {
|
||
width: 24px;
|
||
height: 24px;
|
||
justify-content: center;
|
||
align-items: center;
|
||
border-radius: 12px;
|
||
background-color: #f3f4f6;
|
||
}
|
||
|
||
.remove-icon {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.hot-tags {
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
padding: 16px;
|
||
}
|
||
|
||
.hot-tag {
|
||
margin-right: 12px;
|
||
margin-bottom: 8px;
|
||
padding: 8px 16px;
|
||
background-color: #eff6ff;
|
||
border-radius: 20px;
|
||
}
|
||
|
||
.hot-text {
|
||
font-size: 14px;
|
||
color: #3b82f6;
|
||
}
|
||
|
||
.results-section {
|
||
background-color: #ffffff;
|
||
margin: 12px;
|
||
border-radius: 12px;
|
||
}
|
||
|
||
.results-header {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 16px;
|
||
border-bottom: 1px solid #e5e5e5;
|
||
}
|
||
|
||
.results-count {
|
||
font-size: 14px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.sort-options {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.sort-btn {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
padding: 6px 12px;
|
||
background-color: #f3f4f6;
|
||
border-radius: 16px;
|
||
}
|
||
|
||
.sort-text {
|
||
font-size: 14px;
|
||
color: #374151;
|
||
}
|
||
|
||
.sort-icon {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
margin-left: 4px;
|
||
}
|
||
|
||
.loading-section,
|
||
.error-section,
|
||
.empty-results {
|
||
padding: 40px 16px;
|
||
align-items: center;
|
||
}
|
||
|
||
.loading-text,
|
||
.error-text {
|
||
font-size: 16px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.empty-icon {
|
||
font-size: 48px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.empty-title {
|
||
font-size: 18px;
|
||
color: #1f2937;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.empty-text {
|
||
font-size: 14px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.retry-btn {
|
||
margin-top: 16px;
|
||
padding: 8px 16px;
|
||
background-color: #3b82f6;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.retry-text {
|
||
font-size: 14px;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.results-list {
|
||
padding: 0 16px;
|
||
}
|
||
|
||
.result-item {
|
||
padding: 16px 0;
|
||
border-bottom: 1px solid #e5e5e5;
|
||
}
|
||
|
||
.result-header {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.result-title {
|
||
flex: 1;
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
color: #1f2937;
|
||
line-height: 22px;
|
||
margin-right: 12px;
|
||
}
|
||
|
||
.result-meta {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.quality-indicator {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 4px;
|
||
margin-right: 6px;
|
||
}
|
||
|
||
.result-language {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.result-summary {
|
||
font-size: 14px;
|
||
color: #6b7280;
|
||
line-height: 20px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.result-footer {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.result-author {
|
||
font-size: 12px;
|
||
color: #9ca3af;
|
||
}
|
||
|
||
.result-time {
|
||
font-size: 12px;
|
||
color: #9ca3af;
|
||
}
|
||
|
||
.result-stats {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.stat-text {
|
||
font-size: 12px;
|
||
color: #9ca3af;
|
||
margin-left: 8px;
|
||
}
|
||
|
||
.load-more-section {
|
||
padding: 16px;
|
||
align-items: center;
|
||
}
|
||
|
||
.load-more-btn {
|
||
padding: 12px 24px;
|
||
background-color: #f3f4f6;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.load-more-text {
|
||
font-size: 14px;
|
||
color: #374151;
|
||
}
|
||
|
||
/* 弹窗样式 */
|
||
.filter-modal,
|
||
.sort-modal {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
z-index: 1000;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.filter-content,
|
||
.sort-content {
|
||
width: 320px;
|
||
max-height: 600px;
|
||
background-color: #ffffff;
|
||
border-radius: 12px;
|
||
margin: 20px;
|
||
}
|
||
|
||
.modal-header {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 16px;
|
||
border-bottom: 1px solid #e5e5e5;
|
||
}
|
||
|
||
.modal-title {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.close-btn {
|
||
width: 32px;
|
||
height: 32px;
|
||
justify-content: center;
|
||
align-items: center;
|
||
border-radius: 16px;
|
||
background-color: #f3f4f6;
|
||
}
|
||
|
||
.close-text {
|
||
font-size: 16px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.filter-form {
|
||
max-height: 400px;
|
||
padding: 16px;
|
||
}
|
||
|
||
.filter-group {
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.group-title {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
color: #1f2937;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.category-options,
|
||
.language-options,
|
||
.date-options {
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.category-option,
|
||
.language-option,
|
||
.date-option {
|
||
margin-right: 8px;
|
||
margin-bottom: 8px;
|
||
padding: 8px 16px;
|
||
background-color: #f3f4f6;
|
||
border-radius: 20px;
|
||
}
|
||
|
||
.category-option.active,
|
||
.language-option.active,
|
||
.date-option.active {
|
||
background-color: #eff6ff;
|
||
border: 1px solid #3b82f6;
|
||
}
|
||
|
||
.option-text {
|
||
font-size: 14px;
|
||
color: #374151;
|
||
}
|
||
|
||
.category-option.active .option-text,
|
||
.language-option.active .option-text,
|
||
.date-option.active .option-text {
|
||
color: #3b82f6;
|
||
}
|
||
|
||
.quality-slider {
|
||
padding: 16px 0;
|
||
}
|
||
|
||
.slider-label {
|
||
font-size: 14px;
|
||
color: #374151;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
/* 移除自定义滑块样式,保留官方slider相关样式 */
|
||
|
||
.modal-actions {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
padding: 16px;
|
||
border-top: 1px solid #e5e5e5;
|
||
}
|
||
|
||
.action-btn {
|
||
flex: 1;
|
||
height: 44px;
|
||
justify-content: center;
|
||
align-items: center;
|
||
border-radius: 8px;
|
||
margin: 0 8px;
|
||
}
|
||
|
||
.action-btn.secondary {
|
||
background-color: #f3f4f6;
|
||
}
|
||
|
||
.action-btn.primary {
|
||
background-color: #3b82f6;
|
||
}
|
||
|
||
.action-text {
|
||
font-size: 16px;
|
||
}
|
||
|
||
.action-btn.secondary .action-text {
|
||
color: #374151;
|
||
}
|
||
|
||
.action-btn.primary .action-text {
|
||
color: #ffffff;
|
||
}
|
||
|
||
.sort-list {
|
||
max-height: 300px;
|
||
}
|
||
|
||
.sort-item {
|
||
padding: 16px;
|
||
border-bottom: 1px solid #f3f4f6;
|
||
}
|
||
|
||
.sort-item.active {
|
||
background-color: #eff6ff;
|
||
}
|
||
|
||
.sort-name {
|
||
font-size: 16px;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.sort-item.active .sort-name {
|
||
color: #3b82f6;
|
||
}
|
||
</style> |