index.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. <template>
  2. <div
  3. :class="{
  4. 'simple-message-list-container': true,
  5. 'simple-message-list-container-mobile': isMobile,
  6. }"
  7. >
  8. <div class="header-container">
  9. <span
  10. class="back"
  11. @click="backPreviousLevel"
  12. >
  13. <Icon
  14. class="close-icon"
  15. :file="addIcon"
  16. :size="'18px'"
  17. />
  18. <span v-if="isReturn">{{ TUITranslateService.t('TUIChat.返回') }}</span>
  19. <span v-else>{{ TUITranslateService.t('TUIChat.关闭') }}</span>
  20. </span>
  21. <span class="title">
  22. {{ currentMergeMessageInfo.title }}
  23. </span>
  24. </div>
  25. <div v-if="isDownloadOccurError">
  26. Load Merge Message Error
  27. </div>
  28. <div
  29. v-else-if="isMergeMessageInfoLoaded"
  30. ref="simpleMessageListRef"
  31. class="message-list"
  32. >
  33. <div
  34. v-for="item in currentMergeMessageInfo.messageList"
  35. :key="item.ID"
  36. :class="{
  37. 'message-item': true,
  38. }"
  39. >
  40. <MessageContainer
  41. :sender="item.nick"
  42. :avatar="item.avatar"
  43. :type="item.messageBody[0].type"
  44. :time="item.time"
  45. >
  46. <!-- text -->
  47. <div
  48. v-if="item.messageBody[0].type === TYPES.MSG_TEXT"
  49. class="message-text"
  50. >
  51. <span
  52. v-for="(textInfo, index) in parseTextToRenderArray(item.messageBody[0].payload['text'])"
  53. :key="index"
  54. class="message-text-container"
  55. >
  56. <span
  57. v-if="textInfo.type === 'text'"
  58. class="text"
  59. >{{ textInfo.content }}</span>
  60. <img
  61. v-else
  62. class="simple-emoji"
  63. :src="textInfo.content"
  64. alt="small-face"
  65. >
  66. </span>
  67. </div>
  68. <!-- image -->
  69. <div
  70. v-else-if="item.messageBody[0].type === TYPES.MSG_IMAGE"
  71. class="message-image"
  72. >
  73. <img
  74. class="image"
  75. :src="(item.messageBody[0].payload)['imageInfoArray'][2]['url']"
  76. mode="widthFix"
  77. alt="image"
  78. >
  79. </div>
  80. <!-- video -->
  81. <div
  82. v-else-if="item.messageBody[0].type === TYPES.MSG_VIDEO"
  83. class="message-video"
  84. >
  85. <div
  86. v-if="isUniFrameWork"
  87. @click="previewVideoInUniapp((item.messageBody[0].payload)['remoteVideoUrl'])"
  88. >
  89. <image
  90. class="image"
  91. :src="(item.messageBody[0].payload)['thumbUrl']"
  92. mode="widthFix"
  93. alt="image"
  94. />
  95. <Icon
  96. class="video-play-icon"
  97. :file="playIcon"
  98. />
  99. </div>
  100. <video
  101. v-else
  102. class="video"
  103. controls
  104. :poster="(item.messageBody[0].payload)['thumbUrl']"
  105. >
  106. <source
  107. :src="(item.messageBody[0].payload)['remoteVideoUrl']"
  108. type="video/mp4"
  109. >
  110. </video>
  111. </div>
  112. <!-- audio -->
  113. <div
  114. v-else-if="item.messageBody[0].type === TYPES.MSG_AUDIO"
  115. class="message-audio"
  116. >
  117. <span>{{ TUITranslateService.t("TUIChat.语音") }}&nbsp;</span>
  118. <span>{{ item.messageBody[0].payload.second }}s</span>
  119. </div>
  120. <!-- big face -->
  121. <div
  122. v-else-if="item.messageBody[0].type === TYPES.MSG_FACE"
  123. class="message-face"
  124. >
  125. <img
  126. class="image"
  127. :src="resolveBigFaceUrl(item.messageBody[0].payload.data)"
  128. alt="face"
  129. >
  130. </div>
  131. <!-- file -->
  132. <div
  133. v-else-if="item.messageBody[0].type === TYPES.MSG_FILE"
  134. class="message-file"
  135. >
  136. {{ TUITranslateService.t('TUIChat.[文件]') }}
  137. </div>
  138. <!-- location -->
  139. <div
  140. v-else-if="item.messageBody[0].type === TYPES.MSG_LOCATION"
  141. >
  142. {{ TUITranslateService.t('TUIChat.[地理位置]') }}
  143. </div>
  144. <!-- merger -->
  145. <div
  146. v-else-if="item.messageBody[0].type === TYPES.MSG_MERGER"
  147. class="message-merger"
  148. @click.capture="entryNextLevel($event, item)"
  149. >
  150. <MessageRecord
  151. disabled
  152. :renderData="item.messageBody[0].payload"
  153. />
  154. </div>
  155. <!-- custom -->
  156. <div v-else-if="item.messageBody[0].type === TYPES.MSG_CUSTOM">
  157. {{ TUITranslateService.t('TUIChat.[自定义消息]') }}
  158. </div>
  159. </MessageContainer>
  160. </div>
  161. </div>
  162. </div>
  163. </template>
  164. <script setup lang="ts">
  165. import { computed, ref, watch } from '../../../../../adapter-vue';
  166. import TUIChatEngine, {
  167. TUIStore,
  168. TUIChatService,
  169. TUITranslateService,
  170. } from '@tencentcloud/chat-uikit-engine';
  171. import addIcon from '../../../../../assets/icon/back.svg';
  172. import playIcon from '../../../../../assets/icon/video-play.png';
  173. import Icon from '../../../../common/Icon.vue';
  174. import MessageContainer from './message-container.vue';
  175. import MessageRecord from '../message-record/index.vue';
  176. import { parseTextToRenderArray, DEFAULT_BIG_EMOJI_URL, CUSTOM_BIG_EMOJI_URL } from '../../../emoji-config/index';
  177. import { isMobile, isUniFrameWork } from '../../../../../utils/env';
  178. import { IMergeMessageContent } from '../../../../../interface';
  179. interface IProps {
  180. /**
  181. * only use messageID when first render of simple-message-list
  182. * because the nested simple-message-list do not have corresponding message object
  183. * need to download message from sdk by constructed message
  184. * and use downloaded message object to render nested simple-message-list
  185. */
  186. messageID?: string;
  187. isMounted?: boolean;
  188. }
  189. interface IEmits {
  190. (e: 'closeOverlay'): void;
  191. }
  192. const emits = defineEmits<IEmits>();
  193. const props = withDefaults(defineProps<IProps>(), {
  194. messageID: '',
  195. isMounted: false,
  196. });
  197. const TYPES = TUIChatEngine.TYPES;
  198. const isDownloadOccurError = ref(false);
  199. const messageListStack = ref<IMergeMessageContent[]>([]);
  200. const currentMergeMessageInfo = ref<Partial<IMergeMessageContent>>({
  201. title: '',
  202. messageList: [],
  203. });
  204. const simpleMessageListRef = ref<HTMLElement>();
  205. watch(() => messageListStack.value.length, async (newValue) => {
  206. isDownloadOccurError.value = false;
  207. if (newValue < 1) {
  208. return;
  209. }
  210. const stackTopMessageInfo = messageListStack.value[messageListStack.value.length - 1];
  211. if (stackTopMessageInfo.downloadKey && stackTopMessageInfo.messageList.length === 0) {
  212. try {
  213. const res = await TUIChatService.downloadMergedMessages({
  214. payload: stackTopMessageInfo,
  215. type: TUIChatEngine.TYPES.MSG_MERGER,
  216. } as any);
  217. // if download complete message, cover the original message in stack top
  218. messageListStack.value[messageListStack.value.length - 1] = res.payload;
  219. } catch (error) {
  220. isDownloadOccurError.value = true;
  221. }
  222. }
  223. currentMergeMessageInfo.value = messageListStack.value[messageListStack.value.length - 1];
  224. });
  225. watch(() => props.isMounted, (newValue) => {
  226. // For compatibility with uniapp, use watch to implement onMounted
  227. if (newValue) {
  228. if (!props.messageID) {
  229. throw new Error('messageID is required when first render of simple-message-list.');
  230. }
  231. const sdkMessagePayload = TUIStore.getMessageModel(props.messageID).getMessage().payload;
  232. messageListStack.value = [sdkMessagePayload];
  233. } else {
  234. messageListStack.value = [];
  235. }
  236. }, {
  237. immediate: true,
  238. });
  239. const isReturn = computed(() => {
  240. return messageListStack.value.length > 1;
  241. });
  242. const isMergeMessageInfoLoaded = computed(() => {
  243. return currentMergeMessageInfo.value?.messageList ? currentMergeMessageInfo.value.messageList.length > 0 : false;
  244. });
  245. function entryNextLevel(e, sdkMessage: any) {
  246. messageListStack.value.push(sdkMessage.messageBody[0].payload);
  247. e.stopPropagation();
  248. }
  249. function backPreviousLevel() {
  250. messageListStack.value.pop();
  251. if (messageListStack.value.length < 1) {
  252. emits('closeOverlay');
  253. }
  254. }
  255. function previewVideoInUniapp(url: string) {
  256. if (isUniFrameWork) {
  257. const encodedUrl = encodeURIComponent(url);
  258. uni.navigateTo({
  259. url: `/TUIKit/components/TUIChat/video-play?videoUrl=${encodedUrl}`,
  260. });
  261. }
  262. }
  263. function resolveBigFaceUrl(bigFaceKey: string): string {
  264. let url = '';
  265. if (bigFaceKey.indexOf('@custom') > -1) {
  266. url = CUSTOM_BIG_EMOJI_URL + bigFaceKey;
  267. } else {
  268. url = DEFAULT_BIG_EMOJI_URL + bigFaceKey;
  269. if (url.indexOf('@2x') === -1) {
  270. url += '@2x.png';
  271. } else {
  272. url += '.png';
  273. }
  274. }
  275. return url;
  276. }
  277. </script>
  278. <style scoped lang="scss">
  279. :not(not){
  280. display: flex;
  281. flex-direction: column;
  282. min-width: 0;
  283. box-sizing: border-box;
  284. }
  285. .simple-message-list-container {
  286. flex: 1;
  287. position: relative;
  288. overflow: hidden;
  289. width: calc(40vw);
  290. min-width: 550px;
  291. height: calc(100vh - 200px);
  292. background-color: #fff;
  293. box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  294. border-radius: 8px;
  295. &-mobile {
  296. width: 100vw;
  297. height: 100vh;
  298. min-width: auto;
  299. border-radius: 0;
  300. }
  301. .header-container {
  302. width: 100%;
  303. text-align: center;
  304. font-weight: bold;
  305. position: absolute;
  306. top: 0;
  307. left: 0;
  308. z-index: 1;
  309. height: 60px;
  310. justify-content: center;
  311. align-items: center;
  312. padding: 0 70px;
  313. background-color: #fff;
  314. .back {
  315. flex-direction: row;
  316. align-items: center;
  317. position: absolute;
  318. left: 10px;
  319. cursor: pointer;
  320. }
  321. .title {
  322. width: 100%;
  323. display: block;
  324. overflow: hidden;
  325. text-overflow: ellipsis;
  326. white-space: nowrap;
  327. }
  328. }
  329. .message-list {
  330. padding: 60px 20px 20px;
  331. flex: 1 1 auto;
  332. overflow: hidden auto;
  333. }
  334. }
  335. .message-item {
  336. flex-direction: row;
  337. margin: 10px 0;
  338. }
  339. .message-text {
  340. flex-flow: row wrap;
  341. display: inline;
  342. &-container {
  343. display: inline;
  344. flex: 0 0 auto;
  345. flex-direction: row;
  346. .text {
  347. vertical-align: bottom;
  348. display: inline;
  349. word-break: break-all;
  350. }
  351. .simple-emoji {
  352. display: inline-flex;
  353. width: 20px;
  354. height: 20px;
  355. }
  356. }
  357. }
  358. .message-image {
  359. max-width: 180px;
  360. border-radius: 10px;
  361. overflow: hidden;
  362. .image {
  363. max-width: 180px;
  364. }
  365. }
  366. .message-face {
  367. max-width: 100px;
  368. .image {
  369. width: 80px;
  370. height: 80px;
  371. }
  372. }
  373. .message-audio {
  374. flex-direction: row;
  375. }
  376. .message-video {
  377. position: relative;
  378. .image {
  379. max-width: 180px;
  380. }
  381. .video-play-icon {
  382. position: absolute;
  383. top: 50%;
  384. left: 50%;
  385. transform: translate(-50%, -50%);
  386. }
  387. .video {
  388. max-width: 150px;
  389. width: inherit;
  390. height: inherit;
  391. border-radius: 10px;
  392. }
  393. }
  394. .message-combine {
  395. max-width: 300px;
  396. }
  397. </style>