484 lines
13 KiB
JavaScript
484 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;
|