MessageMenu.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. <template>
  2. <view :style="popupStyle" class="message_menu_container" :class="{ message_menu_container_bottom: is_bottom }">
  3. <view class="message_menu_item" v-for="item in menuList" @click="menuClick(item)" :key="item.idx">
  4. <image :src="item.icon" class="message_menu_icon" />
  5. <text class="message_menu_text">{{ item.title }}</text>
  6. </view>
  7. </view>
  8. </template>
  9. <script>
  10. import { mapGetters, mapActions } from 'vuex';
  11. import { MessageMenuTypes, ContactChooseTypes, PageEvents } from '../../../../../constant';
  12. import IMSDK, { GroupMemberRole, MessageType, SessionType } from 'openim-uniapp-polyfill';
  13. import { parseAt } from '../../../../../util/imCommon';
  14. import copy from '../../../../../static/images/chating_message_copy.png';
  15. import forward from '../../../../../static/images/chating_message_forward.png';
  16. import reply from '../../../../../static/images/chating_message_reply.png';
  17. import revoke from '../../../../../static/images/chating_message_revoke.png';
  18. import multiple from '../../../../../static/images/chating_message_multiple.png';
  19. import del from '../../../../../static/images/chating_message_del.png';
  20. const canCopyTypes = [MessageType.TextMessage, MessageType.AtTextMessage, MessageType.QuoteMessage, MessageType.GroupAnnouncementUpdated];
  21. export default {
  22. components: {},
  23. props: {
  24. message: Object,
  25. isSender: {
  26. type: Boolean,
  27. default: false
  28. },
  29. paterWidth: {
  30. type: [Number, String],
  31. default: 0
  32. },
  33. is_bottom: {
  34. type: Boolean,
  35. default: false
  36. },
  37. rect: {
  38. type: Object,
  39. default: () => ({})
  40. }
  41. },
  42. data() {
  43. return {
  44. contentType: null
  45. };
  46. },
  47. computed: {
  48. ...mapGetters(['storeCurrentMemberInGroup', 'storeCurrentUserID',"storeCurrentConversation"]),
  49. getLeft() {
  50. // 145 + 145*0.2
  51. if (this.isSender && this.paterWidth < 174) {
  52. return 'auto';
  53. }
  54. if (!this.isSender && this.paterWidth < 174) {
  55. return '0';
  56. }
  57. return '20%';
  58. },
  59. getRight() {
  60. if (this.isSender && this.paterWidth < 174) {
  61. return '0';
  62. }
  63. return 'auto';
  64. },
  65. popupStyle() {
  66. // 如果有 rect 参数,说明是全局定位
  67. if (this.rect && this.rect.top !== undefined) {
  68. const style = {};
  69. // 假设屏幕高度大概是 750px (rpx) 或者根据实际像素
  70. // nvue 中单位默认是 px
  71. // 简单的判断:如果 top < 200,向下弹;否则向上弹
  72. const isTop = this.rect.top < 200;
  73. if (isTop) {
  74. style.top = (this.rect.top + this.rect.height + 10) + 'px';
  75. } else {
  76. // 向上弹,需要知道 menu 高度。这里无法预知,只能 bottom 定位?
  77. // 但是 bottom 是相对于屏幕底部的。
  78. // 我们可以用 top: rect.top - menuHeight
  79. // 还是 bottom 比较稳妥
  80. // style.bottom = (windowHeight - this.rect.top + 10) + 'px';
  81. // nvue 没有 windowHeight 直接可用,需要 uni.getSystemInfoSync
  82. // 为了简单,我们尽量用 top
  83. // 临时方案:始终向下弹?不行,底部消息会被遮挡。
  84. // 我们还是用 fixed bottom 吧,但是需要知道屏幕高度。
  85. const sys = uni.getSystemInfoSync();
  86. style.bottom = (sys.windowHeight - this.rect.top + 10) + 'px';
  87. }
  88. // 水平定位
  89. // 居中于消息气泡,或者左对齐
  90. // style.left = this.rect.left + 'px';
  91. // 为了不超出屏幕,可能需要 clamp
  92. let left = this.rect.left;
  93. // 简单的防止右溢出
  94. const sys = uni.getSystemInfoSync();
  95. if (left + 200 > sys.windowWidth) { // 假设菜单宽 200
  96. left = sys.windowWidth - 220;
  97. }
  98. style.left = left + 'px';
  99. return style;
  100. }
  101. // 原有的相对定位逻辑(保留以兼容 vue 页面或非全局模式)
  102. let style = {
  103. left: this.getLeft,
  104. right: this.getRight
  105. };
  106. if (this.is_bottom) {
  107. style.top = '100%';
  108. style.marginTop = '10px';
  109. } else {
  110. style.bottom = '100%';
  111. style.marginBottom = '12px';
  112. }
  113. return style;
  114. },
  115. canRevoke() {
  116. const interval = this.message.sendTime < Date.now() - 5 * 60 * 1000;
  117. if (interval) {
  118. return false;
  119. }
  120. if (this.message.sessionType !== SessionType.Single && this.storeCurrentMemberInGroup.roleLevel !== GroupMemberRole.Nomal) {
  121. return true;
  122. }
  123. return this.isSender;
  124. },
  125. isGroupAnnouncement() {
  126. return this.message.contentType === MessageType.GroupAnnouncementUpdated;
  127. },
  128. menuList() {
  129. console.log("qxj MessageMenu menuList compute start");
  130. console.log("qxj message:", this.message);
  131. console.log("qxj contentType:", this.message.contentType);
  132. console.log("qxj canCopyTypes:", canCopyTypes);
  133. // 公共基础菜单(非医生也能看到的项)
  134. const baseMenus = [
  135. {
  136. idx: 0,
  137. type: MessageMenuTypes.Copy,
  138. title: "复制",
  139. icon: copy,
  140. visible: canCopyTypes.includes(this.message.contentType),
  141. },
  142. { idx: 1,
  143. type: MessageMenuTypes.Del,
  144. title: "删除",
  145. icon: del,
  146. visible: true ,
  147. },
  148. {
  149. idx: 3,
  150. type: MessageMenuTypes.Reply,
  151. title: "回复",
  152. icon: reply,
  153. visible: !this.isGroupAnnouncement
  154. },
  155. {
  156. idx: 4,
  157. type: MessageMenuTypes.Revoke,
  158. title: "撤回",
  159. icon: revoke,
  160. visible: this.canRevoke && !this.isGroupAnnouncement
  161. }
  162. ];
  163. const menus1 = [
  164. {
  165. idx: 2,
  166. type: MessageMenuTypes.Forward,
  167. title: "转发",
  168. icon: forward,
  169. visible: !this.isGroupAnnouncement && this.message.contentType==MessageType.VoiceMessage
  170. },
  171. {
  172. idx: 5,
  173. type: MessageMenuTypes.Multiple,
  174. title: "多选",
  175. icon: multiple,
  176. visible: !this.isGroupAnnouncement && this.message.contentType==MessageType.VoiceMessage
  177. }
  178. ];
  179. let tempMenuArrs=this.isDoctorAction(this.storeCurrentConversation.userID) ? baseMenus: [...baseMenus, ...menus1];
  180. let menuList=[];
  181. tempMenuArrs.forEach((item) => {
  182. if(item.visible){
  183. menuList.push(item);
  184. }
  185. });
  186. return menuList;
  187. }
  188. },
  189. mounted() {
  190. this.contentType = this.message.contentType;
  191. },
  192. methods: {
  193. ...mapActions('message', ['deleteMessages', 'updateOneMessage', 'updateQuoteMessageRevoke']),
  194. ...mapActions('conversation', ['addRevokedMessage']),
  195. async menuClick({ type }) {
  196. switch (type) {
  197. case MessageMenuTypes.Copy:
  198. uni.setClipboardData({
  199. data: this.getCopyText(),
  200. success: () => {
  201. uni.hideToast();
  202. this.$nextTick(() => {
  203. uni.$u.toast('复制成功');
  204. });
  205. }
  206. });
  207. break;
  208. case MessageMenuTypes.Del:
  209. try {
  210. await IMSDK.asyncApi(IMSDK.IMMethods.DeleteMessage, IMSDK.uuid(), {
  211. conversationID: this.$store.getters.storeCurrentConversation.conversationID,
  212. clientMsgID: this.message.clientMsgID
  213. });
  214. this.deleteMessages([this.message]);
  215. uni.$u.toast('删除成功');
  216. } catch (error) {
  217. try {
  218. await IMSDK.asyncApi(IMSDK.IMMethods.DeleteMessageFromLocalStorage, IMSDK.uuid(), {
  219. conversationID: this.$store.getters.storeCurrentConversation.conversationID,
  220. clientMsgID: this.message.clientMsgID
  221. });
  222. this.deleteMessages([this.message]);
  223. uni.$u.toast('删除成功');
  224. } catch (error) {
  225. uni.$u.toast('删除失败');
  226. }
  227. }
  228. break;
  229. case MessageMenuTypes.Forward:
  230. this.$emit('enterSubPage');
  231. uni.navigateTo({
  232. url: `/pages_im/pages/common/contactChoose/index?type=${ContactChooseTypes.ForWard}&forwardMessage=${encodeURIComponent(JSON.stringify(this.message))}`
  233. });
  234. break;
  235. case MessageMenuTypes.Multiple:
  236. uni.$emit(PageEvents.MutipleCheckUpdate, {
  237. flag: true,
  238. message: this.message
  239. });
  240. break;
  241. case MessageMenuTypes.Reply:
  242. this.$store.commit('message/SET_QUOTE_MESSAGE', this.message);
  243. break;
  244. case MessageMenuTypes.Revoke:
  245. const _IMSDK = getIMSDK();
  246. _IMSDK.asyncApi(_IMSDK.IMMethods.RevokeMessage, _IMSDK.uuid(), {
  247. conversationID: this.$store.getters.storeCurrentConversation.conversationID,
  248. clientMsgID: this.message.clientMsgID
  249. })
  250. .then(() => {
  251. if (canCopyTypes.includes(this.contentType) && this.message.sendID === this.storeCurrentUserID) {
  252. this.addRevokedMessage(this.message);
  253. }
  254. this.updateOneMessage({
  255. message: {
  256. ...this.message,
  257. contentType: MessageType.RevokeMessage,
  258. notificationElem: {
  259. detail: JSON.stringify({
  260. clientMsgID: this.message.clientMsgID,
  261. revokeTime: Date.now(),
  262. revokerID: this.storeCurrentUserID,
  263. revokerNickname: '你',
  264. revokerRole: 0,
  265. seq: this.message.seq,
  266. sessionType: this.message.sessionType,
  267. sourceMessageSendID: this.message.sendID,
  268. sourceMessageSendTime: this.message.sendTime,
  269. sourceMessageSenderNickname: this.message.senderNickname
  270. })
  271. }
  272. }
  273. });
  274. this.updateQuoteMessageRevoke({
  275. clientMsgID: this.message.clientMsgID
  276. });
  277. })
  278. .catch(() => uni.$u.toast('撤回失败'));
  279. break;
  280. default:
  281. break;
  282. }
  283. this.$emit('close');
  284. },
  285. getMenuList(){
  286. // 公共基础菜单(非医生也能看到的项)
  287. let baseMenus = [
  288. {
  289. idx: 0,
  290. type: MessageMenuTypes.Copy,
  291. title: "复制",
  292. icon: copy,
  293. visible: canCopyTypes.includes(this.message.contentType),
  294. },
  295. { idx: 1,
  296. type: MessageMenuTypes.Del,
  297. title: "删除",
  298. icon: del,
  299. visible: true ,
  300. },
  301. {
  302. idx: 3,
  303. type: MessageMenuTypes.Reply,
  304. title: "回复",
  305. icon: reply,
  306. visible: true
  307. },
  308. {
  309. idx: 4,
  310. type: MessageMenuTypes.Revoke,
  311. title: "撤回",
  312. icon: revoke,
  313. visible: this.canRevoke && !this.isGroupAnnouncement
  314. }
  315. ];
  316. let menus1 = [
  317. {
  318. idx: 2,
  319. type: MessageMenuTypes.Forward,
  320. title: "转发",
  321. icon: forward,
  322. visible: !this.isGroupAnnouncement && this.message.contentType==MessageType.VoiceMessage
  323. },
  324. {
  325. idx: 5,
  326. type: MessageMenuTypes.Multiple,
  327. title: "多选",
  328. icon: multiple,
  329. visible: !this.isGroupAnnouncement && this.message.contentType==MessageType.VoiceMessage
  330. }
  331. ];
  332. let tempMenuArrs=isDoctorAction(this.storeCurrentConversation.userID) ? baseMenus: [...baseMenus, ...menus1];
  333. let menuList=[];
  334. for(let i=0;i<tempMenuArrs.length;i++){
  335. let item=tempMenuArrs[i];
  336. if(item.visible){
  337. menuList.push(item);
  338. }
  339. }
  340. console.log("qxj menuList:");
  341. console.log(menuList);
  342. return menuList;
  343. },
  344. getCopyText() {
  345. if (this.isGroupAnnouncement) {
  346. let detail = {};
  347. try {
  348. detail = JSON.parse(this.message.notificationElem.detail);
  349. } catch (e) {}
  350. return detail.group?.notification ?? '';
  351. }
  352. if (this.message.contentType === MessageType.AtTextMessage) {
  353. return parseAt(this.message.atTextElem, true);
  354. }
  355. if (this.message.contentType === MessageType.QuoteMessage) {
  356. return this.message.quoteElem.text;
  357. }
  358. return this.message.textElem.content;
  359. },
  360. isDoctorAction(userId){
  361. let isDoctorAct=false;
  362. if(userId!=undefined && (userId!="" || userId.length>0)){
  363. if(userId.indexOf('D')!==-1){
  364. isDoctorAct=true;
  365. }
  366. if(userId.indexOf('C')!==-1){
  367. }
  368. }
  369. return isDoctorAct;
  370. }
  371. }
  372. };
  373. </script>
  374. <style scoped lang="scss">
  375. .message_menu_container {
  376. display: flex;
  377. flex-direction: row;
  378. background-color: #5b5b5b;
  379. padding: 8rpx 0;
  380. border-radius: 16rpx;
  381. position: fixed;
  382. /* z-index: 999; */
  383. }
  384. .message_menu_container_bottom {
  385. /* top: unset; */
  386. /* bottom: 0; */
  387. }
  388. .message_menu_item {
  389. display: flex;
  390. flex-direction: column;
  391. align-items: center;
  392. justify-content: center;
  393. font-size: 12px;
  394. color: #fff;
  395. padding: 16rpx 24rpx;
  396. }
  397. .message_menu_icon {
  398. width: 17px;
  399. height: 19px;
  400. margin-bottom: 4px;
  401. }
  402. .message_menu_text {
  403. /* min-width: max-content; nvue not support */
  404. color: #fff;
  405. font-size: 12px;
  406. lines: 1;
  407. }
  408. </style>