index.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. <template>
  2. <div
  3. v-if="!isAllActionItemInvalid && !messageItem.hasRiskContent"
  4. ref="messageToolDom"
  5. :class="['dialog-item', !isPC ? 'dialog-item-h5' : 'dialog-item-web']"
  6. >
  7. <slot name="TUIEmojiPlugin" />
  8. <div
  9. class="dialog-item-list"
  10. :class="!isPC ? 'dialog-item-list-h5' : 'dialog-item-list-web'"
  11. >
  12. <template v-for="(item, index) in actionItems">
  13. <div
  14. v-if="item.renderCondition()"
  15. :key="item.key"
  16. class="list-item"
  17. @click="getFunction(index)"
  18. >
  19. <Icon
  20. :file="item.iconUrl"
  21. width="15px"
  22. height="15px"
  23. />
  24. <span class="list-item-text">{{ item.text }}</span>
  25. </div>
  26. </template>
  27. </div>
  28. </div>
  29. </template>
  30. <script lang="ts" setup>
  31. import TUIChatEngine, {
  32. TUIStore,
  33. StoreName,
  34. TUITranslateService,
  35. IMessageModel,
  36. } from '@tencentcloud/chat-uikit-engine';
  37. import { TUIGlobal } from '@tencentcloud/universal-api';
  38. import { ref, watchEffect, computed, onMounted, onUnmounted } from '../../../../adapter-vue';
  39. import Icon from '../../../common/Icon.vue';
  40. import { Toast, TOAST_TYPE } from '../../../common/Toast/index';
  41. import delIcon from '../../../../assets/icon/msg-del.svg';
  42. import copyIcon from '../../../../assets/icon/msg-copy.svg';
  43. import quoteIcon from '../../../../assets/icon/msg-quote.svg';
  44. import revokeIcon from '../../../../assets/icon/msg-revoke.svg';
  45. import forwardIcon from '../../../../assets/icon/msg-forward.svg';
  46. import translateIcon from '../../../../assets/icon/translate.svg';
  47. import convertText from '../../../../assets/icon/convertText_zh.svg';
  48. import { enableSampleTaskStatus } from '../../../../utils/enableSampleTaskStatus';
  49. import { copyText } from '../../utils/utils';
  50. import { decodeTextMessage } from '../../utils/emojiList';
  51. import { isPC, isUniFrameWork } from '../../../../utils/env';
  52. import { ITranslateInfo, IConvertInfo } from '../../../../interface';
  53. interface IProps {
  54. messageItem: IMessageModel;
  55. }
  56. const props = withDefaults(defineProps<IProps>(), {
  57. messageItem: () => ({}) as IMessageModel,
  58. });
  59. const TYPES = TUIChatEngine.TYPES;
  60. const actionItems = ref([
  61. {
  62. key: 'open',
  63. text: TUITranslateService.t('TUIChat.打开'),
  64. iconUrl: copyIcon,
  65. renderCondition() {
  66. if (!message.value) return false;
  67. return isPC && (message.value?.type === TYPES.MSG_FILE
  68. || message.value.type === TYPES.MSG_VIDEO
  69. || message.value.type === TYPES.MSG_IMAGE);
  70. },
  71. clickEvent: openMessage,
  72. },
  73. {
  74. key: 'copy',
  75. text: TUITranslateService.t('TUIChat.复制'),
  76. iconUrl: copyIcon,
  77. renderCondition() {
  78. if (!message.value) return false;
  79. return message.value.type === TYPES.MSG_TEXT;
  80. },
  81. clickEvent: copyMessage,
  82. },
  83. {
  84. key: 'revoke',
  85. text: TUITranslateService.t('TUIChat.撤回'),
  86. iconUrl: revokeIcon,
  87. renderCondition() {
  88. if (!message.value) return false;
  89. return message.value.flow === 'out' && message.value.status === 'success';
  90. },
  91. clickEvent: revokeMessage,
  92. },
  93. {
  94. key: 'delete',
  95. text: TUITranslateService.t('TUIChat.删除'),
  96. iconUrl: delIcon,
  97. renderCondition() {
  98. if (!message.value) return false;
  99. return message.value.status === 'success';
  100. },
  101. clickEvent: deleteMessage,
  102. },
  103. {
  104. key: 'forward',
  105. text: TUITranslateService.t('TUIChat.转发'),
  106. iconUrl: forwardIcon,
  107. renderCondition() {
  108. if (!message.value) return false;
  109. return message.value.status === 'success';
  110. },
  111. clickEvent: forwardSingleMessage,
  112. },
  113. {
  114. key: 'quote',
  115. text: TUITranslateService.t('TUIChat.引用'),
  116. iconUrl: quoteIcon,
  117. renderCondition() {
  118. if (!message.value) return false;
  119. const _message = TUIStore.getMessageModel(message.value.ID);
  120. return message.value.status === 'success' && !_message.getSignalingInfo();
  121. },
  122. clickEvent: quoteMessage,
  123. },
  124. {
  125. key: 'translate',
  126. text: TUITranslateService.t('TUIChat.翻译'),
  127. visible: false,
  128. iconUrl: translateIcon,
  129. renderCondition() {
  130. if (!message.value) return false;
  131. return message.value.status === 'success' && message.value.type === TYPES.MSG_TEXT;
  132. },
  133. clickEvent: translateMessage,
  134. },
  135. {
  136. key: 'convert',
  137. text: TUITranslateService.t('TUIChat.转文字'),
  138. visible: false,
  139. iconUrl: convertText,
  140. renderCondition() {
  141. if (!message.value) return false;
  142. return message.value.status === 'success' && message.value.type === TYPES.MSG_AUDIO;
  143. },
  144. clickEvent: convertVoiceToText,
  145. },
  146. ]);
  147. const message = ref<IMessageModel>();
  148. const messageToolDom = ref<HTMLElement>();
  149. onMounted(() => {
  150. TUIStore.watch(StoreName.CHAT, {
  151. translateTextInfo: onMessageTranslationInfoUpdated,
  152. voiceToTextInfo: onMessageConvertInfoUpdated,
  153. });
  154. });
  155. onUnmounted(() => {
  156. TUIStore.unwatch(StoreName.CHAT, {
  157. translateTextInfo: onMessageTranslationInfoUpdated,
  158. voiceToTextInfo: onMessageConvertInfoUpdated,
  159. });
  160. });
  161. watchEffect(() => {
  162. message.value = TUIStore.getMessageModel(props.messageItem.ID);
  163. });
  164. const isAllActionItemInvalid = computed(() => {
  165. for (let i = 0; i < actionItems.value.length; ++i) {
  166. if (actionItems.value[i].renderCondition()) {
  167. return false;
  168. }
  169. }
  170. return true;
  171. });
  172. function getFunction(index: number) {
  173. // 兼容 vue2 小程序的写法 不允许动态绑定
  174. actionItems.value[index].clickEvent();
  175. }
  176. function openMessage() {
  177. let url = '';
  178. switch (message.value?.type) {
  179. case TUIChatEngine.TYPES.MSG_FILE:
  180. url = message.value.payload.fileUrl;
  181. break;
  182. case TUIChatEngine.TYPES.MSG_VIDEO:
  183. url = message.value.payload.remoteVideoUrl;
  184. break;
  185. case TUIChatEngine.TYPES.MSG_IMAGE:
  186. url = message.value.payload.imageInfoArray[0].url;
  187. break;
  188. }
  189. window?.open(url, '_blank');
  190. }
  191. function revokeMessage() {
  192. if (!message.value) return;
  193. // 获取 messageModel
  194. const messageModel = TUIStore.getMessageModel(message.value.ID);
  195. messageModel
  196. .revokeMessage()
  197. .then(() => {
  198. enableSampleTaskStatus('revokeMessage');
  199. })
  200. .catch((error: any) => {
  201. // 调用异常时业务侧可以通过 promise.catch 捕获异常进行错误处理
  202. if (error.code === 20016) {
  203. const message = TUITranslateService.t('TUIChat.已过撤回时限');
  204. Toast({
  205. message,
  206. type: TOAST_TYPE.ERROR,
  207. });
  208. }
  209. });
  210. }
  211. function deleteMessage() {
  212. if (!message.value) return;
  213. // 获取 messageModel
  214. const messageModel = TUIStore.getMessageModel(message.value.ID);
  215. messageModel.deleteMessage();
  216. }
  217. async function copyMessage() {
  218. const text = decodeTextMessage(message.value?.payload?.text);
  219. if (isUniFrameWork) {
  220. TUIGlobal?.setClipboardData({
  221. data: text,
  222. });
  223. } else {
  224. copyText(text);
  225. }
  226. }
  227. function forwardSingleMessage() {
  228. if (!message.value) return;
  229. TUIStore.update(StoreName.CUSTOM, 'singleForwardMessageID', message.value.ID);
  230. }
  231. function quoteMessage() {
  232. if (!message.value) return;
  233. message.value.quoteMessage();
  234. }
  235. function translateMessage() {
  236. const enable = TUIStore.getData(StoreName.APP, 'enabledTranslationPlugin');
  237. if (!enable) {
  238. Toast({
  239. message: TUITranslateService.t('TUIChat.请开通翻译功能'),
  240. type: TOAST_TYPE.WARNING,
  241. });
  242. return;
  243. }
  244. if (!message.value) return;
  245. const index = actionItems.value.findIndex(item => item.key === 'translate');
  246. TUIStore.update(StoreName.CHAT, 'translateTextInfo', {
  247. conversationID: message.value.conversationID,
  248. messageID: message.value.ID,
  249. visible: !actionItems.value[index].visible,
  250. });
  251. }
  252. function convertVoiceToText() {
  253. const enable = TUIStore.getData(StoreName.APP, 'enabledVoiceToText');
  254. if (!enable) {
  255. Toast({
  256. message: TUITranslateService.t('TUIChat.请开通语音转文字功能'),
  257. });
  258. return;
  259. }
  260. if (!message.value) return;
  261. const index = actionItems.value.findIndex(item => item.key === 'convert');
  262. TUIStore.update(StoreName.CHAT, 'voiceToTextInfo', {
  263. conversationID: message.value.conversationID,
  264. messageID: message.value.ID,
  265. visible: !actionItems.value[index].visible,
  266. });
  267. }
  268. function onMessageTranslationInfoUpdated(info: Map<string, ITranslateInfo[]>) {
  269. if (info === undefined) return;
  270. const translationInfoList = info.get(props.messageItem.conversationID) || [];
  271. const idx = actionItems.value.findIndex(item => item.key === 'translate');
  272. for (let i = 0; i < translationInfoList.length; ++i) {
  273. const { messageID, visible } = translationInfoList[i];
  274. if (messageID === props.messageItem.ID) {
  275. actionItems.value[idx].text = TUITranslateService.t(visible ? 'TUIChat.隐藏' : 'TUIChat.翻译');
  276. actionItems.value[idx].visible = !!visible;
  277. return;
  278. }
  279. }
  280. actionItems.value[idx].text = TUITranslateService.t('TUIChat.翻译');
  281. }
  282. function onMessageConvertInfoUpdated(info: Map<string, IConvertInfo[]>) {
  283. if (info === undefined) return;
  284. const convertInfoList = info.get(props.messageItem.conversationID) || [];
  285. const idx = actionItems.value.findIndex(item => item.key === 'convert');
  286. for (let i = 0; i < convertInfoList.length; ++i) {
  287. const { messageID, visible } = convertInfoList[i];
  288. if (messageID === props.messageItem.ID) {
  289. actionItems.value[idx].text = TUITranslateService.t(visible ? 'TUIChat.隐藏' : 'TUIChat.转文字');
  290. actionItems.value[idx].visible = !!visible;
  291. return;
  292. }
  293. }
  294. actionItems.value[idx].text = TUITranslateService.t('TUIChat.转文字');
  295. }
  296. defineExpose({
  297. messageToolDom,
  298. });
  299. </script>
  300. <style lang="scss" scoped>
  301. @import "../../../../assets/styles/common";
  302. .dialog-item-web {
  303. background: #fff;
  304. border-radius: 8px;
  305. border: 1px solid #e0e0e0;
  306. padding: 12px 0;
  307. .dialog-item-list {
  308. display: flex;
  309. align-items: baseline;
  310. white-space: nowrap;
  311. flex-wrap: wrap;
  312. width: 280px;
  313. .list-item {
  314. padding: 4px 12px;
  315. display: flex;
  316. flex-direction: row;
  317. align-items: center;
  318. .list-item-text {
  319. padding-left: 4px;
  320. font-size: 12px;
  321. line-height: 17px;
  322. }
  323. }
  324. }
  325. }
  326. .dialog-item-h5 {
  327. @extend .dialog-item-web;
  328. padding: 0;
  329. .dialog-item-list {
  330. flex-wrap: nowrap;
  331. margin: 10px;
  332. justify-content: space-around;
  333. width: 280px;
  334. .list-item {
  335. padding: 0 8px;
  336. display: flex;
  337. flex-direction: column;
  338. align-items: center;
  339. color: #4f4f4f;
  340. .list-item-text {
  341. padding-left: 0;
  342. }
  343. }
  344. }
  345. }
  346. </style>