@tencentcloud/ai-desk-customer-vue 1.7.3 → 1.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/components/CustomerServiceChat/index-web.vue +3 -0
- package/components/CustomerServiceChat/message-list/index-web.vue +67 -8
- package/components/CustomerServiceChat/message-list/message-elements/message-bubble-web.vue +16 -7
- package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-desk.vue +7 -1
- package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-rich-text.vue +7 -48
- package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-desk-elements/message-stream.vue +6 -16
- package/components/CustomerServiceChat/message-list/message-elements/message-desk/message-plugin-web.vue +6 -0
- package/components/CustomerServiceChat/message-list/message-elements/message-image-web.vue +1 -0
- package/components/CustomerServiceChat/message-list/message-elements/message-large-stream.vue +2 -16
- package/components/CustomerServiceChat/message-list/message-elements/message-video-web.vue +20 -8
- package/components/common/ImagePreviewer/image-item-web.vue +9 -1
- package/components/common/ImagePreviewer/index-web.vue +179 -42
- package/package.json +1 -1
- package/utils/utils.ts +24 -0
package/CHANGELOG.md
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
@closeBottomQuickOrder="closeBottomQuickOrder"
|
|
28
28
|
:enableFeedback="props.enableFeedback"
|
|
29
29
|
:enableAINote="props.enableAINote"
|
|
30
|
+
:enableFileMediaPreview="props.enableFileMediaPreview"
|
|
30
31
|
@like="onLike"
|
|
31
32
|
@dislike="onDislike"
|
|
32
33
|
/>
|
|
@@ -153,6 +154,7 @@ interface IProps {
|
|
|
153
154
|
enableFeedback?: number;
|
|
154
155
|
enableAINote?: number;
|
|
155
156
|
enableURLDetection?: number;
|
|
157
|
+
enableFileMediaPreview?: number;
|
|
156
158
|
headerConfig?: IHeaderConfig;
|
|
157
159
|
enableSendingAudio?: number;
|
|
158
160
|
showQueuePage?: number;
|
|
@@ -205,6 +207,7 @@ const props = withDefaults(defineProps<IProps>(), {
|
|
|
205
207
|
enableAINote: 1,
|
|
206
208
|
langList: () => [],
|
|
207
209
|
enableURLDetection: 0,
|
|
210
|
+
enableFileMediaPreview: 0,
|
|
208
211
|
enableSendingAudio: 0,
|
|
209
212
|
showQueuePage: 0,
|
|
210
213
|
showAllRobotWelcomeItems: 0,
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
:enableAINote="props.enableAINote"
|
|
54
54
|
@like="onLike"
|
|
55
55
|
@dislike="onDislike"
|
|
56
|
+
@previewImage="handleImagePreviewByUrl"
|
|
56
57
|
/>
|
|
57
58
|
<div
|
|
58
59
|
v-else
|
|
@@ -72,6 +73,7 @@
|
|
|
72
73
|
:messageItem="JSON.parse(JSON.stringify(item))"
|
|
73
74
|
:enableFeedback="props.enableFeedback"
|
|
74
75
|
:enableAINote="props.enableAINote"
|
|
76
|
+
:fileMediaType="getFileMediaType(item)"
|
|
75
77
|
@blinkMessage="blinkMessage"
|
|
76
78
|
@resendMessage="resendMessage(item)"
|
|
77
79
|
@like="onLike"
|
|
@@ -85,23 +87,27 @@
|
|
|
85
87
|
:flow="item.flow"
|
|
86
88
|
/>
|
|
87
89
|
<ProgressMessage
|
|
88
|
-
v-else-if="item.type === TYPES.MSG_IMAGE
|
|
89
|
-
|
|
90
|
+
v-else-if="item.type === TYPES.MSG_IMAGE
|
|
91
|
+
|| (item.type === TYPES.MSG_FILE && getFileMediaType(item) === 'image')"
|
|
92
|
+
:content="getDisplayImageContent(item)"
|
|
90
93
|
:messageItem="item"
|
|
91
94
|
>
|
|
92
95
|
<MessageImage
|
|
93
|
-
:content="item
|
|
96
|
+
:content="getDisplayImageContent(item)"
|
|
94
97
|
:messageItem="item"
|
|
95
|
-
@previewImage="
|
|
98
|
+
@previewImage="item.type === TYPES.MSG_IMAGE
|
|
99
|
+
? handleImagePreview(item)
|
|
100
|
+
: handleImagePreviewByUrl(item.getMessageContent()?.url)"
|
|
96
101
|
/>
|
|
97
102
|
</ProgressMessage>
|
|
98
103
|
<ProgressMessage
|
|
99
|
-
v-else-if="item.type === TYPES.MSG_VIDEO
|
|
100
|
-
|
|
104
|
+
v-else-if="item.type === TYPES.MSG_VIDEO
|
|
105
|
+
|| (item.type === TYPES.MSG_FILE && getFileMediaType(item) === 'video')"
|
|
106
|
+
:content="getDisplayVideoContent(item)"
|
|
101
107
|
:messageItem="item"
|
|
102
108
|
>
|
|
103
109
|
<MessageVideo
|
|
104
|
-
:content="item
|
|
110
|
+
:content="getDisplayVideoContent(item)"
|
|
105
111
|
:messageItem="item"
|
|
106
112
|
/>
|
|
107
113
|
</ProgressMessage>
|
|
@@ -143,6 +149,7 @@
|
|
|
143
149
|
v-else-if="item.type === TYPES.MSG_STREAM"
|
|
144
150
|
:content="shallowCopyMessage(item.payload)"
|
|
145
151
|
@heightChanged="onHeightChanged"
|
|
152
|
+
@previewImage="handleImagePreviewByUrl"
|
|
146
153
|
/>
|
|
147
154
|
</template>
|
|
148
155
|
</MessageBubble>
|
|
@@ -185,6 +192,7 @@
|
|
|
185
192
|
v-if="showImagePreview"
|
|
186
193
|
:currentImage="currentImagePreview"
|
|
187
194
|
:imageList="imageMessageList"
|
|
195
|
+
:imageUrl="markdownImagePreviewUrl || undefined"
|
|
188
196
|
@close="onImagePreviewerClose"
|
|
189
197
|
/>
|
|
190
198
|
<BottomQuickOrder
|
|
@@ -243,12 +251,14 @@ import {
|
|
|
243
251
|
isNonEmptyObject,
|
|
244
252
|
updateCustomStore,
|
|
245
253
|
throttle,
|
|
246
|
-
shallowCopyMessage
|
|
254
|
+
shallowCopyMessage,
|
|
255
|
+
getMediaTypeByUrl
|
|
247
256
|
} from '../../../utils/utils';
|
|
248
257
|
import { isMessageInvisible, isThinkingMessage, isThinkingMessageOverTime, JSONToObject, isTransferMessageWithoutDesc } from '../../../utils/index';
|
|
249
258
|
import { isCustomerConversation } from '../../../index';
|
|
250
259
|
import { CUSTOM_MESSAGE_SRC } from '../../../constant';
|
|
251
260
|
import { QuickOrderModel } from '../../../interface';
|
|
261
|
+
import type { IImageMessageContent, IVideoMessageContent } from '../../../interface';
|
|
252
262
|
import Log from '../../../utils/logger';
|
|
253
263
|
|
|
254
264
|
interface ScrollConfig {
|
|
@@ -265,11 +275,13 @@ interface IProps {
|
|
|
265
275
|
showBottomQuickOrder: boolean;
|
|
266
276
|
enableFeedback: number;
|
|
267
277
|
enableAINote: number;
|
|
278
|
+
enableFileMediaPreview?: number;
|
|
268
279
|
}
|
|
269
280
|
const props = withDefaults(defineProps<IProps>(), {
|
|
270
281
|
showBottomQuickOrder: false,
|
|
271
282
|
enableFeedback: 0,
|
|
272
283
|
enableAINote: 1,
|
|
284
|
+
enableFileMediaPreview: 0,
|
|
273
285
|
});
|
|
274
286
|
|
|
275
287
|
interface IEmits {
|
|
@@ -310,6 +322,7 @@ const enableAutoScroll = ref(true);
|
|
|
310
322
|
// image preview
|
|
311
323
|
const showImagePreview = ref(false);
|
|
312
324
|
const currentImagePreview = ref<IMessageModel>();
|
|
325
|
+
const markdownImagePreviewUrl = ref<string>('');
|
|
313
326
|
const imageMessageList = computed(() =>
|
|
314
327
|
messageList?.value?.filter((item: IMessageModel) => {
|
|
315
328
|
return (
|
|
@@ -598,9 +611,55 @@ const handleImagePreview = (message: IMessageModel) => {
|
|
|
598
611
|
currentImagePreview.value = message;
|
|
599
612
|
};
|
|
600
613
|
|
|
614
|
+
const getFileMediaType = (message: IMessageModel): '' | 'image' | 'video' => {
|
|
615
|
+
if (!props.enableFileMediaPreview) return '';
|
|
616
|
+
if (message.type !== TYPES.value.MSG_FILE) return '';
|
|
617
|
+
const content = message.getMessageContent?.() || {};
|
|
618
|
+
const mediaType = getMediaTypeByUrl(content.url, content.name);
|
|
619
|
+
return mediaType === 'image' || mediaType === 'video' ? mediaType : '';
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
const getDisplayImageContent = (message: IMessageModel): IImageMessageContent => {
|
|
623
|
+
if (message.type === TYPES.value.MSG_FILE) {
|
|
624
|
+
return mapFileToImageContent(message.getMessageContent());
|
|
625
|
+
}
|
|
626
|
+
return message.getMessageContent();
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
const getDisplayVideoContent = (message: IMessageModel): IVideoMessageContent => {
|
|
630
|
+
if (message.type === TYPES.value.MSG_FILE) {
|
|
631
|
+
return mapFileToVideoContent(message.getMessageContent());
|
|
632
|
+
}
|
|
633
|
+
return message.getMessageContent();
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
const mapFileToImageContent = (content: any): IImageMessageContent => ({
|
|
637
|
+
url: content.url,
|
|
638
|
+
showName: content.name,
|
|
639
|
+
width: 0,
|
|
640
|
+
height: 0,
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
const mapFileToVideoContent = (content: any): IVideoMessageContent => ({
|
|
644
|
+
url: content.url,
|
|
645
|
+
showName: content.name,
|
|
646
|
+
snapshotUrl: '',
|
|
647
|
+
snapshotWidth: 0,
|
|
648
|
+
snapshotHeight: 0,
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
const handleImagePreviewByUrl = (url: string) => {
|
|
652
|
+
if (showImagePreview.value || isLongpressing.value || !url) {
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
markdownImagePreviewUrl.value = url;
|
|
656
|
+
showImagePreview.value = true;
|
|
657
|
+
};
|
|
658
|
+
|
|
601
659
|
const onImagePreviewerClose = () => {
|
|
602
660
|
showImagePreview.value = false;
|
|
603
661
|
currentImagePreview.value = null;
|
|
662
|
+
markdownImagePreviewUrl.value = '';
|
|
604
663
|
};
|
|
605
664
|
|
|
606
665
|
// toggle message
|
|
@@ -55,11 +55,7 @@
|
|
|
55
55
|
/>
|
|
56
56
|
<div class="content-main">
|
|
57
57
|
<img
|
|
58
|
-
v-if="
|
|
59
|
-
(message.type === TYPES.MSG_IMAGE ||
|
|
60
|
-
message.type === TYPES.MSG_VIDEO) &&
|
|
61
|
-
message.hasRiskContent
|
|
62
|
-
"
|
|
58
|
+
v-if="isDisplayAsImageOrVideo && message.hasRiskContent"
|
|
63
59
|
:class="[
|
|
64
60
|
'message-risk-replace',
|
|
65
61
|
!isPC && 'message-risk-replace-h5',
|
|
@@ -168,6 +164,7 @@ interface IProps {
|
|
|
168
164
|
isAudioPlayed?: boolean | undefined;
|
|
169
165
|
enableFeedback: number;
|
|
170
166
|
enableAINote: number;
|
|
167
|
+
fileMediaType?: '' | 'image' | 'video';
|
|
171
168
|
}
|
|
172
169
|
|
|
173
170
|
interface IEmits {
|
|
@@ -189,6 +186,7 @@ const props = withDefaults(defineProps<IProps>(), {
|
|
|
189
186
|
classNameList: () => [],
|
|
190
187
|
enableFeedback: 0,
|
|
191
188
|
enableAINote: 1,
|
|
189
|
+
fileMediaType: '',
|
|
192
190
|
});
|
|
193
191
|
|
|
194
192
|
const TYPES = TUIChatEngine.TYPES;
|
|
@@ -317,11 +315,22 @@ const isProductCardOrOrderMessage = computed(() => {
|
|
|
317
315
|
return false;
|
|
318
316
|
});
|
|
319
317
|
|
|
318
|
+
const isDisplayAsImageOrVideo = computed(() => {
|
|
319
|
+
if (props.fileMediaType === 'image' || props.fileMediaType === 'video') {
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
return (
|
|
323
|
+
message.value.type === TYPES.MSG_IMAGE
|
|
324
|
+
|| message.value.type === TYPES.MSG_VIDEO
|
|
325
|
+
);
|
|
326
|
+
});
|
|
327
|
+
|
|
320
328
|
const isNoPadding = computed(() => {
|
|
321
329
|
return (
|
|
322
330
|
!hasEmojiReaction.value
|
|
323
|
-
&&
|
|
324
|
-
|
|
331
|
+
&& (
|
|
332
|
+
isDisplayAsImageOrVideo.value
|
|
333
|
+
|| message.value.type === TYPES.MSG_MERGER
|
|
325
334
|
)
|
|
326
335
|
);
|
|
327
336
|
});
|
|
@@ -38,12 +38,14 @@
|
|
|
38
38
|
<MessageRichText
|
|
39
39
|
:payload="payload"
|
|
40
40
|
@heightChanged="onHeightChanged"
|
|
41
|
+
@previewImage="onPreviewImage"
|
|
41
42
|
/>
|
|
42
43
|
</div>
|
|
43
44
|
<div v-if="payload.src === CUSTOM_MESSAGE_SRC.STREAM_TEXT">
|
|
44
45
|
<MessageStream
|
|
45
46
|
:payload="payload"
|
|
46
47
|
@heightChanged="onHeightChanged"
|
|
48
|
+
@previewImage="onPreviewImage"
|
|
47
49
|
/>
|
|
48
50
|
</div>
|
|
49
51
|
<div v-if="payload.src === CUSTOM_MESSAGE_SRC.MULTI_BRANCH">
|
|
@@ -132,7 +134,7 @@ export default {
|
|
|
132
134
|
default: () => ({}),
|
|
133
135
|
},
|
|
134
136
|
},
|
|
135
|
-
emits: ['showFormPopup', 'heightChanged', 'messageSent'],
|
|
137
|
+
emits: ['showFormPopup', 'heightChanged', 'messageSent', 'previewImage'],
|
|
136
138
|
setup(props: Props, { emit }) {
|
|
137
139
|
const payload = computed<customerServicePayloadType>(() => {
|
|
138
140
|
return props.message && JSONToObject(props.message?.payload?.data);
|
|
@@ -163,6 +165,9 @@ export default {
|
|
|
163
165
|
const onHeightChanged = () => {
|
|
164
166
|
emit('heightChanged');
|
|
165
167
|
};
|
|
168
|
+
const onPreviewImage = (url: string) => {
|
|
169
|
+
emit('previewImage', url);
|
|
170
|
+
};
|
|
166
171
|
|
|
167
172
|
return {
|
|
168
173
|
payload,
|
|
@@ -172,6 +177,7 @@ export default {
|
|
|
172
177
|
sendCustomMessage,
|
|
173
178
|
handleShowFormPopup,
|
|
174
179
|
onHeightChanged,
|
|
180
|
+
onPreviewImage,
|
|
175
181
|
};
|
|
176
182
|
},
|
|
177
183
|
};
|
|
@@ -5,15 +5,6 @@
|
|
|
5
5
|
class="message-marked"
|
|
6
6
|
v-html="parsedContent"
|
|
7
7
|
/>
|
|
8
|
-
<div v-if="image" class="rich-image-previewer" @click="closeImage">
|
|
9
|
-
<img
|
|
10
|
-
class="rich-image-preview"
|
|
11
|
-
:style="{
|
|
12
|
-
transform: `scale(1) rotate(0deg)`,
|
|
13
|
-
}"
|
|
14
|
-
:src="imageSrc"
|
|
15
|
-
/>
|
|
16
|
-
</div>
|
|
17
8
|
</div>
|
|
18
9
|
</div>
|
|
19
10
|
</template>
|
|
@@ -22,7 +13,7 @@
|
|
|
22
13
|
import vue from '../../../../../../adapter-vue';
|
|
23
14
|
import { parseMarkdown } from "./marked";
|
|
24
15
|
import { customerServicePayloadType } from '../../../../../../interface';
|
|
25
|
-
const { computed
|
|
16
|
+
const { computed } = vue;
|
|
26
17
|
interface Props {
|
|
27
18
|
payload: customerServicePayloadType;
|
|
28
19
|
}
|
|
@@ -34,15 +25,12 @@ export default {
|
|
|
34
25
|
default: () => ({}),
|
|
35
26
|
},
|
|
36
27
|
},
|
|
28
|
+
emits: ['heightChanged', 'previewImage'],
|
|
37
29
|
setup(props: Props, { emit }) {
|
|
38
|
-
const image = ref(false);
|
|
39
|
-
const imageSrc = ref('');
|
|
40
|
-
const imageList = [];
|
|
41
30
|
const parsedContent = computed(() => {
|
|
42
31
|
// @ts-ignore
|
|
43
32
|
window.onMarkdownImageClicked = function(href: string) {
|
|
44
|
-
|
|
45
|
-
imageSrc.value = decodeURIComponent(href);
|
|
33
|
+
emit('previewImage', decodeURIComponent(href));
|
|
46
34
|
}
|
|
47
35
|
// @ts-ignore
|
|
48
36
|
window.onMarkdownMediaLoad = function() {
|
|
@@ -51,47 +39,14 @@ export default {
|
|
|
51
39
|
return parseMarkdown(props.payload.content);
|
|
52
40
|
});
|
|
53
41
|
|
|
54
|
-
const closeImage = () => {
|
|
55
|
-
image.value = !image.value;
|
|
56
|
-
imageSrc.value = '';
|
|
57
|
-
}
|
|
58
|
-
|
|
59
42
|
return {
|
|
60
43
|
props,
|
|
61
44
|
parsedContent,
|
|
62
|
-
image,
|
|
63
|
-
imageSrc,
|
|
64
|
-
closeImage,
|
|
65
|
-
imageList,
|
|
66
45
|
};
|
|
67
46
|
},
|
|
68
47
|
};
|
|
69
48
|
</script>
|
|
70
49
|
<style lang="scss">
|
|
71
|
-
.rich-image-previewer {
|
|
72
|
-
position: fixed;
|
|
73
|
-
z-index: 101;
|
|
74
|
-
width: 100vw;
|
|
75
|
-
height: 100vh;
|
|
76
|
-
background: rgba(#000, 0.8);
|
|
77
|
-
top: 0;
|
|
78
|
-
left: 0;
|
|
79
|
-
display: flex;
|
|
80
|
-
flex-direction: column;
|
|
81
|
-
align-items: center;
|
|
82
|
-
justify-content: center;
|
|
83
|
-
user-select: none;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
.rich-image-preview {
|
|
87
|
-
max-width: 90%;
|
|
88
|
-
height: auto;
|
|
89
|
-
display: flex;
|
|
90
|
-
align-items: center;
|
|
91
|
-
justify-content: center;
|
|
92
|
-
overflow: hidden;
|
|
93
|
-
pointer-events: auto;
|
|
94
|
-
}
|
|
95
50
|
.message-video {
|
|
96
51
|
position: relative;
|
|
97
52
|
display: flex;
|
|
@@ -145,4 +100,8 @@ export default {
|
|
|
145
100
|
}
|
|
146
101
|
}
|
|
147
102
|
}
|
|
103
|
+
|
|
104
|
+
.message-marked .image-container {
|
|
105
|
+
-webkit-tap-highlight-color: transparent;
|
|
106
|
+
}
|
|
148
107
|
</style>
|
|
@@ -4,12 +4,6 @@
|
|
|
4
4
|
<span v-if="isCursorBlinking" class="blinking-cursor">|</span>
|
|
5
5
|
<div ref="contentRef" :class="['message-marked']"></div>
|
|
6
6
|
</div>
|
|
7
|
-
<div v-if="image" class="rich-image-previewer" @click="closeImage">
|
|
8
|
-
<img
|
|
9
|
-
class="rich-image-preview"
|
|
10
|
-
:src="imageSrc"
|
|
11
|
-
/>
|
|
12
|
-
</div>
|
|
13
7
|
</div>
|
|
14
8
|
</template>
|
|
15
9
|
|
|
@@ -32,8 +26,6 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
32
26
|
|
|
33
27
|
const isCursorBlinking = ref<boolean>(true);
|
|
34
28
|
const isStreaming = ref<boolean>(false);
|
|
35
|
-
const image = ref(false);
|
|
36
|
-
const imageSrc = ref('');
|
|
37
29
|
const chunks = ref<string>('');
|
|
38
30
|
const isFinished = ref<boolean>(true);
|
|
39
31
|
const prevChunksLength = ref<number>(0);
|
|
@@ -43,8 +35,7 @@ const contentRef = ref<HTMLElement>(); // DOM 容器引用
|
|
|
43
35
|
// 设置全局回调函数(只需设置一次)
|
|
44
36
|
// @ts-ignore
|
|
45
37
|
window.onMarkdownImageClicked = function(href: string) {
|
|
46
|
-
|
|
47
|
-
imageSrc.value = decodeURIComponent(href);
|
|
38
|
+
emits('previewImage', decodeURIComponent(href));
|
|
48
39
|
}
|
|
49
40
|
// @ts-ignore
|
|
50
41
|
window.onMarkdownMediaLoad = function() {
|
|
@@ -288,7 +279,7 @@ watch(streamContent, () => {
|
|
|
288
279
|
doRender();
|
|
289
280
|
});
|
|
290
281
|
|
|
291
|
-
const emits = defineEmits(['heightChanged']);
|
|
282
|
+
const emits = defineEmits(['heightChanged', 'previewImage']);
|
|
292
283
|
// 不支持 ResizeObserver 的浏览器太老旧,默认不处理了
|
|
293
284
|
const canIUseResizeObserver = typeof ResizeObserver === 'undefined' ? false : true;
|
|
294
285
|
|
|
@@ -431,11 +422,6 @@ const observeHeightChanged = (newHeight) => {
|
|
|
431
422
|
}
|
|
432
423
|
};
|
|
433
424
|
|
|
434
|
-
const closeImage = () => {
|
|
435
|
-
image.value = !image.value;
|
|
436
|
-
imageSrc.value = '';
|
|
437
|
-
};
|
|
438
|
-
|
|
439
425
|
</script>
|
|
440
426
|
<style lang="scss" scoped>
|
|
441
427
|
@import "../../../../style/common.scss";
|
|
@@ -452,6 +438,10 @@ const closeImage = () => {
|
|
|
452
438
|
margin-left: 1.5em;
|
|
453
439
|
}
|
|
454
440
|
|
|
441
|
+
::v-deep .message-marked .image-container {
|
|
442
|
+
-webkit-tap-highlight-color: transparent;
|
|
443
|
+
}
|
|
444
|
+
|
|
455
445
|
.blinking-cursor {
|
|
456
446
|
animation: blink 0.8s step-end infinite;
|
|
457
447
|
color: #000;
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
:message="props.message"
|
|
21
21
|
@heightChanged="onHeightChanged"
|
|
22
22
|
@messageSent="onMessageSent"
|
|
23
|
+
@previewImage="onPreviewImage"
|
|
23
24
|
/>
|
|
24
25
|
</template>
|
|
25
26
|
<template #messageTip>
|
|
@@ -65,6 +66,7 @@ const emits = defineEmits([
|
|
|
65
66
|
'messageSent',
|
|
66
67
|
'like',
|
|
67
68
|
'dislike',
|
|
69
|
+
'previewImage',
|
|
68
70
|
]);
|
|
69
71
|
const messageModel = computed(() => TUIStore.getMessageModel(props.message.ID));
|
|
70
72
|
|
|
@@ -111,6 +113,10 @@ const onMessageSent = () => {
|
|
|
111
113
|
emits('messageSent');
|
|
112
114
|
};
|
|
113
115
|
|
|
116
|
+
const onPreviewImage = (url: string) => {
|
|
117
|
+
emits('previewImage', url);
|
|
118
|
+
};
|
|
119
|
+
|
|
114
120
|
function onLike(type: number, messageInfo: Object) {
|
|
115
121
|
emits('like', type, messageInfo);
|
|
116
122
|
};
|
package/components/CustomerServiceChat/message-list/message-elements/message-large-stream.vue
CHANGED
|
@@ -4,12 +4,6 @@
|
|
|
4
4
|
<span v-if="isCursorBlinking" class="blinking-cursor">|</span>
|
|
5
5
|
<div ref="preRef" :class="['message-marked']" v-html="displayedContent" />
|
|
6
6
|
</div>
|
|
7
|
-
<div v-if="image" class="rich-image-previewer" @click="closeImage">
|
|
8
|
-
<img
|
|
9
|
-
class="rich-image-preview"
|
|
10
|
-
:src="imageSrc"
|
|
11
|
-
/>
|
|
12
|
-
</div>
|
|
13
7
|
</div>
|
|
14
8
|
</template>
|
|
15
9
|
|
|
@@ -32,8 +26,6 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
32
26
|
|
|
33
27
|
const isCursorBlinking = ref<boolean>(true);
|
|
34
28
|
const isStreaming = ref<boolean>(false);
|
|
35
|
-
const image = ref(false);
|
|
36
|
-
const imageSrc = ref('');
|
|
37
29
|
const markdown = ref<string>('');
|
|
38
30
|
const isStreamEnded = ref<boolean>(true);
|
|
39
31
|
const prevmarkdownLength = ref<number>(0);
|
|
@@ -41,8 +33,7 @@ const streamContent = ref<string>('');
|
|
|
41
33
|
const displayedContent = computed(() => {
|
|
42
34
|
// @ts-ignore
|
|
43
35
|
window.onMarkdownImageClicked = function(href:string) {
|
|
44
|
-
|
|
45
|
-
imageSrc.value = decodeURIComponent(href);
|
|
36
|
+
emits('previewImage', decodeURIComponent(href));
|
|
46
37
|
}
|
|
47
38
|
// @ts-ignore
|
|
48
39
|
window.onMarkdownMediaLoad = function() {
|
|
@@ -53,7 +44,7 @@ const displayedContent = computed(() => {
|
|
|
53
44
|
return result;
|
|
54
45
|
});
|
|
55
46
|
const preRef = ref();
|
|
56
|
-
const emits = defineEmits(['heightChanged']);
|
|
47
|
+
const emits = defineEmits(['heightChanged', 'previewImage']);
|
|
57
48
|
// 不支持 ResizeObserver 的浏览器太老旧,默认不处理了
|
|
58
49
|
const canIUseResizeObserver = typeof ResizeObserver === 'undefined' ? false : true;
|
|
59
50
|
|
|
@@ -141,11 +132,6 @@ const observeHeightChanged = (newHeight) => {
|
|
|
141
132
|
}
|
|
142
133
|
};
|
|
143
134
|
|
|
144
|
-
const closeImage = () => {
|
|
145
|
-
image.value = !image.value;
|
|
146
|
-
imageSrc.value = '';
|
|
147
|
-
};
|
|
148
|
-
|
|
149
135
|
</script>
|
|
150
136
|
<style lang="scss" scoped>
|
|
151
137
|
@import "../../style/common.scss";
|
|
@@ -45,10 +45,14 @@
|
|
|
45
45
|
class="dialog-video"
|
|
46
46
|
>
|
|
47
47
|
<div
|
|
48
|
-
class="
|
|
48
|
+
class="icon icon-close"
|
|
49
49
|
@click.stop="toggleVideoPreviewer"
|
|
50
50
|
>
|
|
51
|
-
<Icon
|
|
51
|
+
<Icon
|
|
52
|
+
:file="closeSVG"
|
|
53
|
+
width="14px"
|
|
54
|
+
height="14px"
|
|
55
|
+
/>
|
|
52
56
|
</div>
|
|
53
57
|
<div
|
|
54
58
|
class="dialog-video-box"
|
|
@@ -277,13 +281,21 @@ async function handlePosterUrl(
|
|
|
277
281
|
flex-direction: column;
|
|
278
282
|
align-items: center;
|
|
279
283
|
|
|
280
|
-
|
|
284
|
+
.icon-close {
|
|
285
|
+
position: absolute;
|
|
286
|
+
cursor: pointer;
|
|
287
|
+
border-radius: 50%;
|
|
288
|
+
top: 3%;
|
|
289
|
+
right: 3%;
|
|
290
|
+
padding: 8px;
|
|
291
|
+
background: #fff;
|
|
281
292
|
display: flex;
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
293
|
+
z-index: 1;
|
|
294
|
+
|
|
295
|
+
&:before,
|
|
296
|
+
&:after {
|
|
297
|
+
background-color: #444;
|
|
298
|
+
}
|
|
287
299
|
}
|
|
288
300
|
|
|
289
301
|
&-box {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<img
|
|
3
3
|
class="image-preview"
|
|
4
4
|
:style="{
|
|
5
|
-
transform: `scale(${props.zoom}) rotate(${props.rotate}deg)`,
|
|
5
|
+
transform: `translate3d(${props.translateX}px, ${props.translateY}px, 0) scale(${props.zoom}) rotate(${props.rotate}deg)`,
|
|
6
6
|
}"
|
|
7
7
|
:src="props.src"
|
|
8
8
|
>
|
|
@@ -21,6 +21,14 @@ const props = defineProps({
|
|
|
21
21
|
type: String,
|
|
22
22
|
default: '',
|
|
23
23
|
},
|
|
24
|
+
translateX: {
|
|
25
|
+
type: Number,
|
|
26
|
+
default: 0,
|
|
27
|
+
},
|
|
28
|
+
translateY: {
|
|
29
|
+
type: Number,
|
|
30
|
+
default: 0,
|
|
31
|
+
},
|
|
24
32
|
});
|
|
25
33
|
</script>
|
|
26
34
|
<style lang="scss">
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
@wheel.stop="handleWheel"
|
|
14
14
|
>
|
|
15
15
|
<ul
|
|
16
|
+
v-if="!props.imageUrl"
|
|
16
17
|
ref="ulRef"
|
|
17
18
|
class="image-list"
|
|
18
19
|
:style="{
|
|
@@ -31,40 +32,59 @@
|
|
|
31
32
|
<ImageItem
|
|
32
33
|
:zoom="zoom"
|
|
33
34
|
:rotate="rotate"
|
|
35
|
+
:translateX="translateX"
|
|
36
|
+
:translateY="translateY"
|
|
34
37
|
:src="getImageUrl(item)"
|
|
35
38
|
:messageItem="item"
|
|
36
39
|
/>
|
|
37
40
|
</li>
|
|
38
41
|
</ul>
|
|
42
|
+
<ul
|
|
43
|
+
v-else
|
|
44
|
+
ref="ulRef"
|
|
45
|
+
class="image-list"
|
|
46
|
+
:style="{ width: '100%' }"
|
|
47
|
+
>
|
|
48
|
+
<li class="image-item">
|
|
49
|
+
<ImageItem
|
|
50
|
+
:zoom="zoom"
|
|
51
|
+
:rotate="rotate"
|
|
52
|
+
:translateX="translateX"
|
|
53
|
+
:translateY="translateY"
|
|
54
|
+
:src="props.imageUrl"
|
|
55
|
+
/>
|
|
56
|
+
</li>
|
|
57
|
+
</ul>
|
|
39
58
|
</div>
|
|
40
59
|
<div
|
|
41
|
-
v-show="isPC"
|
|
42
60
|
class="icon icon-close"
|
|
43
61
|
@click="close"
|
|
44
62
|
>
|
|
45
63
|
<Icon
|
|
46
64
|
:file="iconClose"
|
|
47
|
-
width="
|
|
48
|
-
height="
|
|
65
|
+
width="14px"
|
|
66
|
+
height="14px"
|
|
49
67
|
/>
|
|
50
68
|
</div>
|
|
51
69
|
<div
|
|
52
|
-
v-if="isPC && currentImageIndex > 0"
|
|
70
|
+
v-if="isPC && !props.imageUrl && currentImageIndex > 0"
|
|
53
71
|
class="image-button image-button-left"
|
|
54
72
|
@click="goPrev"
|
|
55
73
|
>
|
|
56
74
|
<Icon :file="iconArrowLeft" />
|
|
57
75
|
</div>
|
|
58
76
|
<div
|
|
59
|
-
v-if="isPC && currentImageIndex < imageList.length - 1"
|
|
77
|
+
v-if="isPC && !props.imageUrl && currentImageIndex < imageList.length - 1"
|
|
60
78
|
class="image-button image-button-right"
|
|
61
79
|
@click="goNext"
|
|
62
80
|
>
|
|
63
81
|
<Icon :file="iconArrowLeft" />
|
|
64
82
|
</div>
|
|
65
|
-
<div
|
|
83
|
+
<div
|
|
84
|
+
v-if="isPC"
|
|
85
|
+
:class="['actions-bar', isMobile && 'actions-bar-h5']"
|
|
86
|
+
>
|
|
66
87
|
<div
|
|
67
|
-
v-if="isPC"
|
|
68
88
|
class="icon-zoom-in"
|
|
69
89
|
@click="zoomIn"
|
|
70
90
|
>
|
|
@@ -75,7 +95,6 @@
|
|
|
75
95
|
/>
|
|
76
96
|
</div>
|
|
77
97
|
<div
|
|
78
|
-
v-if="isPC"
|
|
79
98
|
class="icon-zoom-out"
|
|
80
99
|
@click="zoomOut"
|
|
81
100
|
>
|
|
@@ -86,7 +105,6 @@
|
|
|
86
105
|
/>
|
|
87
106
|
</div>
|
|
88
107
|
<div
|
|
89
|
-
v-if="isPC"
|
|
90
108
|
class="icon-refresh-left"
|
|
91
109
|
@click="rotateLeft"
|
|
92
110
|
>
|
|
@@ -97,7 +115,6 @@
|
|
|
97
115
|
/>
|
|
98
116
|
</div>
|
|
99
117
|
<div
|
|
100
|
-
v-if="isPC"
|
|
101
118
|
class="icon-refresh-right"
|
|
102
119
|
@click="rotateRight"
|
|
103
120
|
>
|
|
@@ -107,7 +124,7 @@
|
|
|
107
124
|
height="27px"
|
|
108
125
|
/>
|
|
109
126
|
</div>
|
|
110
|
-
<span class="image-counter">
|
|
127
|
+
<span v-if="!props.imageUrl" class="image-counter">
|
|
111
128
|
{{ currentImageIndex + 1 }} / {{ imageList.length }}
|
|
112
129
|
</span>
|
|
113
130
|
</div>
|
|
@@ -154,10 +171,12 @@ const props = withDefaults(
|
|
|
154
171
|
defineProps<{
|
|
155
172
|
imageList: IMessageModel[];
|
|
156
173
|
currentImage: IMessageModel;
|
|
174
|
+
imageUrl?: string;
|
|
157
175
|
}>(),
|
|
158
176
|
{
|
|
159
177
|
imageList: () => [] as IMessageModel[],
|
|
160
178
|
messageItem: () => ({} as IMessageModel),
|
|
179
|
+
imageUrl: '',
|
|
161
180
|
},
|
|
162
181
|
);
|
|
163
182
|
|
|
@@ -175,14 +194,33 @@ const minZoom = ref(0.1);
|
|
|
175
194
|
const currentImageIndex = ref(0);
|
|
176
195
|
const image = ref();
|
|
177
196
|
const ulRef = ref();
|
|
197
|
+
const translateX = ref(0);
|
|
198
|
+
const translateY = ref(0);
|
|
178
199
|
// touch
|
|
179
200
|
let startX = 0;
|
|
201
|
+
let startY = 0;
|
|
202
|
+
let baseTranslateX = 0;
|
|
203
|
+
let baseTranslateY = 0;
|
|
180
204
|
const touchStore = {} as touchesPosition;
|
|
181
205
|
let moveFlag = false;
|
|
182
206
|
let twoTouchesFlag = false;
|
|
183
207
|
let timer: number | null = null;
|
|
208
|
+
// double tap to zoom
|
|
209
|
+
let lastTapTime = 0;
|
|
210
|
+
let lastTapX = 0;
|
|
211
|
+
let lastTapY = 0;
|
|
212
|
+
let singleTapTimer: number | null = null;
|
|
213
|
+
let dblTapStep = 0;
|
|
214
|
+
const DOUBLE_TAP_DELAY = 300;
|
|
215
|
+
const DOUBLE_TAP_DIST = 30;
|
|
216
|
+
const TAP_MOVE_THRESHOLD = 10;
|
|
217
|
+
const DBL_TAP_ZOOM_LEVELS = [2, 3.5];
|
|
184
218
|
|
|
185
219
|
watchEffect(() => {
|
|
220
|
+
if (props.imageUrl) {
|
|
221
|
+
currentImageIndex.value = 0;
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
186
224
|
currentImageIndex.value = props.imageList.findIndex((message: any) => {
|
|
187
225
|
return message.ID === props?.currentImage?.ID;
|
|
188
226
|
});
|
|
@@ -204,6 +242,8 @@ const handleTouchMove = (e: any) => {
|
|
|
204
242
|
if (e.touches && e.touches.length === 2) {
|
|
205
243
|
twoTouchesFlag = true;
|
|
206
244
|
handleTwoTouches(e);
|
|
245
|
+
} else if (e.touches && e.touches.length === 1 && zoom.value > 1) {
|
|
246
|
+
handleOneTouchPan(e);
|
|
207
247
|
}
|
|
208
248
|
};
|
|
209
249
|
|
|
@@ -225,26 +265,46 @@ const handleTouchEnd = (e: any) => {
|
|
|
225
265
|
}
|
|
226
266
|
// H5 touch move to left to go to prev image
|
|
227
267
|
// H5 touch move to right to go to next image
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
268
|
+
const endX = e?.changedTouches[0]?.pageX ?? 0;
|
|
269
|
+
const endY = e?.changedTouches[0]?.pageY ?? 0;
|
|
270
|
+
const isTap = Math.hypot(endX - startX, endY - startY) < TAP_MOVE_THRESHOLD;
|
|
271
|
+
if (moveFlag && !isTap) {
|
|
272
|
+
if (timer === null) {
|
|
273
|
+
moveEndX = endX;
|
|
274
|
+
X = moveEndX - startX;
|
|
275
|
+
if (zoom.value <= 1 && !props.imageUrl) {
|
|
234
276
|
if (X > 100) {
|
|
235
277
|
goPrev();
|
|
236
278
|
} else if (X < -100) {
|
|
237
279
|
goNext();
|
|
238
280
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
281
|
+
}
|
|
282
|
+
timer = setTimeout(() => {
|
|
283
|
+
timer = null;
|
|
284
|
+
}, 200);
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
const now = Date.now();
|
|
288
|
+
const dist = Math.hypot(endX - lastTapX, endY - lastTapY);
|
|
289
|
+
if (now - lastTapTime < DOUBLE_TAP_DELAY && dist < DOUBLE_TAP_DIST) {
|
|
290
|
+
if (singleTapTimer !== null) {
|
|
291
|
+
clearTimeout(singleTapTimer);
|
|
292
|
+
singleTapTimer = null;
|
|
293
|
+
}
|
|
294
|
+
lastTapTime = 0;
|
|
295
|
+
dblTapZoom(endX, endY);
|
|
296
|
+
} else {
|
|
297
|
+
lastTapTime = now;
|
|
298
|
+
lastTapX = endX;
|
|
299
|
+
lastTapY = endY;
|
|
300
|
+
if (singleTapTimer !== null) {
|
|
301
|
+
clearTimeout(singleTapTimer);
|
|
302
|
+
}
|
|
303
|
+
singleTapTimer = setTimeout(() => {
|
|
304
|
+
singleTapTimer = null;
|
|
242
305
|
close();
|
|
243
|
-
|
|
306
|
+
}, DOUBLE_TAP_DELAY);
|
|
244
307
|
}
|
|
245
|
-
timer = setTimeout(() => {
|
|
246
|
-
timer = null;
|
|
247
|
-
}, 200);
|
|
248
308
|
}
|
|
249
309
|
};
|
|
250
310
|
|
|
@@ -265,9 +325,19 @@ const handleWheel = (e: any) => {
|
|
|
265
325
|
|
|
266
326
|
const moveInit = (e: any) => {
|
|
267
327
|
startX = e?.changedTouches[0]?.pageX;
|
|
328
|
+
startY = e?.changedTouches[0]?.pageY;
|
|
329
|
+
baseTranslateX = translateX.value;
|
|
330
|
+
baseTranslateY = translateY.value;
|
|
268
331
|
moveFlag = false;
|
|
269
332
|
};
|
|
270
333
|
|
|
334
|
+
const handleOneTouchPan = (e: any) => {
|
|
335
|
+
const touch = e?.touches[0];
|
|
336
|
+
if (!touch) return;
|
|
337
|
+
translateX.value = baseTranslateX + (touch.pageX - startX);
|
|
338
|
+
translateY.value = baseTranslateY + (touch.pageY - startY);
|
|
339
|
+
};
|
|
340
|
+
|
|
271
341
|
const twoTouchesInit = (e: any) => {
|
|
272
342
|
const touch1 = e?.touches[0];
|
|
273
343
|
const touch2 = e?.touches[1];
|
|
@@ -314,7 +384,24 @@ const handleTwoTouches = (e: any) => {
|
|
|
314
384
|
touchStore.pageX2 as number,
|
|
315
385
|
touchStore.pageY2 as number,
|
|
316
386
|
);
|
|
317
|
-
|
|
387
|
+
const newZoom = Math.min(Math.max(0.5, zoom.value * touchZoom), 4);
|
|
388
|
+
// anchor zoom at the midpoint of two fingers
|
|
389
|
+
const wrapperRect = image.value?.getBoundingClientRect?.();
|
|
390
|
+
if (wrapperRect) {
|
|
391
|
+
const centerX = wrapperRect.left + wrapperRect.width / 2;
|
|
392
|
+
const centerY = wrapperRect.top + wrapperRect.height / 2;
|
|
393
|
+
const midX = (touch1.pageX + touch2.pageX) / 2;
|
|
394
|
+
const midY = (touch1.pageY + touch2.pageY) / 2;
|
|
395
|
+
const ratio = newZoom / zoom.value;
|
|
396
|
+
translateX.value = midX - centerX - (midX - centerX - translateX.value) * ratio;
|
|
397
|
+
translateY.value = midY - centerY - (midY - centerY - translateY.value) * ratio;
|
|
398
|
+
}
|
|
399
|
+
zoom.value = newZoom;
|
|
400
|
+
// refresh anchor for the next move frame
|
|
401
|
+
touchStore.pageX1 = touch1.pageX;
|
|
402
|
+
touchStore.pageY1 = touch1.pageY;
|
|
403
|
+
touchStore.pageX2 = touch2.pageX;
|
|
404
|
+
touchStore.pageY2 = touch2.pageY;
|
|
318
405
|
};
|
|
319
406
|
|
|
320
407
|
onMounted(() => {
|
|
@@ -357,9 +444,39 @@ const goPrev = () => {
|
|
|
357
444
|
const initStyle = () => {
|
|
358
445
|
zoom.value = 1;
|
|
359
446
|
rotate.value = 0;
|
|
447
|
+
translateX.value = 0;
|
|
448
|
+
translateY.value = 0;
|
|
449
|
+
dblTapStep = 0;
|
|
450
|
+
lastTapTime = 0;
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// 1st double tap: zoom to 1.5x; 2nd: 2.5x; 3rd: reset to 1x
|
|
454
|
+
const dblTapZoom = (x: number, y: number) => {
|
|
455
|
+
const wrapperRect = image.value?.getBoundingClientRect?.();
|
|
456
|
+
if (!wrapperRect) return;
|
|
457
|
+
if (dblTapStep >= DBL_TAP_ZOOM_LEVELS.length) {
|
|
458
|
+
// reset to original size
|
|
459
|
+
zoom.value = 1;
|
|
460
|
+
translateX.value = 0;
|
|
461
|
+
translateY.value = 0;
|
|
462
|
+
dblTapStep = 0;
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const targetZoom = DBL_TAP_ZOOM_LEVELS[dblTapStep];
|
|
466
|
+
// anchor zoom at the double tap point (same formula as handleTwoTouches)
|
|
467
|
+
const centerX = wrapperRect.left + wrapperRect.width / 2;
|
|
468
|
+
const centerY = wrapperRect.top + wrapperRect.height / 2;
|
|
469
|
+
const ratio = targetZoom / zoom.value;
|
|
470
|
+
translateX.value = x - centerX - (x - centerX - translateX.value) * ratio;
|
|
471
|
+
translateY.value = y - centerY - (y - centerY - translateY.value) * ratio;
|
|
472
|
+
zoom.value = targetZoom;
|
|
473
|
+
dblTapStep += 1;
|
|
360
474
|
};
|
|
361
475
|
|
|
362
476
|
const getImageUrl = (message: IMessageModel) => {
|
|
477
|
+
if (props.imageUrl) {
|
|
478
|
+
return props.imageUrl;
|
|
479
|
+
}
|
|
363
480
|
if (isPC) {
|
|
364
481
|
return message?.payload?.imageInfoArray[0]?.url;
|
|
365
482
|
} else {
|
|
@@ -368,6 +485,11 @@ const getImageUrl = (message: IMessageModel) => {
|
|
|
368
485
|
};
|
|
369
486
|
|
|
370
487
|
const save = () => {
|
|
488
|
+
// bare url preview mode: download the url directly
|
|
489
|
+
if (props.imageUrl) {
|
|
490
|
+
downloadImgInWeb(props.imageUrl);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
371
493
|
const imageMessage = props.imageList[
|
|
372
494
|
currentImageIndex.value
|
|
373
495
|
] as IMessageModel;
|
|
@@ -383,6 +505,14 @@ const save = () => {
|
|
|
383
505
|
|
|
384
506
|
};
|
|
385
507
|
|
|
508
|
+
const getExtFromUrl = (src: string) => {
|
|
509
|
+
const pathname = src.split('?')[0].split('#')[0];
|
|
510
|
+
const dotIndex = pathname.lastIndexOf('.');
|
|
511
|
+
if (dotIndex < 0) return 'jpg';
|
|
512
|
+
const ext = pathname.slice(dotIndex + 1).toLowerCase();
|
|
513
|
+
return ext && ext.length <= 5 ? ext : 'jpg';
|
|
514
|
+
};
|
|
515
|
+
|
|
386
516
|
const downloadImgInWeb = (src: string) => {
|
|
387
517
|
const option: any = {
|
|
388
518
|
mode: 'cors',
|
|
@@ -390,16 +520,23 @@ const downloadImgInWeb = (src: string) => {
|
|
|
390
520
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
391
521
|
}),
|
|
392
522
|
};
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
523
|
+
let extension: string;
|
|
524
|
+
if (props.imageUrl) {
|
|
525
|
+
// bare url preview mode: derive extension from url
|
|
526
|
+
extension = getExtFromUrl(src);
|
|
527
|
+
} else {
|
|
528
|
+
const imageMessage = props.imageList[
|
|
529
|
+
currentImageIndex.value
|
|
530
|
+
] as IMessageModel;
|
|
531
|
+
const imageFormat: number = imageMessage?.payload?.imageFormat;
|
|
532
|
+
if (!imageFormatMap.has(imageFormat)) {
|
|
533
|
+
Toast({
|
|
534
|
+
message: TUITranslateService.t('Component.暂不支持下载此类型图片'),
|
|
535
|
+
type: TOAST_TYPE.ERROR,
|
|
536
|
+
});
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
extension = imageFormatMap.get(imageFormat) as string;
|
|
403
540
|
}
|
|
404
541
|
// If the browser supports fetch, use blob to download, so as to avoid the browser clicking the a tag and jumping to the preview of the new page
|
|
405
542
|
if ((window as any).fetch) {
|
|
@@ -409,14 +546,14 @@ const downloadImgInWeb = (src: string) => {
|
|
|
409
546
|
const a = document.createElement('a');
|
|
410
547
|
const url = window.URL.createObjectURL(blob);
|
|
411
548
|
a.href = url;
|
|
412
|
-
a.download = url + '.' +
|
|
549
|
+
a.download = url + '.' + extension;
|
|
413
550
|
a.click();
|
|
414
551
|
});
|
|
415
552
|
} else {
|
|
416
553
|
const a = document.createElement('a');
|
|
417
554
|
a.href = src;
|
|
418
555
|
a.target = '_blank';
|
|
419
|
-
a.download = src + '.' +
|
|
556
|
+
a.download = src + '.' + extension;
|
|
420
557
|
a.click();
|
|
421
558
|
}
|
|
422
559
|
};
|
|
@@ -438,7 +575,7 @@ onUnmounted(() => {
|
|
|
438
575
|
bottom: 5%;
|
|
439
576
|
padding: 12px;
|
|
440
577
|
border-radius: 6px;
|
|
441
|
-
background:
|
|
578
|
+
background: #fff;
|
|
442
579
|
|
|
443
580
|
&-h5 {
|
|
444
581
|
padding: 6px;
|
|
@@ -465,7 +602,7 @@ onUnmounted(() => {
|
|
|
465
602
|
|
|
466
603
|
.image-previewer {
|
|
467
604
|
position: fixed;
|
|
468
|
-
z-index:
|
|
605
|
+
z-index: 9999;
|
|
469
606
|
width: 100vw;
|
|
470
607
|
height: 100vh;
|
|
471
608
|
background: rgba(#000, 0.3);
|
|
@@ -558,8 +695,8 @@ onUnmounted(() => {
|
|
|
558
695
|
border-radius: 50%;
|
|
559
696
|
top: 3%;
|
|
560
697
|
right: 3%;
|
|
561
|
-
padding:
|
|
562
|
-
background:
|
|
698
|
+
padding: 8px;
|
|
699
|
+
background: #fff;
|
|
563
700
|
display: flex;
|
|
564
701
|
|
|
565
702
|
&::before,
|
|
@@ -585,7 +722,7 @@ onUnmounted(() => {
|
|
|
585
722
|
position: absolute;
|
|
586
723
|
bottom: 5%;
|
|
587
724
|
right: 5%;
|
|
588
|
-
padding:
|
|
725
|
+
padding: 8px;
|
|
589
726
|
border-radius: 6px;
|
|
590
727
|
background: rgba(255, 255, 255, 0.8);
|
|
591
728
|
}
|
package/package.json
CHANGED
package/utils/utils.ts
CHANGED
|
@@ -59,6 +59,30 @@ export const handleSkeletonSize = (
|
|
|
59
59
|
return { width: maxWidth, height: height * (maxWidth / width) };
|
|
60
60
|
};
|
|
61
61
|
|
|
62
|
+
const IMAGE_EXTENSIONS = ['gif', 'jpeg', 'jpg', 'png', 'bmp', 'webp'];
|
|
63
|
+
const VIDEO_EXTENSIONS = ['mov', 'mp4'];
|
|
64
|
+
|
|
65
|
+
export function getMediaTypeByUrl(url?: string, name?: string): 'image' | 'video' | 'file' {
|
|
66
|
+
const source = name || url || '';
|
|
67
|
+
if (!source) {
|
|
68
|
+
return 'file';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const pathname = source.split(/[?#]/)[0];
|
|
72
|
+
const dotIndex = pathname.lastIndexOf('.');
|
|
73
|
+
if (dotIndex < 0) {
|
|
74
|
+
return 'file';
|
|
75
|
+
}
|
|
76
|
+
const ext = pathname.slice(dotIndex + 1).toLowerCase();
|
|
77
|
+
if (IMAGE_EXTENSIONS.includes(ext)) {
|
|
78
|
+
return 'image';
|
|
79
|
+
}
|
|
80
|
+
if (VIDEO_EXTENSIONS.includes(ext)) {
|
|
81
|
+
return 'video';
|
|
82
|
+
}
|
|
83
|
+
return 'file';
|
|
84
|
+
}
|
|
85
|
+
|
|
62
86
|
// Image loading complete
|
|
63
87
|
export function getImgLoad(container: any, className: string, callback: any) {
|
|
64
88
|
const images = container?.querySelectorAll(`.${className}`) || [];
|