Files
akmon/components/message/MessageSearch.uvue
2026-01-20 08:04:15 +08:00

657 lines
15 KiB
Plaintext

<template>
<view class="message-search">
<!-- 搜索框 -->
<view class="search-header">
<view class="search-input-wrapper">
<text class="search-icon"></text>
<input
class="search-input"
type="text"
placeholder="搜索消息内容、标题或发送者"
v-model="keyword"
@input="handleInput"
@confirm="handleSearch"
/>
<text
v-if="keyword.length > 0"
class="clear-icon"
@click="handleClear"
>
✖️
</text>
</view>
<button class="search-btn" @click="handleSearch">
<text class="search-text">搜索</text>
</button>
</view>
<!-- 搜索筛选 -->
<view class="search-filters">
<scroll-view class="filter-scroll" scroll-x="true">
<view class="filter-items">
<!-- 消息类型筛选 -->
<view
class="filter-item"
:class="{ 'active': selectedTypeId === '' }"
@click="handleTypeFilter('')"
>
<text class="filter-text">全部类型</text>
</view>
<view
v-for="type in messageTypes"
:key="type.id"
class="filter-item"
:class="{ 'active': selectedTypeId === type.id }"
@click="handleTypeFilter(type.id)"
>
<text class="filter-text">{{ type.name }}</text>
</view>
<!-- 时间筛选 -->
<view class="filter-divider"></view>
<view
v-for="timeRange in timeRanges"
:key="timeRange.value"
class="filter-item"
:class="{ 'active': selectedTimeRange === timeRange.value }"
@click="handleTimeFilter(timeRange.value)"
>
<text class="filter-text">{{ timeRange.label }}</text>
</view>
<!-- 状态筛选 -->
<view class="filter-divider"></view>
<view
v-for="status in statusOptions"
:key="status.value"
class="filter-item"
:class="{ 'active': selectedStatus === status.value }"
@click="handleStatusFilter(status.value)"
>
<text class="filter-text">{{ status.label }}</text>
</view>
</view>
</scroll-view>
</view>
<!-- 搜索历史 -->
<view v-if="showHistory && searchHistory.length > 0" class="search-history">
<view class="history-header">
<text class="history-title">搜索历史</text>
<text class="clear-history" @click="clearHistory">清空</text>
</view>
<view class="history-items">
<view
v-for="(item, index) in searchHistory"
:key="index"
class="history-item"
@click="handleHistoryClick(item)"
>
<text class="history-icon"></text>
<text class="history-text">{{ item }}</text>
<text class="remove-history" @click.stop="removeHistoryItem(index)">✖️</text>
</view>
</view>
</view>
<!-- 搜索建议 -->
<view v-if="showSuggestions && suggestions.length > 0" class="search-suggestions">
<view class="suggestions-header">
<text class="suggestions-title">搜索建议</text>
</view>
<view class="suggestions-items">
<view
v-for="suggestion in suggestions"
:key="suggestion"
class="suggestion-item"
@click="handleSuggestionClick(suggestion)"
>
<text class="suggestion-icon"></text>
<text class="suggestion-text">{{ suggestion }}</text>
</view>
</view>
</view>
<!-- 搜索结果 -->
<view v-if="showResults" class="search-results">
<!-- 结果统计 -->
<view class="results-header">
<text class="results-count">找到 {{ totalResults }} 条消息</text>
<view class="sort-options">
<picker-view
class="sort-picker"
:value="[selectedSortIndex]"
@change="handleSortChange"
>
<picker-view-column>
<view v-for="(option, index) in sortOptions" :key="option.value">
<text class="sort-text">{{ option.label }}</text>
</view>
</picker-view-column>
</picker-view>
</view>
</view>
<!-- 结果列表 -->
<MessageList
:messages="searchResults"
:loading="searching"
:loading-more="loadingMore"
:has-more="hasMoreResults"
@click="handleResultClick"
@action="handleResultAction"
@load-more="handleLoadMoreResults"
/>
</view>
<!-- 无结果状态 -->
<view v-if="showNoResults" class="no-results">
<text class="no-results-icon"></text>
<text class="no-results-text">未找到相关消息</text>
<text class="no-results-desc">尝试使用其他关键词或调整筛选条件</text>
</view>
</view>
</template>
<script lang="uts">
import MessageList from './MessageList.uvue'
import type { Message, MessageType } from '../../utils/msgTypes.uts'
type TimeRange = {
value: string
label: string
}
type StatusOption = {
value: string
label: string
}
type SortOption = {
value: string
label: string
}
export default {
name: 'MessageSearch',
components: {
MessageList
},
props: {
messageTypes: {
type: Array as PropType<Array<MessageType>>,
default: (): Array<MessageType> => []
},
searching: {
type: Boolean,
default: false
}
},
emits: ['search', 'result-click', 'result-action'],
data() {
return {
keyword: '' as string,
selectedTypeId: '' as string,
selectedTimeRange: '' as string,
selectedStatus: '' as string,
selectedSortIndex: [0] as Array<number>,
searchResults: [] as Array<Message>,
totalResults: 0 as number,
loadingMore: false as boolean,
hasMoreResults: false as boolean,
searchHistory: [] as Array<string>,
suggestions: [] as Array<string>,
showHistory: true as boolean,
showSuggestions: false as boolean,
showResults: false as boolean,
showNoResults: false as boolean,
timeRanges: [
{ value: '', label: '全部时间' },
{ value: 'today', label: '今天' },
{ value: 'week', label: '本周' },
{ value: 'month', label: '本月' }
] as Array<TimeRange>,
statusOptions: [
{ value: '', label: '全部状态' },
{ value: 'unread', label: '未读' },
{ value: 'read', label: '已读' },
{ value: 'starred', label: '已收藏' },
{ value: 'urgent', label: '紧急' }
] as Array<StatusOption>,
sortOptions: [
{ value: 'time_desc', label: '时间降序' },
{ value: 'time_asc', label: '时间升序' },
{ value: 'priority_desc', label: '优先级降序' },
{ value: 'relevance', label: '相关度' }
] as Array<SortOption>
}
},
mounted() {
this.loadSearchHistory()
this.loadSuggestions()
},
methods: {
handleInput() {
// 实时搜索建议
if (this.keyword.length > 0) {
this.showHistory = false
this.showSuggestions = true
this.generateSuggestions()
} else {
this.showSuggestions = false
this.showHistory = true
this.showResults = false
this.showNoResults = false
}
},
handleSearch() {
if (this.keyword.trim().length === 0) {
return
}
this.performSearch()
this.addToHistory(this.keyword.trim())
this.showHistory = false
this.showSuggestions = false
},
handleClear() {
this.keyword = ''
this.showHistory = true
this.showSuggestions = false
this.showResults = false
this.showNoResults = false
},
handleTypeFilter(typeId: string) {
this.selectedTypeId = typeId
if (this.keyword.trim().length > 0) {
this.performSearch()
}
},
handleTimeFilter(timeRange: string) {
this.selectedTimeRange = timeRange
if (this.keyword.trim().length > 0) {
this.performSearch()
}
},
handleStatusFilter(status: string) {
this.selectedStatus = status
if (this.keyword.trim().length > 0) {
this.performSearch()
}
},
handleSortChange(e: UniPickerViewChangeEvent) {
this.selectedSortIndex = e.detail.value
if (this.keyword.trim().length > 0) {
this.performSearch()
}
},
handleHistoryClick(historyItem: string) {
this.keyword = historyItem
this.handleSearch()
},
handleSuggestionClick(suggestion: string) {
this.keyword = suggestion
this.handleSearch()
},
removeHistoryItem(index: number) {
this.searchHistory.splice(index, 1)
this.saveSearchHistory()
},
clearHistory() {
this.searchHistory = []
this.saveSearchHistory()
},
handleResultClick(message: Message) {
this.$emit('result-click', message)
},
handleResultAction(event: any) {
this.$emit('result-action', event)
},
handleLoadMoreResults() {
// 加载更多搜索结果
if (!this.loadingMore && this.hasMoreResults) {
this.loadingMore = true
// TODO: 实现分页搜索
this.loadingMore = false
}
},
performSearch() {
const searchParams = {
keyword: this.keyword.trim(),
typeId: this.selectedTypeId,
timeRange: this.selectedTimeRange,
status: this.selectedStatus,
sort: this.sortOptions[this.selectedSortIndex[0]].value
}
this.$emit('search', searchParams)
this.showResults = true
},
updateSearchResults(results: Array<Message>, total: number, hasMore: boolean) {
this.searchResults = results
this.totalResults = total
this.hasMoreResults = hasMore
this.showResults = true
this.showNoResults = results.length === 0
},
addToHistory(keyword: string) {
const index = this.searchHistory.indexOf(keyword)
if (index !== -1) {
this.searchHistory.splice(index, 1)
}
this.searchHistory.unshift(keyword)
// 限制历史记录数量
if (this.searchHistory.length > 10) {
this.searchHistory = this.searchHistory.slice(0, 10)
}
this.saveSearchHistory()
},
loadSearchHistory() {
try {
const historyStr = uni.getStorageSync('message_search_history')
if (historyStr !== null && historyStr !== '') {
this.searchHistory = JSON.parse(historyStr as string) as Array<string>
}
} catch (e) {
this.searchHistory = []
}
},
saveSearchHistory() {
try {
uni.setStorageSync('message_search_history', JSON.stringify(this.searchHistory))
} catch (e) {
console.error('保存搜索历史失败:', e)
}
},
loadSuggestions() {
// 加载常用搜索建议
this.suggestions = [
'系统通知',
'训练计划',
'作业提醒',
'紧急消息',
'今日消息'
]
},
generateSuggestions() {
// 根据关键词生成建议
const keyword = this.keyword.toLowerCase()
const filtered: Array<string> = []
for (let i = 0; i < this.suggestions.length; i++) {
const suggestion = this.suggestions[i]
if (suggestion.toLowerCase().indexOf(keyword) !== -1) {
filtered.push(suggestion)
}
}
this.suggestions = filtered
}
}
}
</script>
<style scoped>
.message-search {
height: 100%;
background-color: #f5f5f5;
}
.search-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background-color: #ffffff;
border-bottom: 1px solid #e0e0e0;
}
.search-input-wrapper {
flex: 1;
display: flex;
align-items: center;
background-color: #f8f9fa;
border-radius: 8px;
padding: 8px 12px;
}
.search-icon {
font-size: 16px;
color: #666666;
margin-right: 8px;
}
.search-input {
flex: 1;
font-size: 16px;
color: #333333;
border: none;
background: transparent;
}
.clear-icon {
font-size: 14px;
color: #999999;
padding: 4px;
}
.search-btn {
padding: 8px 16px;
background-color: #2196f3;
border: none;
border-radius: 6px;
}
.search-text {
font-size: 14px;
color: #ffffff;
}
.search-filters {
background-color: #ffffff;
border-bottom: 1px solid #e0e0e0;
}
.filter-scroll {
height: 50px;
}
.filter-items {
display: flex;
align-items: center;
padding: 0 16px;
gap: 8px;
}
.filter-item {
padding: 6px 12px;
border-radius: 16px;
background-color: #f0f0f0;
white-space: nowrap;
}
.filter-item.active {
background-color: #2196f3;
}
.filter-text {
font-size: 12px;
color: #666666;
}
.filter-item.active .filter-text {
color: #ffffff;
}
.filter-divider {
width: 1px;
height: 20px;
background-color: #e0e0e0;
margin: 0 8px;
}
.search-history,
.search-suggestions {
background-color: #ffffff;
margin-top: 8px;
}
.history-header,
.suggestions-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
}
.history-title,
.suggestions-title {
font-size: 14px;
font-weight: 400;
color: #333333;
}
.clear-history {
font-size: 12px;
color: #2196f3;
}
.history-items,
.suggestions-items {
padding: 0 16px;
}
.history-item,
.suggestion-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 0;
border-bottom: 1px solid #f8f8f8;
}
.history-icon,
.suggestion-icon {
font-size: 14px;
color: #999999;
}
.history-text,
.suggestion-text {
flex: 1;
font-size: 14px;
color: #333333;
}
.remove-history {
font-size: 12px;
color: #999999;
padding: 4px;
}
.search-results {
flex: 1;
background-color: #ffffff;
margin-top: 8px;
}
.results-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
}
.results-count {
font-size: 14px;
color: #666666;
}
.sort-picker {
width: 120px;
height: 30px;
}
.sort-text {
font-size: 12px;
color: #333333;
text-align: center;
line-height: 30px;
}
.no-results {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 300px;
background-color: #ffffff;
margin-top: 8px;
}
.no-results-icon {
font-size: 48px;
margin-bottom: 16px;
}
.no-results-text {
font-size: 16px;
color: #333333;
margin-bottom: 8px;
}
.no-results-desc {
font-size: 14px;
color: #666666;
text-align: center;
}
/* 响应式布局 */
@media (max-width: 480px) {
.search-header {
padding: 12px;
}
.filter-items {
padding: 0 12px;
}
.results-header {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
}
</style>