index.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. <template>
  2. <div ref="conversationListInnerDomRef" class="tui-conversation-list">
  3. <ActionsMenu
  4. v-if="isShowOverlay"
  5. :selectedConversation="currentConversation"
  6. :actionsMenuPosition="actionsMenuPosition"
  7. :selectedConversationDomRect="currentConversationDomRect"
  8. @closeConversationActionMenu="closeConversationActionMenu"
  9. />
  10. <div
  11. v-for="(conversation, index) in conversationList"
  12. :id="`convlistitem-${index}`"
  13. :key="index"
  14. :class="[
  15. 'tui-conversation-content',
  16. isMobile && 'tui-conversation-content-h5 disable-select',
  17. ]"
  18. >
  19. <div
  20. :class="[
  21. isPC && 'isPC',
  22. 'tui-conversation-item',
  23. currentConversationID === conversation.conversationID &&
  24. 'tui-conversation-item-selected',
  25. conversation.isPinned && 'tui-conversation-item-pinned',
  26. ]"
  27. @click="
  28. enterConversationChat(conversation.conversationID, conversation)
  29. "
  30. @longpress="showConversationActionMenu($event, conversation, index)"
  31. @contextmenu="
  32. showConversationActionMenu($event, conversation, index, true)
  33. "
  34. >
  35. <aside class="left">
  36. <Avatar
  37. useSkeletonAnimation
  38. :url="conversation.userProfile.avatar"
  39. size="60px"
  40. />
  41. <!-- <div
  42. v-if="userOnlineStatusMap && isShowUserOnlineStatus(conversation)"
  43. :class="[
  44. 'online-status',
  45. Object.keys(userOnlineStatusMap).length > 0 &&
  46. Object.keys(userOnlineStatusMap).includes(
  47. conversation.userProfile.userID
  48. ) &&
  49. userOnlineStatusMap[conversation.userProfile.userID]
  50. .statusType === 1
  51. ? 'online-status-online'
  52. : 'online-status-offline',
  53. ]"
  54. /> -->
  55. <span
  56. v-if="conversation.unreadCount > 0 && !conversation.isMuted"
  57. class="num"
  58. >
  59. {{
  60. conversation.unreadCount > 99 ? "99+" : conversation.unreadCount
  61. }}
  62. </span>
  63. <span
  64. v-if="conversation.unreadCount > 0 && conversation.isMuted"
  65. class="num-notify"
  66. />
  67. </aside>
  68. <div class="content">
  69. <div class="content-header">
  70. <label class="content-header-label">
  71. <p class="name">{{ conversation.getShowName() }}</p>
  72. </label>
  73. <div class="middle-box">
  74. <span
  75. v-if="
  76. conversation.type === 'GROUP' &&
  77. conversation.groupAtInfoList &&
  78. conversation.groupAtInfoList.length > 0
  79. "
  80. class="middle-box-at"
  81. >{{ conversation.getGroupAtInfo() }}</span
  82. >
  83. <p class="middle-box-content">
  84. {{ conversation.getLastMessage("text") }}
  85. </p>
  86. </div>
  87. </div>
  88. <div class="content-footer">
  89. <span class="time">{{ conversation.getLastMessage("time") }}</span>
  90. <Icon v-if="conversation.isMuted" :file="muteIcon" />
  91. </div>
  92. </div>
  93. </div>
  94. </div>
  95. <empty-view
  96. v-if="conversationList.length == 0"
  97. title="暂无数据"
  98. ></empty-view>
  99. </div>
  100. </template>
  101. <script lang="ts" setup>
  102. interface IUserStatus {
  103. statusType: number;
  104. customStatus: string;
  105. }
  106. interface IUserStatusMap {
  107. [userID: string]: IUserStatus;
  108. }
  109. import { ref, onMounted, onUnmounted } from "../../../adapter-vue";
  110. import TUIChatEngine, {
  111. TUIStore,
  112. StoreName,
  113. TUIConversationService,
  114. IConversationModel,
  115. } from "@tencentcloud/chat-uikit-engine";
  116. import {
  117. TUIGlobal,
  118. isIOS,
  119. addLongPressListener,
  120. } from "@tencentcloud/universal-api";
  121. import Icon from "../../common/Icon.vue";
  122. import Avatar from "../../common/Avatar/index.vue";
  123. import ActionsMenu from "../actions-menu/index.vue";
  124. import muteIcon from "../../../assets/icon/mute.svg";
  125. import emptyView from "@/components/empty-view.vue";
  126. import { isPC, isH5, isUniFrameWork, isMobile } from "../../../utils/env";
  127. const emits = defineEmits(["handleSwitchConversation", "getPassingRef"]);
  128. const currentConversation = ref<IConversationModel>();
  129. const currentConversationID = ref<string>();
  130. const currentConversationDomRect = ref<DOMRect>();
  131. const isShowOverlay = ref<boolean>(false);
  132. const conversationList = ref<IConversationModel[]>([]);
  133. setTimeout(() => {
  134. console.log("会话列表数据")
  135. console.log(conversationList);
  136. }, 2000);
  137. const conversationListDomRef = ref<HTMLElement | undefined>();
  138. const conversationListInnerDomRef = ref<HTMLElement | undefined>();
  139. const actionsMenuPosition = ref<{
  140. top: number;
  141. left: number | undefined;
  142. conversationHeight: number | undefined;
  143. }>({
  144. top: 0,
  145. left: undefined,
  146. conversationHeight: undefined,
  147. });
  148. const displayOnlineStatus = ref(false); // 在线状态 默认关闭
  149. const userOnlineStatusMap = ref<IUserStatusMap>();
  150. let lastestOpenActionsMenuTime: number | null = null;
  151. onMounted(() => {
  152. TUIStore.watch(StoreName.CONV, {
  153. currentConversationID: onCurrentConversationIDUpdated,
  154. conversationList: onConversationListUpdated,
  155. currentConversation: onCurrentConversationUpdated,
  156. });
  157. // 初始状态
  158. TUIStore.watch(StoreName.USER, {
  159. displayOnlineStatus: onDisplayOnlineStatusUpdated,
  160. userStatusList: onUserStatusListUpdated,
  161. });
  162. if (!isUniFrameWork && isIOS && !isPC) {
  163. addLongPressHandler();
  164. }
  165. });
  166. onUnmounted(() => {
  167. TUIStore.unwatch(StoreName.CONV, {
  168. currentConversationID: onCurrentConversationIDUpdated,
  169. conversationList: onConversationListUpdated,
  170. currentConversation: onCurrentConversationUpdated,
  171. });
  172. // 初始状态
  173. TUIStore.unwatch(StoreName.USER, {
  174. displayOnlineStatus: onDisplayOnlineStatusUpdated,
  175. userStatusList: onUserStatusListUpdated,
  176. });
  177. });
  178. const isShowUserOnlineStatus = (conversation: IConversationModel): boolean => {
  179. return (
  180. displayOnlineStatus.value &&
  181. conversation.type === TUIChatEngine.TYPES.CONV_C2C
  182. );
  183. };
  184. const showConversationActionMenu = (
  185. event: Event,
  186. conversation: IConversationModel,
  187. index: number,
  188. isContextMenuEvent?: boolean
  189. ) => {
  190. if (isContextMenuEvent) {
  191. event.preventDefault();
  192. if (isUniFrameWork) {
  193. return;
  194. }
  195. }
  196. currentConversation.value = conversation;
  197. lastestOpenActionsMenuTime = Date.now();
  198. getActionsMenuPosition(event, index);
  199. };
  200. const closeConversationActionMenu = () => {
  201. // 防止连续触发overlay的tap事件
  202. if (
  203. lastestOpenActionsMenuTime &&
  204. Date.now() - lastestOpenActionsMenuTime > 300
  205. ) {
  206. currentConversation.value = undefined;
  207. isShowOverlay.value = false;
  208. }
  209. };
  210. const getActionsMenuPosition = (event: Event, index: number) => {
  211. if (isUniFrameWork) {
  212. if (typeof conversationListDomRef.value === "undefined") {
  213. emits("getPassingRef", conversationListDomRef);
  214. }
  215. const query = TUIGlobal?.createSelectorQuery().in(
  216. conversationListDomRef.value
  217. );
  218. query
  219. .select(`#convlistitem-${index}`)
  220. .boundingClientRect((data) => {
  221. if (data) {
  222. actionsMenuPosition.value = {
  223. // uni-h5的uni-page-head不被认为是视窗中的成员,因此手动上head的高度
  224. top: data.bottom + (isH5 ? 44 : 0),
  225. // @ts-expect-error in uniapp event has touches property
  226. left: event.touches[0].pageX,
  227. conversationHeight: data.height,
  228. };
  229. isShowOverlay.value = true;
  230. }
  231. })
  232. .exec();
  233. } else {
  234. // 处理Vue原生
  235. const rect =
  236. (
  237. (event.currentTarget || event.target) as HTMLElement
  238. )?.getBoundingClientRect() || {};
  239. if (rect) {
  240. actionsMenuPosition.value = {
  241. top: rect.bottom,
  242. left: isPC ? (event as MouseEvent).clientX : undefined,
  243. conversationHeight: rect.height,
  244. };
  245. }
  246. isShowOverlay.value = true;
  247. }
  248. };
  249. const enterConversationChat = (conversationID: string, conversation: any) => {
  250. emits("handleSwitchConversation", conversationID);
  251. TUIConversationService.switchConversation(conversationID);
  252. };
  253. function addLongPressHandler() {
  254. if (!conversationListInnerDomRef.value) {
  255. return;
  256. }
  257. addLongPressListener({
  258. element: conversationListInnerDomRef.value,
  259. onLongPress: (event, target) => {
  260. const index = (
  261. Array.from(conversationListInnerDomRef.value!.children) as HTMLElement[]
  262. ).indexOf(target!);
  263. showConversationActionMenu(event, conversationList.value[index], index);
  264. },
  265. options: {
  266. eventDelegation: {
  267. subSelector: ".tui-conversation-content",
  268. },
  269. },
  270. });
  271. }
  272. function onCurrentConversationUpdated(conversation: IConversationModel) {
  273. currentConversation.value = conversation;
  274. }
  275. function onConversationListUpdated(list: IConversationModel[]) {
  276. let newList = [];
  277. // 房产用户端会话列表的conversationID第四位只能是C或者不含"A", "B", "D", "E", "F", "G", "H"
  278. let tagArr = ["A", "B", "D", "E", "F", "G", "H"];
  279. for (let key in list) {
  280. let fourWord = list[key].conversationID.substring(3, 4);
  281. if (fourWord == "C") {
  282. newList.push(list[key]);
  283. } else {
  284. if (tagArr.indexOf(fourWord) == -1) {
  285. newList.push(list[key]);
  286. }
  287. }
  288. }
  289. conversationList.value = newList;
  290. }
  291. function onCurrentConversationIDUpdated(id: string) {
  292. currentConversationID.value = id;
  293. }
  294. function onDisplayOnlineStatusUpdated(status: boolean) {
  295. displayOnlineStatus.value = status;
  296. }
  297. function onUserStatusListUpdated(list: Map<string, IUserStatus>) {
  298. if (list.size !== 0) {
  299. userOnlineStatusMap.value = [...list.entries()].reduce(
  300. (obj, [key, value]) => {
  301. obj[key] = value;
  302. return obj;
  303. },
  304. {} as IUserStatusMap
  305. );
  306. }
  307. }
  308. // 暴露给父组件,当监听到滑动事件时关闭actionsMenu
  309. defineExpose({ closeChildren: closeConversationActionMenu });
  310. </script>
  311. <style lang="scss" scoped src="./style/index.scss"></style>
  312. <style lang="scss" scoped>
  313. page {
  314. background: #f4f4f4;
  315. }
  316. .tui-conversation-item {
  317. padding: 12px;
  318. display: flex;
  319. cursor: pointer;
  320. height: 73px;
  321. flex-direction: row;
  322. align-items: center;
  323. .content-header {
  324. .name {
  325. font-weight: 500;
  326. font-size: 34upx;
  327. color: #333333;
  328. }
  329. .middle-box-content {
  330. font-weight: 500;
  331. font-size: 30upx;
  332. color: #666666;
  333. }
  334. }
  335. .content {
  336. line-height: 35px;
  337. }
  338. .left {
  339. width: auto;
  340. height: auto;
  341. }
  342. }
  343. .disable-select {
  344. -webkit-touch-callout: none;
  345. -webkit-user-select: none;
  346. -khtml-user-select: none;
  347. -moz-user-select: none;
  348. -ms-user-select: none;
  349. user-select: none;
  350. }
  351. ::v-deep .empty-box {
  352. padding-top: 20%;
  353. background: #f4f4f4;
  354. }
  355. </style>