index.vue 11 KB

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