123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399 |
- <template>
- <div :class="containerClassNameList">
- <!-- todo 统一组件处理-->
- <div
- class="message-bubble-main-content"
- :class="[message.flow === 'in' ? '' : 'reverse']"
- >
- <Avatar
- useSkeletonAnimation
- :url="message.avatar || ''"
- />
- <main
- class="message-body"
- @click.stop
- >
- <div
- v-if="message.flow === 'in' && message.conversationType === 'GROUP'"
- class="message-body-nick-name"
- >
- {{ props.content.showName }}
- </div>
- <div :class="['message-body-main', message.flow === 'out' && 'message-body-main-reverse']">
- <div
- :class="[
- 'blink',
- 'message-body-content',
- message.flow === 'out' ? 'content-out' : 'content-in',
- message.hasRiskContent && 'content-has-risk',
- isNoPadding ? 'content-no-padding' : '',
- isNoPadding && isBlink ? 'blink-shadow' : '',
- !isNoPadding && isBlink ? 'blink-content' : '',
- ]"
- >
- <div class="content-main">
- <img
- v-if="
- (message.type === TYPES.MSG_IMAGE || message.type === TYPES.MSG_VIDEO) &&
- message.hasRiskContent
- "
- :class="['message-risk-replace', !isPC && 'message-risk-replace-h5']"
- :src="riskImageReplaceUrl"
- >
- <template v-else>
- <slot />
- </template>
- </div>
- <!-- 敏感信息失败提示 -->
- <div
- v-if="message.hasRiskContent"
- class="content-has-risk-tips"
- >
- {{ riskContentText }}
- </div>
- </div>
- <!-- 发送失败 -->
- <div
- v-if="message.status === 'fail' || message.hasRiskContent"
- class="message-label fail"
- @click="resendMessage()"
- >
- !
- </div>
- <!-- 加载图标 -->
- <Icon
- v-if="message.status === 'unSend' && needLoadingIconMessageType.includes(message.type)"
- class="message-label loading-circle"
- :file="loadingIcon"
- :width="'15px'"
- :height="'15px'"
- />
- <!-- 已读 & 未读 -->
- <ReadStatus
- class="message-label align-self-bottom"
- :message="shallowCopyMessage(message)"
- @openReadUserPanel="openReadUserPanel"
- />
- </div>
- </main>
- </div>
- <!-- message extra area -->
- <div class="message-bubble-extra-content">
- <!-- extra: message translation -->
- <MessageTranslate
- :class="message.flow === 'out' ? 'reverse' : 'flex-row'"
- :message="message"
- />
- <!-- extra: message convert voice to text -->
- <MessageConvert
- :class="message.flow === 'out' ? 'reverse' : 'flex-row'"
- :message="message"
- />
- <!-- extra: message quote -->
- <MessageQuote
- :class="message.flow === 'out' ? 'reverse' : 'flex-row'"
- :message="message"
- @blinkMessage="blinkMessage"
- @scrollTo="scrollTo"
- />
- </div>
- </div>
- </template>
- <script lang="ts" setup>
- import { computed, toRefs } from '../../../../adapter-vue';
- import TUIChatEngine, { TUITranslateService, IMessageModel } from '@tencentcloud/chat-uikit-engine';
- import Icon from '../../../common/Icon.vue';
- import ReadStatus from './read-status/index.vue';
- import MessageQuote from './message-quote/index.vue';
- import Avatar from '../../../common/Avatar/index.vue';
- import MessageTranslate from './message-translate/index.vue';
- import MessageConvert from './message-convert/index.vue';
- import loadingIcon from '../../../../assets/icon/loading.png';
- import { shallowCopyMessage } from '../../utils/utils';
- import { isPC } from '../../../../utils/env';
- interface IProps {
- messageItem: IMessageModel;
- content?: any;
- blinkMessageIDList?: string[];
- classNameList?: string[];
- }
- interface IEmits {
- (e: 'resendMessage'): void;
- (e: 'blinkMessage', messageID: string): void;
- (e: 'setReadReceiptPanelVisible', visible: boolean, message?: IMessageModel): void;
- // 下面的方法是 uniapp 专属
- (e: 'scrollTo', scrollHeight: number): void;
- }
- const emits = defineEmits<IEmits>();
- const props = withDefaults(defineProps<IProps>(), {
- messageItem: () => ({} as IMessageModel),
- content: () => ({}),
- blinkMessageIDList: () => [],
- classNameList: () => [],
- });
- const TYPES = TUIChatEngine.TYPES;
- const riskImageReplaceUrl = 'https://web.sdk.qcloud.com/component/TUIKit/assets/has_risk_default.png';
- const needLoadingIconMessageType = [
- TYPES.MSG_LOCATION,
- TYPES.MSG_TEXT,
- TYPES.MSG_CUSTOM,
- TYPES.MSG_MERGER,
- TYPES.MSG_FACE,
- ];
- const { blinkMessageIDList, messageItem: message } = toRefs(props);
- const containerClassNameList = computed(() => {
- return ['message-bubble', ...props.classNameList];
- });
- const isNoPadding = computed(() => {
- return [TYPES.MSG_IMAGE, TYPES.MSG_VIDEO].includes(message.value.type);
- });
- const riskContentText = computed<string>(() => {
- let content = TUITranslateService.t('TUIChat.涉及敏感内容') + ', ';
- if (message.value.flow === 'out') {
- content += TUITranslateService.t('TUIChat.发送失败');
- } else {
- content += TUITranslateService.t(
- message.value.type === TYPES.MSG_AUDIO ? 'TUIChat.无法收听' : 'TUIChat.无法查看',
- );
- }
- return content;
- });
- const isBlink = computed(() => {
- if (message.value?.ID) {
- return blinkMessageIDList?.value?.includes(message.value.ID);
- }
- return false;
- });
- function resendMessage() {
- if (!message.value?.hasRiskContent) {
- emits('resendMessage');
- }
- }
- function blinkMessage(messageID: string) {
- emits('blinkMessage', messageID);
- }
- function scrollTo(scrollHeight: number) {
- emits('scrollTo', scrollHeight);
- }
- function openReadUserPanel() {
- emits('setReadReceiptPanelVisible', true, message.value);
- }
- </script>
- <style lang="scss" scoped>
- .flex-row {
- display: flex;
- }
- .reverse {
- display: flex;
- flex-direction: row-reverse;
- justify-content: flex-start;
- }
- .message-bubble {
- width: 100%;
- box-sizing: border-box;
- display: flex;
- flex-direction: column;
- padding: 0 20px 25px;
- user-select: none;
- -webkit-touch-callout: none; /* 系统默认菜单被禁用 */
- -webkit-user-select: none; /* webkit浏览器 */
- -khtml-user-select: none; /* 早期浏览器 */
- -moz-user-select: none;/* 火狐 */
- -ms-user-select: none; /* IE10 */
- .message-bubble-main-content {
- display: flex;
- .message-avatar {
- display: block;
- width: 36px;
- height: 36px;
- border-radius: 5px;
- flex: 0 0 auto;
- }
- .message-body {
- display: flex;
- flex: 0 1 auto;
- flex-direction: column;
- align-items: flex-start;
- margin: 0 8px;
- max-width: calc(100% - 54px);
- .message-body-nick-name {
- margin-bottom: 4px;
- font-size: 12px;
- color: #999;
- max-width: 150px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .message-body-main {
- max-width: 100%;
- display: flex;
- flex-direction: row;
- min-width: 0;
- box-sizing: border-box;
- &-reverse {
- flex-direction: row-reverse;
- }
- .message-body-content {
- display: flex;
- flex-direction: column;
- min-width: 0;
- box-sizing: border-box;
- padding: 12px;
- font-size: 14px;
- color: #000;
- letter-spacing: 0;
- word-wrap: break-word;
- word-break: break-all;
- position: relative;
- .content-main {
- box-sizing: border-box;
- display: flex;
- flex-direction: column;
- flex-shrink: 0;
- align-content: flex-start;
- border: 0 solid black;
- margin: 0;
- padding: 0;
- min-width: 0;
- .message-risk-replace {
- width: 130px;
- height: 130px;
- }
- }
- .content-has-risk-tips {
- font-size: 12px;
- color: #fa5151;
- font-family: PingFangSC-Regular;
- margin-top: 5px;
- border-top: 1px solid #e5c7c7;
- padding-top: 5px;
- }
- }
- .content-in {
- background: #fbfbfb;
- border-radius: 0 10px 10px;
- }
- .content-out {
- background: #fff;
- border-radius: 10px 0 10px 10px;
- }
- .content-no-padding {
- padding: 0;
- background: transparent;
- border-radius: 10px;
- overflow: hidden;
- }
- .content-no-padding.content-has-risk {
- padding: 12px;
- }
- .content-has-risk {
- background: rgba(250, 81, 81, 0.16);
- }
- .blink-shadow {
- @keyframes shadow-blink {
- 50% {
- box-shadow: rgba(255, 156, 25, 1) 0 0 10px 0;
- }
- }
- box-shadow: rgba(255, 156, 25, 0) 0 0 10px 0;
- animation: shadow-blink 1s linear 3;
- }
- .blink-content {
- @keyframes reference-blink {
- 50% {
- background-color: #ff9c19;
- }
- }
- animation: reference-blink 1s linear 3;
- }
- .message-label {
- align-self: flex-end;
- font-family: PingFangSC-Regular;
- font-size: 12px;
- color: #b6b8ba;
- word-break: keep-all;
- flex: 0 0 auto;
- margin: 0 8px;
- &.fail {
- width: 15px;
- height: 15px;
- border-radius: 15px;
- background: red;
- color: #fff;
- display: flex;
- justify-content: center;
- align-items: center;
- cursor: pointer;
- }
- &.loading-circle {
- opacity: 0;
- animation: circle-loading 2s linear 1s infinite;
- }
- @keyframes circle-loading {
- 0% {
- transform: rotate(0);
- opacity: 1;
- }
- 100% {
- opacity: 1;
- transform: rotate(360deg);
- }
- }
- }
- .align-self-bottom {
- align-self: flex-end;
- }
- }
- }
- }
- .message-bubble-extra-content {
- display: flex;
- flex-direction: column;
- }
- }
- </style>
|