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="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("conversationList", conversationList);
  135. }, 2000);
  136. const conversationListDomRef = ref<HTMLElement | undefined>();
  137. const conversationListInnerDomRef = ref<HTMLElement | undefined>();
  138. const actionsMenuPosition = ref<{
  139. top: number;
  140. left: number | undefined;
  141. conversationHeight: number | undefined;
  142. }>({
  143. top: 0,
  144. left: undefined,
  145. conversationHeight: undefined,
  146. });
  147. const displayOnlineStatus = ref(false); // 在线状态 默认关闭
  148. const userOnlineStatusMap = ref<IUserStatusMap>();
  149. let lastestOpenActionsMenuTime: number | null = null;
  150. onMounted(() => {
  151. TUIStore.watch(StoreName.CONV, {
  152. currentConversationID: onCurrentConversationIDUpdated,
  153. conversationList: onConversationListUpdated,
  154. currentConversation: onCurrentConversationUpdated,
  155. });
  156. // 初始状态
  157. TUIStore.watch(StoreName.USER, {
  158. displayOnlineStatus: onDisplayOnlineStatusUpdated,
  159. userStatusList: onUserStatusListUpdated,
  160. });
  161. if (!isUniFrameWork && isIOS && !isPC) {
  162. addLongPressHandler();
  163. }
  164. });
  165. onUnmounted(() => {
  166. TUIStore.unwatch(StoreName.CONV, {
  167. currentConversationID: onCurrentConversationIDUpdated,
  168. conversationList: onConversationListUpdated,
  169. currentConversation: onCurrentConversationUpdated,
  170. });
  171. // 初始状态
  172. TUIStore.unwatch(StoreName.USER, {
  173. displayOnlineStatus: onDisplayOnlineStatusUpdated,
  174. userStatusList: onUserStatusListUpdated,
  175. });
  176. });
  177. const isShowUserOnlineStatus = (conversation: IConversationModel): boolean => {
  178. return (
  179. displayOnlineStatus.value &&
  180. conversation.type === TUIChatEngine.TYPES.CONV_C2C
  181. );
  182. };
  183. const showConversationActionMenu = (
  184. event: Event,
  185. conversation: IConversationModel,
  186. index: number,
  187. isContextMenuEvent?: boolean
  188. ) => {
  189. if (isContextMenuEvent) {
  190. event.preventDefault();
  191. if (isUniFrameWork) {
  192. return;
  193. }
  194. }
  195. currentConversation.value = conversation;
  196. lastestOpenActionsMenuTime = Date.now();
  197. getActionsMenuPosition(event, index);
  198. };
  199. const closeConversationActionMenu = () => {
  200. // 防止连续触发overlay的tap事件
  201. if (
  202. lastestOpenActionsMenuTime &&
  203. Date.now() - lastestOpenActionsMenuTime > 300
  204. ) {
  205. currentConversation.value = undefined;
  206. isShowOverlay.value = false;
  207. }
  208. };
  209. const getActionsMenuPosition = (event: Event, index: number) => {
  210. if (isUniFrameWork) {
  211. if (typeof conversationListDomRef.value === "undefined") {
  212. emits("getPassingRef", conversationListDomRef);
  213. }
  214. const query = TUIGlobal?.createSelectorQuery().in(
  215. conversationListDomRef.value
  216. );
  217. query
  218. .select(`#convlistitem-${index}`)
  219. .boundingClientRect((data) => {
  220. if (data) {
  221. actionsMenuPosition.value = {
  222. // uni-h5的uni-page-head不被认为是视窗中的成员,因此手动上head的高度
  223. top: data.bottom + (isH5 ? 44 : 0),
  224. // @ts-expect-error in uniapp event has touches property
  225. left: event.touches[0].pageX,
  226. conversationHeight: data.height,
  227. };
  228. isShowOverlay.value = true;
  229. }
  230. })
  231. .exec();
  232. } else {
  233. // 处理Vue原生
  234. const rect =
  235. (
  236. (event.currentTarget || event.target) as HTMLElement
  237. )?.getBoundingClientRect() || {};
  238. if (rect) {
  239. actionsMenuPosition.value = {
  240. top: rect.bottom,
  241. left: isPC ? (event as MouseEvent).clientX : undefined,
  242. conversationHeight: rect.height,
  243. };
  244. }
  245. isShowOverlay.value = true;
  246. }
  247. };
  248. const enterConversationChat = (conversationID: string, conversation: any) => {
  249. emits("handleSwitchConversation", conversationID);
  250. emits("handleSwitchConversationObj", conversation);
  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. console.log(list);
  277. let newList = [];
  278. // 家政用户端会话列表的conversationID第四位只能是E
  279. for (let key in list) {
  280. let fourWord = list[key].conversationID.substring(3, 4);
  281. //判断是否为用户发来的消息,
  282. let userFOurWord = list[key].conversationID.substring(0, 3);
  283. let loginType = uni.getStorageSync("loginType");
  284. if (loginType && loginType == 1) {
  285. // 判断是否为用户消息userFOurWord == "C2C"不添加该判断 则 看不到用户发来的消息
  286. if (fourWord == "E" || userFOurWord == "C2C") {
  287. newList.push(list[key]);
  288. }
  289. } else {
  290. if (fourWord == "E") {
  291. newList.push(list[key]);
  292. }
  293. }
  294. }
  295. conversationList.value = newList;
  296. }
  297. function onCurrentConversationIDUpdated(id: string) {
  298. currentConversationID.value = id;
  299. }
  300. function onDisplayOnlineStatusUpdated(status: boolean) {
  301. displayOnlineStatus.value = status;
  302. }
  303. function onUserStatusListUpdated(list: Map<string, IUserStatus>) {
  304. if (list.size !== 0) {
  305. userOnlineStatusMap.value = [...list.entries()].reduce(
  306. (obj, [key, value]) => {
  307. obj[key] = value;
  308. return obj;
  309. },
  310. {} as IUserStatusMap
  311. );
  312. }
  313. }
  314. // 暴露给父组件,当监听到滑动事件时关闭actionsMenu
  315. defineExpose({ closeChildren: closeConversationActionMenu });
  316. </script>
  317. <style lang="scss" scoped src="./style/index.scss"></style>
  318. <style lang="scss" scoped>
  319. .tui-conversation-item {
  320. padding: 12px;
  321. display: flex;
  322. cursor: pointer;
  323. height: 73px;
  324. flex-direction: row;
  325. align-items: center;
  326. .content-header {
  327. .name {
  328. font-weight: 500;
  329. font-size: 34upx;
  330. color: #333333;
  331. }
  332. .middle-box-content {
  333. font-weight: 500;
  334. font-size: 30upx;
  335. color: #666666;
  336. }
  337. }
  338. .content {
  339. line-height: 35px;
  340. }
  341. .left {
  342. width: auto;
  343. height: auto;
  344. }
  345. }
  346. .disable-select {
  347. -webkit-touch-callout: none;
  348. -webkit-user-select: none;
  349. -khtml-user-select: none;
  350. -moz-user-select: none;
  351. -ms-user-select: none;
  352. user-select: none;
  353. }
  354. ::v-deep .empty-box {
  355. padding-top: 20%;
  356. // background: #f4f4f4;
  357. }
  358. </style>