Initial Commit
This commit is contained in:
commit
1d71a02738
237 changed files with 64293 additions and 0 deletions
484
utils/message-search-manager.js
Normal file
484
utils/message-search-manager.js
Normal file
|
|
@ -0,0 +1,484 @@
|
|||
// 消息搜索管理器 - 微信小程序专用
|
||||
// 处理消息搜索、历史记录管理、本地缓存等
|
||||
|
||||
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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue