index.vue 10 KB

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