485 lines
13 KiB
JavaScript
485 lines
13 KiB
JavaScript
|
|
// 消息搜索管理器 - 微信小程序专用
|
|||
|
|
// 处理消息搜索、历史记录管理、本地缓存等
|
|||
|
|
|
|||
|
|
const apiClient = require('./api-client.js');
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 消息搜索管理器
|
|||
|
|
* 功能:
|
|||
|
|
* 1. 全局消息搜索
|
|||
|
|
* 2. 会话内搜索
|
|||
|
|
* 3. 搜索结果高亮
|
|||
|
|
* 4. 搜索历史管理
|
|||
|
|
* 5. 本地缓存优化
|
|||
|
|
* 6. 分页加载
|
|||
|
|
*/
|
|||
|
|
class MessageSearchManager {
|
|||
|
|
constructor() {
|
|||
|
|
this.isInitialized = false;
|
|||
|
|
|
|||
|
|
// 搜索配置
|
|||
|
|
this.searchConfig = {
|
|||
|
|
// 最小搜索关键词长度
|
|||
|
|
minKeywordLength: 1,
|
|||
|
|
|
|||
|
|
// 搜索结果每页数量
|
|||
|
|
pageSize: 20,
|
|||
|
|
|
|||
|
|
// 最大搜索历史数量
|
|||
|
|
maxSearchHistory: 50,
|
|||
|
|
|
|||
|
|
// 本地缓存过期时间(毫秒)
|
|||
|
|
cacheExpireTime: 30 * 60 * 1000, // 30分钟
|
|||
|
|
|
|||
|
|
// 搜索防抖延迟(毫秒)
|
|||
|
|
debounceDelay: 300,
|
|||
|
|
|
|||
|
|
// 支持的搜索类型
|
|||
|
|
searchTypes: ['text', 'image', 'file', 'all']
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 搜索缓存
|
|||
|
|
this.searchCache = new Map();
|
|||
|
|
|
|||
|
|
// 搜索历史
|
|||
|
|
this.searchHistory = [];
|
|||
|
|
|
|||
|
|
// 当前搜索状态
|
|||
|
|
this.currentSearch = {
|
|||
|
|
keyword: '',
|
|||
|
|
type: 'all',
|
|||
|
|
conversationId: null,
|
|||
|
|
page: 1,
|
|||
|
|
hasMore: true,
|
|||
|
|
loading: false,
|
|||
|
|
results: []
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 防抖定时器
|
|||
|
|
this.debounceTimer = null;
|
|||
|
|
|
|||
|
|
this.init();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 初始化搜索管理器
|
|||
|
|
async init() {
|
|||
|
|
if (this.isInitialized) return;
|
|||
|
|
|
|||
|
|
console.log('🔍 初始化消息搜索管理器...');
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 加载搜索历史
|
|||
|
|
await this.loadSearchHistory();
|
|||
|
|
|
|||
|
|
// 清理过期缓存
|
|||
|
|
this.cleanupExpiredCache();
|
|||
|
|
|
|||
|
|
this.isInitialized = true;
|
|||
|
|
console.log('✅ 消息搜索管理器初始化完成');
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('❌ 消息搜索管理器初始化失败:', error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 全局搜索消息
|
|||
|
|
async searchMessages(keyword, options = {}) {
|
|||
|
|
try {
|
|||
|
|
// 验证搜索关键词
|
|||
|
|
if (!this.validateKeyword(keyword)) {
|
|||
|
|
return { success: false, error: '搜索关键词无效' };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log('🔍 搜索消息:', keyword);
|
|||
|
|
|
|||
|
|
// 设置搜索参数
|
|||
|
|
const searchParams = {
|
|||
|
|
keyword: keyword.trim(),
|
|||
|
|
type: options.type || 'all',
|
|||
|
|
conversationId: options.conversationId || null,
|
|||
|
|
page: options.page || 1,
|
|||
|
|
pageSize: options.pageSize || this.searchConfig.pageSize
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 检查缓存
|
|||
|
|
const cacheKey = this.generateCacheKey(searchParams);
|
|||
|
|
const cachedResult = this.getFromCache(cacheKey);
|
|||
|
|
if (cachedResult) {
|
|||
|
|
console.log('🔍 使用缓存搜索结果');
|
|||
|
|
return cachedResult;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新搜索状态
|
|||
|
|
this.updateSearchState(searchParams);
|
|||
|
|
|
|||
|
|
// 执行搜索
|
|||
|
|
const result = await this.performSearch(searchParams);
|
|||
|
|
|
|||
|
|
// 缓存结果
|
|||
|
|
if (result.success) {
|
|||
|
|
this.saveToCache(cacheKey, result);
|
|||
|
|
|
|||
|
|
// 添加到搜索历史
|
|||
|
|
this.addToSearchHistory(keyword);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return result;
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('❌ 搜索消息失败:', error);
|
|||
|
|
return { success: false, error: error.message };
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 会话内搜索
|
|||
|
|
async searchInConversation(conversationId, keyword, options = {}) {
|
|||
|
|
return await this.searchMessages(keyword, {
|
|||
|
|
...options,
|
|||
|
|
conversationId: conversationId
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 防抖搜索
|
|||
|
|
searchWithDebounce(keyword, options = {}, callback) {
|
|||
|
|
// 清除之前的定时器
|
|||
|
|
if (this.debounceTimer) {
|
|||
|
|
clearTimeout(this.debounceTimer);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置新的定时器
|
|||
|
|
this.debounceTimer = setTimeout(async () => {
|
|||
|
|
try {
|
|||
|
|
const result = await this.searchMessages(keyword, options);
|
|||
|
|
if (callback) {
|
|||
|
|
callback(result);
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('❌ 防抖搜索失败:', error);
|
|||
|
|
if (callback) {
|
|||
|
|
callback({ success: false, error: error.message });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}, this.searchConfig.debounceDelay);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 执行搜索
|
|||
|
|
async performSearch(searchParams) {
|
|||
|
|
try {
|
|||
|
|
this.currentSearch.loading = true;
|
|||
|
|
|
|||
|
|
// 构建搜索请求
|
|||
|
|
const requestData = {
|
|||
|
|
keyword: searchParams.keyword,
|
|||
|
|
messageType: searchParams.type === 'all' ? null : this.getMessageTypeCode(searchParams.type),
|
|||
|
|
page: searchParams.page,
|
|||
|
|
pageSize: searchParams.pageSize
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 如果指定了会话ID,则进行会话内搜索
|
|||
|
|
if (searchParams.conversationId) {
|
|||
|
|
requestData.conversationId = searchParams.conversationId;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 调用搜索API(按统一客户端签名)
|
|||
|
|
const response = await apiClient.post('/api/v1/messages/search', requestData);
|
|||
|
|
|
|||
|
|
if (response && (response.code === 0 || response.code === 200 || response.success)) {
|
|||
|
|
const searchResult = {
|
|||
|
|
success: true,
|
|||
|
|
data: {
|
|||
|
|
messages: response.data?.messages || [],
|
|||
|
|
total: response.data?.total || 0,
|
|||
|
|
page: searchParams.page,
|
|||
|
|
pageSize: searchParams.pageSize,
|
|||
|
|
hasMore: response.data?.hasMore || false,
|
|||
|
|
keyword: searchParams.keyword,
|
|||
|
|
searchTime: Date.now()
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 处理搜索结果
|
|||
|
|
searchResult.data.messages = this.processSearchResults(
|
|||
|
|
searchResult.data.messages,
|
|||
|
|
searchParams.keyword
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
console.log(`🔍 搜索完成,找到 ${searchResult.data.total} 条消息`);
|
|||
|
|
return searchResult;
|
|||
|
|
|
|||
|
|
} else {
|
|||
|
|
const msg = response?.message || response?.error || '搜索失败';
|
|||
|
|
throw new Error(msg);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('❌ 执行搜索失败:', error);
|
|||
|
|
return { success: false, error: error.message };
|
|||
|
|
|
|||
|
|
} finally {
|
|||
|
|
this.currentSearch.loading = false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理搜索结果
|
|||
|
|
processSearchResults(messages, keyword) {
|
|||
|
|
return messages.map(message => {
|
|||
|
|
// 添加高亮信息
|
|||
|
|
const highlightedContent = this.highlightKeyword(message.content, keyword);
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
...message,
|
|||
|
|
highlightedContent: highlightedContent,
|
|||
|
|
searchKeyword: keyword,
|
|||
|
|
// 添加消息摘要(用于显示上下文)
|
|||
|
|
summary: this.generateMessageSummary(message.content, keyword)
|
|||
|
|
};
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 关键词高亮
|
|||
|
|
highlightKeyword(content, keyword) {
|
|||
|
|
if (!content || !keyword) return content;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 转义特殊字符
|
|||
|
|
const escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|||
|
|
const regex = new RegExp(`(${escapedKeyword})`, 'gi');
|
|||
|
|
|
|||
|
|
return content.replace(regex, '<mark class="search-highlight">$1</mark>');
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('❌ 关键词高亮失败:', error);
|
|||
|
|
return content;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 生成消息摘要
|
|||
|
|
generateMessageSummary(content, keyword, maxLength = 100) {
|
|||
|
|
if (!content || !keyword) return content;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const keywordIndex = content.toLowerCase().indexOf(keyword.toLowerCase());
|
|||
|
|
if (keywordIndex === -1) return content.substring(0, maxLength);
|
|||
|
|
|
|||
|
|
// 计算摘要范围
|
|||
|
|
const start = Math.max(0, keywordIndex - 30);
|
|||
|
|
const end = Math.min(content.length, keywordIndex + keyword.length + 30);
|
|||
|
|
|
|||
|
|
let summary = content.substring(start, end);
|
|||
|
|
|
|||
|
|
// 添加省略号
|
|||
|
|
if (start > 0) summary = '...' + summary;
|
|||
|
|
if (end < content.length) summary = summary + '...';
|
|||
|
|
|
|||
|
|
return summary;
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('❌ 生成消息摘要失败:', error);
|
|||
|
|
return content.substring(0, maxLength);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 加载更多搜索结果
|
|||
|
|
async loadMoreResults() {
|
|||
|
|
if (!this.currentSearch.hasMore || this.currentSearch.loading) {
|
|||
|
|
return { success: false, error: '没有更多结果' };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const nextPage = this.currentSearch.page + 1;
|
|||
|
|
const result = await this.searchMessages(this.currentSearch.keyword, {
|
|||
|
|
type: this.currentSearch.type,
|
|||
|
|
conversationId: this.currentSearch.conversationId,
|
|||
|
|
page: nextPage
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (result.success) {
|
|||
|
|
// 合并结果
|
|||
|
|
this.currentSearch.results = [...this.currentSearch.results, ...result.data.messages];
|
|||
|
|
this.currentSearch.page = nextPage;
|
|||
|
|
this.currentSearch.hasMore = result.data.hasMore;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 清除搜索结果
|
|||
|
|
clearSearchResults() {
|
|||
|
|
this.currentSearch = {
|
|||
|
|
keyword: '',
|
|||
|
|
type: 'all',
|
|||
|
|
conversationId: null,
|
|||
|
|
page: 1,
|
|||
|
|
hasMore: true,
|
|||
|
|
loading: false,
|
|||
|
|
results: []
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证搜索关键词
|
|||
|
|
validateKeyword(keyword) {
|
|||
|
|
if (!keyword || typeof keyword !== 'string') {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const trimmedKeyword = keyword.trim();
|
|||
|
|
return trimmedKeyword.length >= this.searchConfig.minKeywordLength;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取消息类型代码
|
|||
|
|
getMessageTypeCode(type) {
|
|||
|
|
const typeMap = {
|
|||
|
|
'text': 0,
|
|||
|
|
'image': 1,
|
|||
|
|
'voice': 2,
|
|||
|
|
'video': 3,
|
|||
|
|
'file': 4
|
|||
|
|
};
|
|||
|
|
return typeMap[type] || null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新搜索状态
|
|||
|
|
updateSearchState(searchParams) {
|
|||
|
|
this.currentSearch = {
|
|||
|
|
keyword: searchParams.keyword,
|
|||
|
|
type: searchParams.type,
|
|||
|
|
conversationId: searchParams.conversationId,
|
|||
|
|
page: searchParams.page,
|
|||
|
|
hasMore: true,
|
|||
|
|
loading: true,
|
|||
|
|
results: []
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 生成缓存键
|
|||
|
|
generateCacheKey(searchParams) {
|
|||
|
|
return `search_${searchParams.keyword}_${searchParams.type}_${searchParams.conversationId || 'global'}_${searchParams.page}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 从缓存获取
|
|||
|
|
getFromCache(cacheKey) {
|
|||
|
|
const cached = this.searchCache.get(cacheKey);
|
|||
|
|
if (cached && (Date.now() - cached.timestamp) < this.searchConfig.cacheExpireTime) {
|
|||
|
|
return cached.data;
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 保存到缓存
|
|||
|
|
saveToCache(cacheKey, data) {
|
|||
|
|
this.searchCache.set(cacheKey, {
|
|||
|
|
data: data,
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 限制缓存大小
|
|||
|
|
if (this.searchCache.size > 100) {
|
|||
|
|
const firstKey = this.searchCache.keys().next().value;
|
|||
|
|
this.searchCache.delete(firstKey);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 清理过期缓存
|
|||
|
|
cleanupExpiredCache() {
|
|||
|
|
const now = Date.now();
|
|||
|
|
for (const [key, value] of this.searchCache) {
|
|||
|
|
if (now - value.timestamp > this.searchConfig.cacheExpireTime) {
|
|||
|
|
this.searchCache.delete(key);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 添加到搜索历史
|
|||
|
|
addToSearchHistory(keyword) {
|
|||
|
|
const trimmedKeyword = keyword.trim();
|
|||
|
|
if (!trimmedKeyword) return;
|
|||
|
|
|
|||
|
|
// 移除重复项
|
|||
|
|
this.searchHistory = this.searchHistory.filter(item => item !== trimmedKeyword);
|
|||
|
|
|
|||
|
|
// 添加到开头
|
|||
|
|
this.searchHistory.unshift(trimmedKeyword);
|
|||
|
|
|
|||
|
|
// 限制历史数量
|
|||
|
|
if (this.searchHistory.length > this.searchConfig.maxSearchHistory) {
|
|||
|
|
this.searchHistory = this.searchHistory.slice(0, this.searchConfig.maxSearchHistory);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 保存到本地存储
|
|||
|
|
this.saveSearchHistory();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取搜索历史
|
|||
|
|
getSearchHistory() {
|
|||
|
|
return [...this.searchHistory];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 清除搜索历史
|
|||
|
|
clearSearchHistory() {
|
|||
|
|
this.searchHistory = [];
|
|||
|
|
this.saveSearchHistory();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 删除搜索历史项
|
|||
|
|
removeSearchHistoryItem(keyword) {
|
|||
|
|
this.searchHistory = this.searchHistory.filter(item => item !== keyword);
|
|||
|
|
this.saveSearchHistory();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 加载搜索历史
|
|||
|
|
async loadSearchHistory() {
|
|||
|
|
try {
|
|||
|
|
const history = wx.getStorageSync('messageSearchHistory') || [];
|
|||
|
|
this.searchHistory = history;
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('❌ 加载搜索历史失败:', error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 保存搜索历史
|
|||
|
|
async saveSearchHistory() {
|
|||
|
|
try {
|
|||
|
|
wx.setStorageSync('messageSearchHistory', this.searchHistory);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('❌ 保存搜索历史失败:', error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取搜索建议
|
|||
|
|
getSearchSuggestions(keyword) {
|
|||
|
|
if (!keyword) return this.searchHistory.slice(0, 10);
|
|||
|
|
|
|||
|
|
const lowerKeyword = keyword.toLowerCase();
|
|||
|
|
return this.searchHistory
|
|||
|
|
.filter(item => item.toLowerCase().includes(lowerKeyword))
|
|||
|
|
.slice(0, 10);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取当前搜索状态
|
|||
|
|
getCurrentSearchState() {
|
|||
|
|
return { ...this.currentSearch };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取搜索统计
|
|||
|
|
getSearchStats() {
|
|||
|
|
return {
|
|||
|
|
historyCount: this.searchHistory.length,
|
|||
|
|
cacheSize: this.searchCache.size,
|
|||
|
|
currentKeyword: this.currentSearch.keyword,
|
|||
|
|
isLoading: this.currentSearch.loading,
|
|||
|
|
hasResults: this.currentSearch.results.length > 0
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 重置搜索管理器
|
|||
|
|
reset() {
|
|||
|
|
this.searchCache.clear();
|
|||
|
|
this.clearSearchResults();
|
|||
|
|
this.clearSearchHistory();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 创建全局实例
|
|||
|
|
const messageSearchManager = new MessageSearchManager();
|
|||
|
|
|
|||
|
|
module.exports = messageSearchManager;
|