utils.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. import TUIChatEngine, {
  2. TUIFriendService,
  3. TUIConversationService,
  4. TUIGroupService,
  5. TUIChatService,
  6. TUITranslateService,
  7. SearchCloudMessagesParams,
  8. IGroupModel,
  9. TUIStore,
  10. StoreName,
  11. IMessageModel,
  12. } from '@tencentcloud/chat-uikit-engine';
  13. import { ISearchCloudMessageResult, IFriendType, ISearchResultListItem, IUserProfile } from '../../interface';
  14. import { searchMessageTypeList } from './search-type-list';
  15. import { Toast, TOAST_TYPE } from '../common/Toast/index';
  16. import { messageTypeAbstractMap } from './type';
  17. import { isUniFrameWork } from '../../utils/env';
  18. import { TUIGlobal } from '@tencentcloud/universal-api';
  19. /**************************************
  20. * TUISearch 搜索逻辑
  21. **************************************/
  22. // 消息云端搜索
  23. export const searchCloudMessages = (
  24. params: SearchCloudMessagesParams,
  25. ): Promise<{ data: ISearchCloudMessageResult }> => {
  26. return TUIChatService.searchCloudMessages(params)
  27. .then((imResponse) => {
  28. return imResponse;
  29. })
  30. .catch((error) => {
  31. Toast({
  32. message: TUITranslateService.t('TUISearch.消息云端搜索失败:') + error?.message,
  33. type: TOAST_TYPE.ERROR,
  34. duration: 3000,
  35. });
  36. return null;
  37. });
  38. };
  39. // 联系人搜索
  40. export const searchFriends = (userIDList: Array<string>): Promise<Array<IFriendType>> => {
  41. // 仅展示已存在好友关系的用户
  42. return TUIFriendService.getFriendProfile({ userIDList })
  43. .then((imResponse) => {
  44. return imResponse;
  45. })
  46. .catch((error) => {
  47. console.warn('search user failed:', error?.message);
  48. Toast({
  49. message: TUITranslateService.t('TUISearch.查找联系人失败:') + error?.message,
  50. type: TOAST_TYPE.ERROR,
  51. duration: 1000,
  52. });
  53. return null;
  54. });
  55. };
  56. // 搜索所有已加入群聊
  57. export const searchGroups = (groupIDList: Array<string>): Promise<Array<IGroupModel>> => {
  58. // searchGroupList.value = [];
  59. const promiseList: Array<Promise<IGroupModel>> = [];
  60. groupIDList.forEach((groupID: string) => {
  61. // todo: 此处需等待engine searchGroupByID 包裹好结果后,替换为 searchGroupByID 接口
  62. const promise = TUIGroupService.searchGroupByID(groupID)
  63. .then((imResponse) => {
  64. // 仅展示已加入的群聊
  65. if (imResponse?.data?.group?.isJoinedGroup) {
  66. return imResponse?.data?.group;
  67. }
  68. })
  69. .catch((error) => {
  70. console.warn('search group failed:', error?.message);
  71. });
  72. promiseList.push(promise);
  73. });
  74. return Promise.all(promiseList)
  75. .then((imResponse) => {
  76. return imResponse.filter(x => x !== undefined);
  77. })
  78. .catch((error) => {
  79. Toast({
  80. message: TUITranslateService.t('TUISearch.查找群聊失败:') + error?.message,
  81. type: TOAST_TYPE.ERROR,
  82. duration: 1000,
  83. });
  84. return [];
  85. });
  86. };
  87. /**************************************
  88. * TUISearch 交互逻辑
  89. **************************************/
  90. // 切换会话
  91. export const enterConversation = (item: { conversationID?: string;groupID?: string; userID?: string }) => {
  92. const conversationID
  93. = item?.conversationID || (item?.groupID ? `GROUP${item?.groupID}` : `C2C${item?.userID}`);
  94. TUIConversationService.switchConversation(conversationID)
  95. .then(() => {
  96. TUIStore.update(StoreName.SEARCH, 'currentSearchingStatus', {
  97. isSearching: false,
  98. searchType: 'global',
  99. });
  100. TUIStore.update(StoreName.SEARCH, 'currentSearchInputValue', {
  101. value: '',
  102. searchType: 'global',
  103. });
  104. isUniFrameWork && TUIGlobal?.navigateTo({
  105. url: '/TUIKit/components/TUIChat/index',
  106. });
  107. })
  108. .catch((error) => {
  109. console.warn('switch conversation failed:', error?.message);
  110. Toast({
  111. message: TUITranslateService.t('TUISearch.进入会话失败'),
  112. type: TOAST_TYPE.ERROR,
  113. duration: 1000,
  114. });
  115. });
  116. };
  117. /**************************************
  118. * TUISearch UI展示逻辑
  119. **************************************/
  120. // 解析搜索结果展示名称
  121. export const generateSearchResultShowName = (result: IMessageModel | ISearchResultListItem | IGroupModel | IFriendType | IUserProfile, resultContent: Record<string, string>): string => {
  122. if (!result) {
  123. return '';
  124. }
  125. if ((result as IMessageModel).ID) {
  126. return resultContent?.showName;
  127. }
  128. if ((result as IGroupModel).groupID) {
  129. return (result as IGroupModel).name || (result as IGroupModel).groupID;
  130. }
  131. if ((result as IFriendType | IUserProfile).userID) {
  132. return (result as IFriendType).remark || (result as IUserProfile).nick || (result as IFriendType).userID || '';
  133. }
  134. if ((result as ISearchResultListItem).conversation?.conversationID) {
  135. // 验证 uniapp 中 conversationModel 是否会失去原型导致解析失败
  136. if (typeof (result as ISearchResultListItem).conversation.getShowName === 'function') {
  137. return (result as ISearchResultListItem).conversation.getShowName();
  138. } else {
  139. return TUIStore.getConversationModel((result as ISearchResultListItem).conversation.conversationID)?.getShowName?.() || (result as ISearchResultListItem).conversation.conversationID;
  140. }
  141. }
  142. return '';
  143. };
  144. // 解析搜索结果展示头像
  145. export const generateSearchResultAvatar = (result: IMessageModel | ISearchResultListItem | IGroupModel | IFriendType | IUserProfile): string => {
  146. if (!result) {
  147. return '';
  148. }
  149. if ((result as IMessageModel).ID) {
  150. return (result as IMessageModel).avatar || 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png';
  151. }
  152. if ((result as IGroupModel).groupID) {
  153. return (result as IGroupModel).avatar || `https://web.sdk.qcloud.com/im/assets/images/${(result as IGroupModel)?.type}.svg`;
  154. }
  155. if ((result as IUserProfile).userID) {
  156. return (result as IUserProfile).avatar || 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png';
  157. }
  158. if ((result as ISearchResultListItem)?.conversation?.conversationID) {
  159. // 验证 uniapp 中 conversationModel 是否会失去原型导致解析失败
  160. if (typeof (result as ISearchResultListItem).conversation.getAvatar === 'function') {
  161. return (result as ISearchResultListItem).conversation?.getAvatar();
  162. } else {
  163. return TUIStore.getConversationModel((result as ISearchResultListItem).conversation.conversationID)?.getAvatar?.();
  164. }
  165. }
  166. return '';
  167. };
  168. // 解析搜索结果展示内容(包含对于关键词内容匹配高亮)
  169. export const generateSearchResultShowContent = (
  170. result: IMessageModel | ISearchResultListItem | IGroupModel | IUserProfile,
  171. resultType: string,
  172. keywordList: Array<string>,
  173. isTypeShow = true, // 除文本消息以外类型消息是否需要类型前缀
  174. ): Array<{ text: string; isHighlight: boolean }> => {
  175. if ((result as IGroupModel)?.groupID) {
  176. return [
  177. { text: 'groupID: ', isHighlight: false },
  178. { text: (result as IGroupModel).groupID, isHighlight: true },
  179. ];
  180. }
  181. if ((result as IUserProfile)?.userID) {
  182. return [
  183. { text: 'userID: ', isHighlight: false },
  184. { text: (result as IUserProfile).userID, isHighlight: true },
  185. ];
  186. }
  187. if ((result as ISearchResultListItem)?.conversation || (result as IMessageModel)?.flow) {
  188. if ((result as ISearchResultListItem)?.messageCount === 1 || (result as IMessageModel)?.flow) {
  189. // 单条消息摘要显示结果:
  190. // 文本消息,显示消息内容+关键词高亮
  191. // 文件类型消息,显示[文件]文件名+关键词高亮
  192. // 自定义类型消息,显示[自定义消息]description+关键词高亮
  193. // 其他类型消息,显示[消息类型]
  194. const message: IMessageModel = (result as IMessageModel)?.flow
  195. ? (result as IMessageModel)
  196. : (result as ISearchResultListItem)?.messageList[0];
  197. const text
  198. = message?.payload?.text || message?.payload?.fileName || message?.payload?.description;
  199. const abstract: Array<{ text: string; isHighlight: boolean }> = [];
  200. if ((result as IMessageModel)?.type !== TUIChatEngine.TYPES.MSG_TEXT && isTypeShow) {
  201. abstract.push({
  202. text: TUITranslateService.t(`TUISearch.${messageTypeAbstractMap[(result as IMessageModel)?.type]}`),
  203. isHighlight: false,
  204. });
  205. }
  206. abstract.push(...generateMessageContentHighlight(text, keywordList));
  207. return abstract;
  208. } else {
  209. return [
  210. {
  211. text: `${(result as ISearchResultListItem)?.messageCount}${TUITranslateService.t(
  212. 'TUISearch.条相关',
  213. )}${TUITranslateService.t(
  214. `TUISearch.${
  215. resultType === 'allMessage' ? '结果' : searchMessageTypeList[resultType]?.label
  216. }`,
  217. )}`,
  218. isHighlight: false,
  219. },
  220. ];
  221. }
  222. }
  223. return [];
  224. };
  225. // 解析搜索消息结果【高亮关键词】位置
  226. export const generateMessageContentHighlight = (
  227. content: string,
  228. keywordList: Array<string>,
  229. ): Array<{ text: string; isHighlight: boolean }> => {
  230. if (!content || !keywordList || !keywordList.length) {
  231. return [{ text: content || '', isHighlight: false }];
  232. }
  233. // 获取所有 key 匹配的开始与结束位置
  234. const matches: Array<Array<number>> = [];
  235. for (let i = 0; i < keywordList.length; i++) {
  236. // 特殊字符转译
  237. const substring = keywordList[i]?.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  238. const regex = new RegExp(substring, 'gi'); // 全局搜索并且忽略大小写
  239. let match;
  240. while ((match = regex.exec(content)) !== null) {
  241. const start: number = match.index;
  242. const end: number = match.index + match[0].length - 1;
  243. matches.push([start, end]);
  244. }
  245. }
  246. // 合并重复范围结果, 并按照从小到达排列,比如 [[1,3],[2,4]]合并为[[1,4]]
  247. const mergedRanges = [matches[0]];
  248. if (matches.length > 1) {
  249. matches.sort((a: Array<number>, b: Array<number>) => a[0] - b[0]);
  250. // const mergedRanges = [matches[0]];
  251. for (let i = 1; i < matches.length; i++) {
  252. const currentRange = matches[i];
  253. const lastMergedRange = mergedRanges[mergedRanges.length - 1];
  254. // currentRange[0] - 1 是为了处理[[1,2],[3,4]]能合并为[[1,4]]的特殊情况
  255. if (currentRange[0] - 1 <= lastMergedRange[1]) {
  256. lastMergedRange[1] = Math.max(lastMergedRange[1], currentRange[1]);
  257. } else {
  258. mergedRanges.push(currentRange);
  259. }
  260. }
  261. }
  262. if (!mergedRanges[0]) {
  263. return [{ text: content, isHighlight: false }];
  264. }
  265. // 根据高亮范围分割原内容字符串,增加highlight相关标识字段
  266. const contentArray: Array<{ text: string; isHighlight: boolean }> = [];
  267. let start = 0;
  268. for (let i = 0; i < mergedRanges.length; i++) {
  269. const str1 = content.substring(start, mergedRanges[i][0]);
  270. str1 && contentArray.push({ text: str1, isHighlight: false });
  271. const str2 = content.substring(mergedRanges[i][0], mergedRanges[i][1] + 1);
  272. str2 && contentArray.push({ text: str2, isHighlight: true });
  273. start = mergedRanges[i][1] + 1;
  274. }
  275. // 添加结尾最后一段
  276. const lastStr = content.substring(start);
  277. lastStr && contentArray.push({ text: lastStr, isHighlight: false });
  278. return contentArray;
  279. };
  280. // 计算时间戳函数
  281. // calculate timestamp
  282. export const generateSearchResultTime = (timestamp: number): string => {
  283. const todayZero = new Date().setHours(0, 0, 0, 0);
  284. const thisYear = new Date(new Date().getFullYear(), 0, 1, 0, 0, 0, 0).getTime();
  285. const target = new Date(timestamp);
  286. const oneDay = 24 * 60 * 60 * 1000;
  287. const oneWeek = 7 * oneDay;
  288. const diff = todayZero - target.getTime();
  289. function formatNum(num: number): string {
  290. return num < 10 ? '0' + num : num.toString();
  291. }
  292. if (diff <= 0) {
  293. // today, only display hour:minute
  294. return `${formatNum(target.getHours())}:${formatNum(target.getMinutes())}`;
  295. } else if (diff <= oneDay) {
  296. // yesterday, display yesterday:hour:minute
  297. return `${TUITranslateService.t('time.昨天')} ${formatNum(target.getHours())}:${formatNum(
  298. target.getMinutes(),
  299. )}`;
  300. } else if (diff <= oneWeek - oneDay) {
  301. // Within a week, display weekday hour:minute
  302. const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
  303. const weekday = weekdays[target.getDay()];
  304. return `${TUITranslateService.t('time.' + weekday)} ${formatNum(target.getHours())}:${formatNum(
  305. target.getMinutes(),
  306. )}`;
  307. } else if (target.getTime() >= thisYear) {
  308. // Over a week, within this year, display mouth/day hour:minute
  309. return `${target.getMonth() + 1}/${target.getDate()} ${formatNum(
  310. target.getHours(),
  311. )}:${formatNum(target.getMinutes())}`;
  312. } else {
  313. // Not within this year, display year/mouth/day hour:minute
  314. return `${target.getFullYear()}/${target.getMonth() + 1}/${target.getDate()} ${formatNum(
  315. target.getHours(),
  316. )}:${formatNum(target.getMinutes())}`;
  317. }
  318. };
  319. // 计算日期函数
  320. export const generateSearchResultYMD = (timestamp: number): string => {
  321. const date = new Date(timestamp * 1000); // 将时间戳转换为毫秒
  322. const year = date.getFullYear(); // 获取年份
  323. const month = ('0' + (date.getMonth() + 1)).slice(-2); // 获取月份,并补零
  324. const day = ('0' + date.getDate()).slice(-2); // 获取日期,并补零
  325. return `${year}-${month}-${day}`; // 返回年月日格式的字符串
  326. };
  327. export const debounce = <F extends (...args: any[]) => void>(func: F, waitFor: number) => {
  328. let timeout: ReturnType<typeof setTimeout> | null = null;
  329. const debounced = (...args: Parameters<F>) => {
  330. if (timeout !== null) {
  331. clearTimeout(timeout);
  332. timeout = null;
  333. }
  334. timeout = setTimeout(() => func(...args), waitFor);
  335. };
  336. return debounced as (...args: Parameters<F>) => ReturnType<F>;
  337. };