message.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  1. <template>
  2. <view class="chat-message">
  3. <view class="chat-message-item" v-for="(item, index) in flist" :key="`${item.from}-${index}`" :class="[
  4. `${item._isMy ? 'is-right' : 'is-left'}`,
  5. `is-${item._mode}`,
  6. {
  7. 'is-animation': item.animation,
  8. },
  9. ]">
  10. <!-- 发言时间 -->
  11. <view class="date" v-if="item._date">
  12. <text>{{ item._date }}</text>
  13. </view>
  14. <!-- 内容 -->
  15. <view class="main">
  16. <!-- 头像 -->
  17. <view class="avatar" @tap="toDetail(item.from)">
  18. <!-- <view class="avatorImg2" v-if="item._isMy">{{item.avatar}}</view>
  19. <view class="avatorImg" v-else>{{item.avatar}}</view> -->
  20. <image :src="item.avatar" mode="aspectFill" />
  21. </view>
  22. <!-- 详细 -->
  23. <view class="det">
  24. <template v-if="conversationType=='GROUP' && !item._isMy">
  25. <view class="name-sex">
  26. <view class="name">{{item.nick}}</view>
  27. <view class="man" v-if="item.sex == 0">
  28. <image src="@/static/icon/brief-icon-1.png" mode="aspectFill" />
  29. </view>
  30. <view class="woman" v-else-if="item.sex == 1">
  31. <image src="@/static/icon/brief-icon-2.png" mode="aspectFill" />
  32. </view>
  33. </view>
  34. </template>
  35. <!-- 内容 -->
  36. <view class="content" @tap="tapCont(item)" @longpress="longPressItem(item)">
  37. <!-- 文本 -->
  38. <template v-if="item._mode === 'text'">
  39. {{ item.payload.text }}
  40. </template>
  41. <!-- 图片 -->
  42. <template v-else-if="item._mode === 'image'">
  43. <cl-loading-mask color="#fff" :loading="item.loading" :text="`${item.progress}%`">
  44. <image mode="widthFix" :src="item.payload.url"></image>
  45. </cl-loading-mask>
  46. </template>
  47. <!-- 表情 -->
  48. <template v-else-if="item._mode === 'emoji'">
  49. <image :src="item.content.imageUrl"></image>
  50. </template>
  51. <!-- 语音 -->
  52. <template v-else-if="item._mode === 'voice'">
  53. <icon-voice v-if="item._isMy" name="icon-voice-right" :play="item.isPlay"></icon-voice>
  54. <!-- <text class="duration">{{ item.payload.second }}</text> -->
  55. <text style="color:#007AFF;">语音消息,点击收听</text>
  56. <icon-voice v-if="!item._isMy" name="icon-voice-left" :play="item.isPlay"></icon-voice>
  57. </template>
  58. <!-- 视频 -->
  59. <template v-else-if="item._mode === 'video'">
  60. <cl-loading-mask color="#000" :loading="item.loading" :text="`${item.progress}%`">
  61. <view class="item">
  62. <image class="cover" :src="item.payload.thumbUrl" mode="aspectFill" />
  63. <text v-if="!item.loading" class="chat-iconfont icon-play"></text>
  64. </view>
  65. </cl-loading-mask>
  66. </template>
  67. </view>
  68. </view>
  69. </view>
  70. </view>
  71. </view>
  72. </template>
  73. <script>
  74. import dayjs from "../../../uni_modules/cl-uni/utils/dayjs";
  75. import IconVoice from "./icon-voice";
  76. // 音频
  77. const innerAudioContext = uni.createInnerAudioContext();
  78. // 模式,对应 contentType
  79. const modes = ["text", "image", "emoji", "voice", "video"];
  80. export default {
  81. components: {
  82. IconVoice,
  83. },
  84. props: {
  85. /*
  86. * text: 文本
  87. * duration: 时长
  88. * videoUrl: 视频地址
  89. * voiceUrl: 语音地址
  90. * videoCoverUrl: 视频封面
  91. * imageUrl: 图片地址
  92. */
  93. list: Array,
  94. conversationType: String,
  95. },
  96. data() {
  97. return {
  98. voice: {
  99. timer: null,
  100. },
  101. // 登录的用户信息
  102. userInfo: {
  103. userId: 1,
  104. name: 'ai',
  105. avatarUrl: ''
  106. },
  107. };
  108. },
  109. computed: {
  110. flist() {
  111. let date = "";
  112. const {
  113. avatarUrl,
  114. name,
  115. userId
  116. } = this.userInfo;
  117. let temp = this.list.map((e) => {
  118. // 处理日期
  119. e._date = date ?
  120. dayjs(e.create_time).isBefore(dayjs(date).add(5, "minute")) ?
  121. "" :
  122. e.create_time :
  123. e.create_time;
  124. date = e.create_time;
  125. console.log("ismy:", e.from + "||" + getApp().globalData.userId);
  126. // 是否是自己
  127. e._isMy = e.from == getApp().globalData.userId;
  128. if (e._isMy) {
  129. e.avatarUrl = e.avatar;
  130. e.nick = name;
  131. e.userId = userId;
  132. } else {
  133. e.avatarUrl = e.avatar;
  134. }
  135. // 消息模型
  136. e._mode = e.mode;
  137. // switch (e.type) {
  138. // case "TIMTextElem":
  139. // e._mode = "text";
  140. // break;
  141. // case "TIMImageElem":
  142. // e._mode = "image";
  143. // break;
  144. // case "TIMVideoFileElem":
  145. // e._mode = "video";
  146. // break;
  147. // case "TIMSoundElem":
  148. // e._mode = "voice";
  149. // break;
  150. // default:
  151. // e._mode = "error";
  152. // break;
  153. // }
  154. if (e._mode == "error") {
  155. return null;
  156. }
  157. if (e.isRevoked) {
  158. return null;
  159. }
  160. return e;
  161. });
  162. let result = []
  163. for (let i = 0; i < temp.length; i++) {
  164. if (temp[i] != null) {
  165. result.push(temp[i])
  166. }
  167. }
  168. return result;
  169. },
  170. },
  171. filters: {
  172. duration(val) {
  173. return Math.ceil((val || 1) / 1000);
  174. },
  175. },
  176. destroyed() {
  177. this.voicePause();
  178. },
  179. methods: {
  180. // 点击内容
  181. tapCont(item) {
  182. switch (item._mode) {
  183. case "image":
  184. this.previewImage(item);
  185. break;
  186. case "video":
  187. this.videoPlay(item);
  188. break;
  189. case "voice":
  190. this.voicePlay(item);
  191. break;
  192. }
  193. },
  194. // 前往详情
  195. toDetail(uid) {
  196. if (uid == this.userInfo.userId) {
  197. return;
  198. }
  199. uni.navigateTo({
  200. url: "/pages/detail/index?uid=" + uid,
  201. });
  202. },
  203. // 长按
  204. longPressItem(item) {
  205. // console.log('create_time:', item.create_time);
  206. let actList = [];
  207. switch (item._mode) {
  208. case "text":
  209. actList.push('复制');
  210. break;
  211. }
  212. const result = this.isWithinLast3Minutes(item.create_time);
  213. if (result) {
  214. // actList.push('撤回');
  215. }
  216. let that = this;
  217. if (actList.length > 0) {
  218. uni.showActionSheet({
  219. title: '',
  220. itemList: actList,
  221. success: function(res) {
  222. if (res.tapIndex == 0) {
  223. uni.setClipboardData({
  224. data: item.payload.text,
  225. });
  226. } else {
  227. }
  228. },
  229. fail: function(res) {
  230. console.log(res.errMsg);
  231. }
  232. });
  233. }
  234. },
  235. // 图片预览
  236. previewImage(item) {
  237. uni.previewImage({
  238. current: item.payload.url,
  239. urls: this.list.filter((e) => e._mode == "image").map((e) => e.payload.url),
  240. });
  241. },
  242. // 视频播放
  243. videoPlay(item) {
  244. this.$root.video.url = item.payload.videoUrl;
  245. this.$root.video.visible = true;
  246. },
  247. // 播放录音
  248. voicePlay(item) {
  249. // 设置播放状态
  250. this.list.map((e) => {
  251. this.$set(e, "isPlay", e.ID == item.ID ? e.isPlay : false);
  252. });
  253. item.isPlay = !item.isPlay;
  254. // console.log(item.payload.url);
  255. if (item.isPlay) {
  256. // 开始播放
  257. innerAudioContext.src = item.payload.url;
  258. innerAudioContext.play();
  259. // console.log("now play");
  260. } else {
  261. // 暂停播放
  262. this.voicePause();
  263. }
  264. // 清除计时器
  265. clearTimeout(this.voice.timer);
  266. // x 秒后暂停
  267. this.voice.timer = setTimeout(() => {
  268. item.isPlay = false;
  269. }, item.payload.second * 1000 || 1000);
  270. },
  271. // 暂停播放
  272. voicePause() {
  273. innerAudioContext.stop();
  274. this.list.map((e) => {
  275. e.isPlay = false;
  276. });
  277. },
  278. isWithinLast3Minutes(timeString) {
  279. // 将时间字符串转换为 Date 对象
  280. const inputTime = new Date(timeString);
  281. // 获取当前时间的 Date 对象
  282. const currentTime = new Date();
  283. // 获取时间差(以毫秒为单位)
  284. const timeDifference = currentTime - inputTime;
  285. // 将3分钟转换为毫秒
  286. const threeMinutesInMilliseconds = 3 * 60 * 1000;
  287. // 判断时间差是否在3分钟内(过去的3分钟)
  288. return timeDifference >= 0 && timeDifference <= threeMinutesInMilliseconds;
  289. }
  290. },
  291. };
  292. </script>
  293. <style lang="scss" scoped>
  294. @keyframes fadeInRight {
  295. from {
  296. opacity: 0;
  297. transform: translate3d(100%, 0, 0);
  298. }
  299. to {
  300. opacity: 1;
  301. transform: translate3d(0, 0, 0);
  302. }
  303. }
  304. @keyframes fadeInLeft {
  305. from {
  306. opacity: 0;
  307. transform: translate3d(-100%, 0, 0);
  308. }
  309. to {
  310. opacity: 1;
  311. transform: translate3d(0, 0, 0);
  312. }
  313. }
  314. .chat-message {
  315. &-item {
  316. font-size: 26rpx;
  317. padding: 20rpx;
  318. .date {
  319. text-align: center;
  320. margin: 10rpx 0 40rpx 0;
  321. text {
  322. font-size: 24rpx;
  323. color: #fff;
  324. padding: 4rpx 10rpx;
  325. letter-spacing: 1rpx;
  326. }
  327. }
  328. .main {
  329. display: flex;
  330. .avatar {
  331. flex-shrink: 0;
  332. height: 84rpx;
  333. image {
  334. height: 84rpx;
  335. width: 84rpx;
  336. border-radius: 42rpx;
  337. }
  338. .avatorImg {
  339. width: 90rpx;
  340. height: 90rpx;
  341. background-color: #0492fe;
  342. border-radius: 50%;
  343. display: flex;
  344. justify-content: center;
  345. align-items: center;
  346. color: #fff;
  347. font-weight: bold;
  348. font-size: 40rpx;
  349. }
  350. .avatorImg2 {
  351. width: 90rpx;
  352. height: 90rpx;
  353. background-color: #d4237a;
  354. border-radius: 50%;
  355. display: flex;
  356. justify-content: center;
  357. align-items: center;
  358. color: #fff;
  359. font-weight: bold;
  360. font-size: 40rpx;
  361. }
  362. }
  363. .det {
  364. display: flex;
  365. flex-direction: column;
  366. max-width: 60%;
  367. .content {
  368. display: inline-block;
  369. padding: 24rpx 36rpx;
  370. border-radius: 16rpx;
  371. box-sizing: border-box;
  372. position: relative;
  373. font-size: 28rpx !important;
  374. }
  375. }
  376. }
  377. &.is-left {
  378. .main {
  379. .det {
  380. margin-left: 20rpx;
  381. align-items: flex-start;
  382. .content {
  383. background: #282828;
  384. border-radius: 0rpx 36rpx 36rpx 36rpx;
  385. font-size: 35rpx;
  386. font-weight: 400;
  387. color: #fff;
  388. }
  389. }
  390. }
  391. }
  392. &.is-right {
  393. .main {
  394. flex-direction: row-reverse;
  395. .det {
  396. margin-right: 20rpx;
  397. align-items: flex-end;
  398. .content {
  399. background: #2B313B;
  400. border-radius: 36rpx 36rpx 0rpx 36rpx;
  401. font-size: 35rpx;
  402. font-weight: 400;
  403. color: #fff;
  404. }
  405. }
  406. }
  407. &.is-voice {
  408. .content {
  409. justify-content: flex-end;
  410. }
  411. }
  412. }
  413. &.is-animation {
  414. &.is-left {
  415. animation: fadeInLeft 0.5s ease both;
  416. }
  417. &.is-right {
  418. animation: fadeInRight 0.5s ease both;
  419. }
  420. }
  421. &.is-text {
  422. .content {
  423. max-width: 100%;
  424. min-width: 80rpx;
  425. word-wrap: break-word;
  426. }
  427. }
  428. &.is-text,
  429. &.is-voice {
  430. .content {
  431. padding: 20rpx;
  432. line-height: 40rpx;
  433. letter-spacing: 1rpx;
  434. }
  435. }
  436. &.is-emoji {
  437. .content {
  438. padding: 20rpx;
  439. image {
  440. height: 40rpx;
  441. width: 40rpx;
  442. }
  443. }
  444. }
  445. &.is-voice {
  446. .det {
  447. .content {
  448. display: flex;
  449. align-items: center;
  450. // width: 130rpx;
  451. .duration {
  452. &::after {
  453. content: '"';
  454. }
  455. }
  456. }
  457. }
  458. }
  459. &.is-video {
  460. .item {
  461. height: 300rpx;
  462. width: 500rpx;
  463. text-align: center;
  464. line-height: 300rpx;
  465. position: relative;
  466. overflow: hidden;
  467. .cover {
  468. height: 100%;
  469. width: 100%;
  470. position: absolute;
  471. left: 0;
  472. top: 0;
  473. border-radius: 10rpx;
  474. }
  475. .chat-iconfont {
  476. color: gray;
  477. font-size: 100rpx;
  478. position: relative;
  479. }
  480. }
  481. }
  482. &.is-image {
  483. .main {
  484. .det {
  485. .content {
  486. background-color: #2B313B;
  487. image {
  488. display: block;
  489. max-width: 300rpx;
  490. border-radius: 16rpx;
  491. }
  492. }
  493. }
  494. }
  495. }
  496. }
  497. }
  498. .name-sex {
  499. display: flex;
  500. align-items: center;
  501. }
  502. .man {
  503. image {
  504. width: 17rpx;
  505. height: 17rpx;
  506. }
  507. background-color: #58bcff;
  508. border-radius: 50%;
  509. width: 25rpx;
  510. height: 25rpx;
  511. justify-content: center;
  512. align-items: center;
  513. display: flex;
  514. margin-left: 10rpx;
  515. }
  516. .woman {
  517. image {
  518. width: 17rpx;
  519. height: 17rpx;
  520. }
  521. background-color: #ff5a70;
  522. border-radius: 50%;
  523. width: 25rpx;
  524. height: 25rpx;
  525. justify-content: center;
  526. align-items: center;
  527. display: flex;
  528. margin-left: 10rpx;
  529. }
  530. .name {
  531. margin-bottom: 10rpx;
  532. font-size: 24rpx;
  533. font-weight: 400;
  534. color: #bbccd8;
  535. }
  536. </style>