index.vue 10 KB

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