Files
akmon/pages/info/search.uvue
2026-01-20 08:04:15 +08:00

1397 lines
32 KiB
Plaintext
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.
<!-- 内容搜索页面 - 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>