Initial commit of akmon project
This commit is contained in:
656
components/message/MessageSearch.uvue
Normal file
656
components/message/MessageSearch.uvue
Normal file
@@ -0,0 +1,656 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user