657 lines
15 KiB
Plaintext
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>
|