askbot-dragon 1.7.83-beta → 1.7.85-beta

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.
Files changed (95) hide show
  1. package/README.md +27 -27
  2. package/babel.config.js +6 -6
  3. package/dragon.iml +7 -7
  4. package/package.json +60 -60
  5. package/public/index.html +73 -73
  6. package/src/App.vue +31 -31
  7. package/src/api/index.js +1 -1
  8. package/src/api/mock.http +2 -2
  9. package/src/api/requestUrl.js +185 -185
  10. package/src/assets/js/AliyunlssUtil.js +141 -141
  11. package/src/assets/js/Base64Util.js +22 -22
  12. package/src/assets/js/common.js +261 -261
  13. package/src/assets/js/hammer.js +100 -100
  14. package/src/assets/js/script.js +36 -36
  15. package/src/assets/less/common.css +6773 -6773
  16. package/src/assets/less/converSationContainer/common.less +199 -199
  17. package/src/assets/less/converSationContainer/converSatonContainer.less +493 -493
  18. package/src/assets/less/iconfont.css +37 -37
  19. package/src/assets/less/ticketMessage.less +294 -294
  20. package/src/components/ActionAlertIframe.vue +178 -178
  21. package/src/components/AiGuide.vue +434 -434
  22. package/src/components/AnswerDocknowledge.vue +1203 -1203
  23. package/src/components/AnswerVoice.vue +285 -285
  24. package/src/components/AskIFrame.vue +15 -15
  25. package/src/components/ConversationContainer.vue +10684 -10684
  26. package/src/components/FileType.vue +86 -86
  27. package/src/components/Message.vue +27 -27
  28. package/src/components/MyEditor.vue +342 -342
  29. package/src/components/QwFeedback.vue +302 -302
  30. package/src/components/actionSatisfaction.vue +107 -107
  31. package/src/components/actionSendToBot.vue +62 -62
  32. package/src/components/answerDissatisfaction.vue +62 -62
  33. package/src/components/answerRadio.vue +259 -259
  34. package/src/components/ask-components/DissatisfactionOptions.vue +57 -57
  35. package/src/components/ask-components/Msgloading.vue +37 -37
  36. package/src/components/ask-components/SatisfactionV2.vue +15 -15
  37. package/src/components/askVideo.vue +162 -162
  38. package/src/components/assetDetails.vue +378 -378
  39. package/src/components/assetMessage.vue +229 -229
  40. package/src/components/associationIntention.vue +378 -378
  41. package/src/components/attachmentPreview.vue +90 -90
  42. package/src/components/botActionSatisfactor.vue +68 -68
  43. package/src/components/chatContent.vue +513 -513
  44. package/src/components/feedBack.vue +136 -136
  45. package/src/components/fielListView.vue +351 -351
  46. package/src/components/file/AliyunOssComponents.vue +108 -108
  47. package/src/components/formTemplate.vue +3522 -3522
  48. package/src/components/imgView.vue +31 -31
  49. package/src/components/intelligentSummary.vue +234 -234
  50. package/src/components/kkview.vue +1128 -1128
  51. package/src/components/loadingProcess.vue +164 -164
  52. package/src/components/markDownText.vue +959 -960
  53. package/src/components/message/ActionAlertIframe.vue +112 -112
  54. package/src/components/message/ShopMessage.vue +164 -164
  55. package/src/components/message/TextMessage.vue +928 -928
  56. package/src/components/message/TicketMessage.vue +201 -201
  57. package/src/components/message/swiper/index.js +4 -4
  58. package/src/components/message/swiper/ticketSwiper.vue +503 -503
  59. package/src/components/message/swiper/ticketSwiperItem.vue +61 -61
  60. package/src/components/msgLoading.vue +231 -231
  61. package/src/components/myPopup.vue +73 -73
  62. package/src/components/newPdfPosition.vue +877 -877
  63. package/src/components/pagination.vue +128 -128
  64. package/src/components/pdfPosition.vue +1514 -1514
  65. package/src/components/popup.vue +228 -228
  66. package/src/components/preview/docView.vue +122 -113
  67. package/src/components/preview/excelView.vue +206 -198
  68. package/src/components/preview/newPositionPreview.vue +390 -388
  69. package/src/components/preview/pdfView.vue +831 -831
  70. package/src/components/previewDoc.vue +252 -252
  71. package/src/components/previewPdf.vue +1119 -1119
  72. package/src/components/receiverMessagePlatform.vue +69 -69
  73. package/src/components/recommend.vue +80 -80
  74. package/src/components/selector/hOption.vue +20 -20
  75. package/src/components/selector/hSelector.vue +199 -199
  76. package/src/components/selector/hWrapper.vue +216 -216
  77. package/src/components/senderMessagePlatform.vue +58 -58
  78. package/src/components/source/BotMessage.vue +24 -24
  79. package/src/components/source/CustomMessage.vue +24 -24
  80. package/src/components/test.vue +260 -260
  81. package/src/components/tree.vue +307 -307
  82. package/src/components/utils/AliyunIssUtil.js +103 -103
  83. package/src/components/utils/ckeditor.js +185 -185
  84. package/src/components/utils/format_date.js +25 -25
  85. package/src/components/utils/index.js +6 -6
  86. package/src/components/utils/math_utils.js +29 -29
  87. package/src/components/voiceComponent.vue +119 -119
  88. package/src/components/welcomeKnowledgeFile.vue +347 -347
  89. package/src/components/welcomeLlmCard.vue +144 -144
  90. package/src/components/welcomeSuggest.vue +97 -97
  91. package/src/locales/cn.json +98 -98
  92. package/src/locales/en.json +98 -98
  93. package/src/locales/jp.json +72 -72
  94. package/src/main.js +76 -76
  95. package/vue.config.js +54 -54
@@ -1,960 +1,959 @@
1
- <template>
2
- <div @click="lookImage">
3
- <div class="think" v-if="hasThinkValue">
4
- <p class="think_title" :style="{ height: showThink ? '32px' : '20px' }" @click="showThink = !showThink">
5
- <span class="think_title_left">
6
- <i class="iconfont guoran-icon-deepseek"></i>
7
- {{ $t('dragonCommon.deeplyPondered') }}
8
- <span v-if="!isHistory">({{ $t('dragonCommon.time') }} {{ filterTime(thinkTimeNum) }})</span>
9
- </span>
10
- <span class="think_title_right" :class="showThink ? '' : 'hide_think_title'">
11
- <i class="iconfont guoran-jiantouxiangshang"></i>
12
- </span>
13
- </p>
14
- <div class="think_content" :class="showThink ? '' : 'hide_think'" v-html="parseThinkVal">
15
- </div>
16
- </div>
17
- <!-- <vue-markdown
18
- class="mark_down"
19
- :source="typedContent"
20
- :ref="'markdown' + msgId"
21
- @click.native="clickMarkDown"
22
- :plugins="plugins"
23
- >
24
- </vue-markdown> -->
25
- <!-- v-show="hasContent" -->
26
- <div
27
- class="mark_down"
28
- v-html="markParseText"
29
- ref="markDown"
30
- @click="clickMarkDown">
31
- </div>
32
- <!-- <div v-show="!hasThinkValue && !hasContent" class="blinking-cursor"></div> -->
33
- <div v-if="showPreview">
34
- <img-view :url-list="imgList" @closeViewer="closeViewer"></img-view>
35
- </div>
36
- </div>
37
-
38
- </template>
39
-
40
- <script>
41
- /* eslint-disable */
42
- import VueMarkdown from 'vue-markdown';
43
- import ImgView from "@/components/imgView";
44
- // import Typed from "typed.js";
45
- import hljs from 'highlight.js';
46
- import 'highlight.js/styles/default.css';
47
- import { v4 as uuidv4 } from "uuid";
48
- const marked = require("marked")
49
- export default {
50
- name: "markDownText",
51
- data() {
52
- return {
53
- eventSource: null,
54
- plugins: [
55
-
56
- ],
57
- typedContent: "",
58
- typingSpeed: 15,
59
- showPreview: false,
60
- imgList: [],
61
- isManualScroll: false,
62
- hasThinkValue: false,
63
- chainThinkValue: "",
64
- showThink: true,
65
- streamThinkValueAddFlag: false,
66
- timer: null,
67
- generatingString: "",
68
- generatingThinkString: "",
69
- streamEnd: false,
70
- thinkCount: 0,
71
- thinkTimer: null,
72
- thinkTimeNum: 0,
73
- thinkEnd: true,
74
- signSetFlag: false,
75
- // 重试地址
76
- retryUrl: "",
77
- }
78
- },
79
- props: {
80
- chainValues: {
81
- type: String,
82
- default: ""
83
- },
84
- msgId: {
85
- type: String,
86
- default: ""
87
- },
88
- isHistory: {
89
- type: Boolean,
90
- default: false
91
- },
92
- streamRequestUrl: {
93
- type: String,
94
- default: ""
95
- },
96
- whetherRequestStream: {
97
- type: Boolean,
98
- default: false
99
- }
100
- },
101
- computed: {
102
- hasContent() {
103
- if (!this.markParseText) return false; // 如果 markParseText 为空,返回 false
104
- const tempDiv = document.createElement("div");
105
- tempDiv.innerHTML = this.markParseText;
106
- const textContent = tempDiv.textContent || tempDiv.innerText || "";
107
- return textContent.trim() !== ""; // 如果文本内容不为空,返回 true
108
- },
109
- markParseText() {
110
- const regex = /```json\n{"content":"/g
111
- const regex2 = /```json\n{\n "content": "/g
112
- let str = this.typedContent
113
- str = str.replace(regex, "")
114
- str = str.replace(regex2, "")
115
- str = str.replaceAll("\\n", "\n")
116
- // 处理特殊情况,标题前的多个换行符补充一个空行
117
- str = str.replaceAll("\n\n ###", "<p> <p/>\n ###")
118
- let markdown = marked.marked(str, {
119
- breaks: true
120
- })
121
- return this.parseCode(markdown)
122
- },
123
- parseThinkVal() {
124
- let str = this.chainThinkValue;
125
- str = str.replaceAll("\\n", "\n")
126
- str = str.replaceAll("\n", "<br>")
127
- return this.parseCode(str)
128
- }
129
- },
130
- components: {
131
- ImgView,
132
- VueMarkdown
133
- },
134
- mounted() {
135
- this.$nextTick(() => {
136
- console.log("this.askMassageStream: ", this.askMassageStream);
137
- console.log("this.whetherRequestStream: ", this.whetherRequestStream);
138
- console.log("this.streamRequestUrl: ", this.streamRequestUrl);
139
- console.log("this.isHistory: ", this.isHistory);
140
- if (this.isHistory) {
141
- if (this.whetherRequestStream) {
142
- console.log("71 新版 真 流 打字 效果");
143
- window.addEventListener('wheel', this.handleScroll, true);
144
- this.streamInfo(this.streamRequestUrl);
145
- } else {
146
- this.typedContent = this.filterThinkVal(this.chainValues);
147
- this.$nextTick(() =>{
148
- this.appendCopyBtn()
149
- })
150
- }
151
- } else {
152
- console.log("78 pre 新版 真 流 打字 效果");
153
- // 新版 打字 效果
154
- if (this.askMassageStream && this.whetherRequestStream && this.streamRequestUrl) {
155
- console.log("81 新版 真 流 打字 效果");
156
- window.addEventListener('wheel', this.handleScroll, true);
157
- this.streamInfo(this.streamRequestUrl);
158
- } else {
159
- console.log("markdowncopy 旧版 模拟 流 打字 效果 sseOtherInfo");
160
- this.$emit("sseOtherInfo", this.msgId, "start", "");
161
- this.$emit("sseOtherInfo", this.msgId, "hideloading", "");
162
- // 旧版 模拟 流 打字 效果
163
- this.typedContent = this.filterThinkVal(this.chainValues);
164
- this.$nextTick(() =>{
165
- this.appendCopyBtn()
166
- })
167
- }
168
- }
169
- })
170
- },
171
- methods: {
172
- closeMessageStream() {
173
- this.eventSource.close();
174
- setTimeout(() => {
175
- let str = this.typedContent + this.generatingString
176
- this.$set(this, "typedContent", str)
177
- this.generatingString = "";
178
- let str2 = this.filterText(this.chainThinkValue + this.generatingThinkString)
179
- this.$set(this, "chainThinkValue", str2)
180
- this.generatingThinkString = "";
181
- this.closeTimeOut()
182
- clearInterval(this.thinkTimer)
183
- this.thinkTimer = null
184
- this.thinkEnd = true
185
- setTimeout(() => {
186
- this.removeLoadingNode()
187
- }, 200)
188
- this.$emit("streamCallback", this.typedContent)
189
- this.$emit("sseOtherInfo", this.msgId, "start", "");
190
- }, 200);
191
- },
192
- filterTime(time) {
193
- if (time < 60) {
194
- return time + " 秒";
195
- } else {
196
- return Math.floor(time / 60) + " 分 " + time % 60 + " 秒";
197
- }
198
- },
199
- // 处理历史记录中的 <think> 标签
200
- filterThinkVal(text) {
201
- // 用于匹配 <think> 标签的正则表达式
202
- this.hasThinkValue = false;
203
- this.chainThinkValue = "";
204
- const thinkStartRegex = /<think>/;
205
- // const thinkEndRegex = /<\/think>/;
206
- const thinkEndRegex = /<\/think>/g;
207
- // 查找第一个 <think> 标签的位置
208
- const startIndex = text.search(thinkStartRegex);
209
- if (startIndex === -1) {
210
- // 如果没有找到 <think> 标签,直接返回原始文本和空内容
211
- return text.replaceAll("\\n", "\n")
212
- }
213
- // 从第一个 <think> 标签之后开始查找最后一个 </think> 标签
214
- let endIndex = -1;
215
- let match;
216
- // 循环查找所有匹配项
217
- while ((match = thinkEndRegex.exec(text)) !== null) {
218
- endIndex = match.index;
219
- }
220
- if (endIndex !== -1) {
221
- console.log(`最后一个 </think> 的起始位置是: ${endIndex}`);
222
- // 提取 <think> 标签中的内容
223
- let thinkVal = text.slice(startIndex + '<think>'.length, endIndex);
224
- // 从原始文本中剔除 <think> 标签及其中的内容
225
- let typedContent = text.slice(0, startIndex) + text.slice(endIndex);
226
- this.hasThinkValue = true;
227
- this.chainThinkValue = thinkVal.replaceAll("<think>", "").replaceAll("</think>", "<br/>");
228
- this.chainThinkValue = this.filterText(this.chainThinkValue)
229
- return typedContent.replaceAll("\\n", "\n");
230
- } else {
231
- // text.replaceAll("\\n", "\n")
232
- this.hasThinkValue = true;
233
- this.chainThinkValue = text.replaceAll("<think>", "").replaceAll("</think>", "<br/>");
234
- return ""
235
- }
236
- },
237
- filterText(str) {
238
- /* eslint-disable */
239
- const regex = /\{\n "functionCall": \{/s
240
- str = str.replace(regex, "")
241
- str = str.replaceAll("```json", "")
242
- return str
243
- },
244
- // 处理流式消息中的 <think> 标签
245
- streamFilterThinkValue() {
246
-
247
- },
248
- // 打字效果已在MarkdownText文件中处理
249
- typeNewWriter(text, keyId, msg) {
250
- // setTimeout(() => {
251
- // let ref = 'msgTyped' + keyId;
252
- // const el = this.$refs[ref][0];
253
- // new Typed(el, {
254
- // strings: [text],
255
- // typeSpeed: 15,
256
- // showCursor: false,
257
- // onComplete: () => {
258
- // msg.isHistory = true
259
- // this.$emit('openFirstPreview', msg)
260
- // this.typingSuccess()
261
- // },
262
- // })
263
- // }, 500)
264
- },
265
- handleScroll() {
266
- console.log("this.isManualScroll = true;");
267
- this.isManualScroll = true;
268
- window.removeEventListener('wheel', this.handleScroll, true);
269
- },
270
- scrollMarkdown() {
271
- !this.isManualScroll && this.$nextTick(() => {
272
- let container = document.getElementById("list");
273
- if (container) {
274
- let scrollHeight = container.scrollHeight;
275
- container.scrollTop = scrollHeight;
276
- }
277
- });
278
- },
279
- streamInfo(url) {
280
- // 通知消息列表,当前还未建立sse,先不出空消息
281
- if(this.retryUrl == ""){
282
- this.$emit("sseOtherInfo", this.msgId, "init", "");
283
- }
284
- this.$emit("answerDocKnowledgeFn");
285
- this.eventSource = new EventSource(url);
286
- this.$emit("streamStatus", true, this.msgId);
287
- // this.$set(this, 'showThink', true);
288
- this.signSetFlag = false;
289
- this.thinkEnd = false
290
- let tempString = "";
291
- // let generatingString = "";
292
- // let timer = null;
293
- let images = [];
294
- let matched = [];
295
- let recommendItems = [];
296
- let isFirstStr = true;
297
- const backslashOrNRegex = /[\\n]/; // 不能替换为 /\n/,会导致格式解析错误,在处理方法末尾做了修改
298
- // 如果只有一次推送、并且推送字符串中带有 n
299
- let isOnlyOnce = 0; // 标记是否只有一次推送
300
- let hasN = false; // 标记是否有 n
301
- const thinkStartRegex = /<think>/;
302
- const thinkEndRegex = /<\/think>/
303
- this.eventSource.onmessage = (event) => {
304
- const data = JSON.parse(event.data);
305
- if (event.lastEventId === 'finish') { // 不能删除 finish
306
- console.log("finish+++++");
307
- } else if (event.lastEventId === '[DONE]') {
308
- this.streamEnd = true;
309
- // 只有一次推送,且推送字符串中带有 n
310
- // 用于特殊情况(只有一次内容推送 含有 n)
311
- if (isOnlyOnce == 1 && hasN) {
312
- this.$emit("answerDocKnowledgeFn");
313
- let repStr = tempString.replace(/\\n/g, '\n');
314
- this.$set(this, "typedContent", repStr);
315
- this.scrollMarkdown();
316
- }
317
- let DONETmier = setInterval(() => {
318
- // 在所有文本内容打字效果完毕后 展示底部图片 文件 推荐等
319
- if (this.generatingString == "") {
320
- if (images.length != 0) {
321
- setTimeout(() => {
322
- this.$emit("sseOtherInfo", this.msgId, "images", images);
323
- this.scrollMarkdown();
324
- }, 50);
325
- }
326
- if (matched.length != 0) {
327
- setTimeout(() => {
328
- this.$emit("sseOtherInfo", this.msgId, "matched", matched);
329
- this.scrollMarkdown();
330
- }, 100);
331
- }
332
- if (recommendItems.length != 0) {
333
- setTimeout(() => {
334
- this.$emit("sseOtherInfo", this.msgId, "recommendItems", recommendItems);
335
- this.scrollMarkdown();
336
- }, 150);
337
- }
338
- this.closeTimeOut()
339
- clearInterval(DONETmier);
340
- if(this.thinkTimer != null) {
341
- clearInterval(this.thinkTimer)
342
- this.thinkTimer = null
343
- this.thinkEnd = true
344
- }
345
- this.$emit("streamCallback", this.typedContent)
346
- this.$emit("streamStatus", false, this.msgId);
347
- this.appendCopyBtn()
348
- setTimeout(() => {
349
- this.removeLoadingNode()
350
- }, 200);
351
- this.$emit("sseOtherInfo", this.msgId, "start", "");
352
- }
353
-
354
- }, 100);
355
- this.eventSource.close();
356
- // 重试地址不为空,说明需要重试
357
- console.log("需要重试 this.retryUrl: ", this.retryUrl);
358
- if (this.retryUrl != "") {
359
- console.log("this.retryUrl: ", this.retryUrl);
360
- this.streamInfo(this.retryUrl);
361
- // 将重试地址清空,用于在收到结束标识并结束后 判断是否需要重试
362
- this.retryUrl = "";
363
- this.$emit("sseOtherInfo", this.msgId, "changeHeaderStyleValue", this.$t('common.modelgeneration'));
364
- }
365
- } else if (event.lastEventId === 'matched') {
366
- matched = data;
367
- } else if (event.lastEventId === 'images') {
368
- images = data;
369
- } else if (event.lastEventId === 'recommendItems') {
370
- recommendItems = data;
371
- } else if (event.lastEventId === 'retry') {
372
- this.retryUrl = data.content;
373
- } else if (event.lastEventId === 'generated_image') {
374
- let imgs = JSON.parse(data.content);
375
- let imgsStr = "";
376
- for (let i = 0; i < imgs.length; i++) {
377
- imgsStr += `![](${imgs[i].oss_url})`;
378
- }
379
- this.$set(this, "typedContent", this.typedContent + imgsStr);
380
-
381
- // 模拟图片推送
382
- // let imgs = JSON.parse('[{"oss_url":"https://guoranopen-zjk.oss-cn-zhangjiakou.aliyuncs.com/generated-img/generated_img_865652a94b374337b2ac703466cdea94.png"}]');
383
- // let imgsStr = "";
384
- // for (let i = 0; i < imgs.length; i++) {
385
- // imgsStr += `![](${imgs[i].oss_url})`;
386
- // }
387
- // this.$set(this, "typedContent", this.typedContent + imgsStr);
388
-
389
- } else if (event.lastEventId === 'source') {
390
- this.$emit("changedHeadStatusValue", this.msgId, data);
391
- } else {
392
- // console.log("start event.lastEventId: ", event.lastEventId);
393
- // 设置当前知识答案id,用于loading取消和展示
394
- this.$emit("sseOtherInfo", this.msgId, "start", "");
395
- if(thinkStartRegex.test(data.content) && thinkEndRegex.test(data.content)) {
396
- this.hasThinkValue = true;
397
- this.thinkCount = 0
398
- this.ThinkTime()
399
- } else {
400
- // 最终根据规则是以最后一个 </think> 标识当前深度思考结束,开始正常添加数据到 typedContent
401
- if (thinkStartRegex.test(data.content)) {
402
- if (this.thinkCount == 0) {
403
- this.thinkCount = 0
404
- } else {
405
- this.thinkCount += 1;
406
- }
407
- this.hasThinkValue = true;
408
- this.ThinkTime()
409
- // this.streamThinkValueAddFlag = true;
410
- }
411
- if (thinkEndRegex.test(data.content)) {
412
- this.thinkCount -= 1;
413
- // this.streamThinkValueAddFlag = false;
414
- }
415
- }
416
- // 字符串中存在特定字符,本次接收消息暂不展示
417
- if (backslashOrNRegex.test(data.content) && !this.hasThinkValue) {
418
- tempString += data.content;
419
- // 用于特殊情况(只有一次内容推送 且 含有 n)
420
- isOnlyOnce++;
421
- hasN = true;
422
- } else {
423
- if (data.content.trim() != "") {
424
- isOnlyOnce++;
425
- }
426
- // 此处判断的逻辑是识别到了深度思考 结束,开始处理回复的内容数据,关闭掉深度思考的手动计时,并收起深度思考的内容
427
- let repStr = tempString.replace(/\\n/g, '\n');
428
- if (this.hasThinkValue && this.thinkCount == 0) {
429
- this.generatingThinkString = this.generatingThinkString + repStr + data.content;
430
- this.thinkEnd = false
431
- this.ThinkTime()
432
- } else {
433
- let str = data.content
434
- if (thinkEndRegex.test(data.content)) {
435
- let arr = data.content.split("</think>")
436
- this.generatingThinkString = this.generatingThinkString + arr[0]
437
- str = arr[1]
438
- }
439
- this.generatingString = this.generatingString + repStr + str;
440
- }
441
- tempString = "";
442
- if (isFirstStr) {
443
- isFirstStr = false;
444
- this.timer = 0;
445
- this.openTimeOutSetVal()
446
- }
447
- }
448
- }
449
- };
450
- this.eventSource.onerror = (err) => {
451
- this.typedContent += `\n${this.$t("dragonCommon.eventSourceError")}\n`;
452
- this.closeTimeOut()
453
- this.$emit("streamStatus", false, this.msgId);
454
- this.$emit("streamErrorCallBack", this.msgId);
455
- this.$emit("streamCallback", this.typedContent)
456
- if(this.eventSource) {
457
- try {
458
- this.eventSource.close();
459
- } catch (e) {
460
- console.error('EventSource close failed:', e);
461
- }
462
- }
463
- setTimeout(() => {
464
- this.removeLoadingNode()
465
- }, 200);
466
- };
467
- },
468
- openTimeOutSetVal() {
469
- if (this.timer != null) {
470
- this.timer = setTimeout(() => {
471
- this.closeThinkContent(this)
472
- // 先把深度思考的值 push 完成后再执行正常内容的 push,最终以正常内容的值为空判断为本轮打字效果结束
473
- if (this.generatingThinkString != "") {
474
- let str = this.filterText(this.chainThinkValue + this.generatingThinkString.charAt(0))
475
- this.$set(this, "chainThinkValue", str)
476
- this.generatingThinkString = this.generatingThinkString.slice(1)
477
- this.scrollMarkdown();
478
- } else if (this.generatingString != "") {
479
- this.$set(this, "typedContent", this.typedContent + this.generatingString.charAt(0))
480
- this.generatingString = this.generatingString.slice(1)
481
- this.scrollMarkdown();
482
- if(this.thinkTimer != null) {
483
- clearInterval(this.thinkTimer)
484
- this.thinkTimer = null
485
- this.thinkEnd = true
486
- }
487
- }
488
- this.openTimeOutSetVal()
489
- }, 20);
490
- }
491
- },
492
- closeTimeOut() {
493
- clearTimeout(this.timer);
494
- this.timer = null;
495
- },
496
- ThinkTime() {
497
- if(this.thinkTimer == null) {
498
- this.thinkTimer = setInterval(() => {
499
- this.thinkTimeNum += 1;
500
- }, 1000)
501
- }
502
- },
503
- startTypingEffect() {
504
- // 使用正则表达式将 HTML 内容按 <table> 标签分割
505
- const segments = this.chainValues.split(/(<\/?table[\s\S]*?>[\s\S]*?<\/table>)/i);
506
- console.log("segments: ", segments);
507
-
508
- let segmentIndex = 0;
509
- let i = 0;
510
- const processNextSegment = () => {
511
- if (segmentIndex >= segments.length) {
512
- this.$emit('openFirstPreview')
513
- this.$emit('typingSuccess');
514
- return;
515
- }
516
- const segment = segments[segmentIndex];
517
- if (/^<\/?table[\s\S]*?>[\s\S]*?<\/table>$/i.test(segment)) {
518
- // 如果当前段是 <table> 标签,直接添加到 typedContent 中
519
- this.typedContent += segment;
520
- segmentIndex++;
521
- processNextSegment();
522
- } else {
523
- i = 0;
524
- const updateContent = () => {
525
- if (i < segment.length) {
526
- this.typedContent += segment.charAt(i);
527
- i++;
528
- // 使用 requestAnimationFrame 替代 setInterval,防止卡顿
529
- requestAnimationFrame(updateContent);
530
- } else {
531
- segmentIndex++;
532
- processNextSegment();
533
- }
534
- };
535
- updateContent();
536
- }
537
- };
538
- processNextSegment();
539
- },
540
-
541
- lookImage(e) {
542
- let previewImageUrl = ""
543
- console.log('e.target', e.target)
544
- if (e.target.localName == 'img') {
545
- previewImageUrl = e.target.currentSrc;
546
- this.showPreview = true;
547
- let richtext = JSON.parse(JSON.stringify(this.typedContent))
548
- this.imgList = [];
549
- richtext.replace(/<img [^>]*src=['"]([^'"]+)[^>]*>/g, (match, capture) => {
550
- this.imgList.push(capture);
551
- });
552
- /*当前点击的图片作为第一个图片*/
553
- let index = this.imgList.indexOf(previewImageUrl);
554
- this.imgList.splice(index, 1);
555
- this.imgList.unshift(previewImageUrl);
556
- }
557
-
558
- },
559
- closeViewer() {
560
- this.showPreview = false;
561
- },
562
- clickMarkDown(e) {
563
- if (e.target.tagName == "A") {
564
- window.open(e.target.href, "_blank")
565
- e.stopPropagation()
566
- e.preventDefault()
567
- return false
568
- }
569
- },
570
- closeThinkContent: _.debounce(function (that) {
571
- if (that.thinkEnd && that.generatingThinkString == "" && !that.signSetFlag) {
572
- that.showThink = false;
573
- // 标识已经自动关闭过一次深度思考内容,不再影响手动打开的效果
574
- that.signSetFlag = true;
575
- }
576
- }, 500),
577
- parseCode(markDownText) {
578
- const parser = new DOMParser();
579
- const doc = parser.parseFromString(markDownText, 'text/html');
580
- doc.querySelectorAll('pre code').forEach((block) => {
581
- hljs.highlightBlock(block);
582
- });
583
- doc.querySelectorAll('a').forEach((block) => {
584
- block.setAttribute('target', '_blank');
585
- block.setAttribute('rel', 'noopener noreferrer');
586
- });
587
- let node = this.getLastNode(doc)
588
- if (node && !this.isHistory) {
589
- let loading = document.createElement('span')
590
- loading.classList.add("loading-circle")
591
- loading.id = "loading-circle"
592
- this.removeLoadingNode()
593
- node.appendChild(loading)
594
- }
595
- return doc.documentElement.outerHTML
596
- },
597
- removeLoadingNode() {
598
- let old = document.getElementById("loading-circle")
599
- if(old) old.remove()
600
- },
601
- getLastNode(node) {
602
- let filterList = ["STRONG", "BR", "SPAN", "I", "EM", "B", "A"]
603
- if(node.lastElementChild && !filterList.includes(node.lastElementChild.tagName)) {
604
- return this.getLastNode(node.lastElementChild)
605
- } else {
606
- return node
607
- }
608
- },
609
- appendCopyBtn() {
610
- let doc = this.$refs.markDown.$el ? this.$refs.markDown.$el : this.$refs.markDown
611
- let codeList = doc.querySelectorAll('pre code')
612
- codeList.forEach(ele =>{
613
- let list = ele.getElementsByClassName("code_copy_btn")
614
- if(list.length == 0 && ele.classList.contains("hljs") && !ele.classList.contains("hljs-undefined")) {
615
- let text = ele.innerText
616
- let btnId = uuidv4();
617
- let copy = document.createElement('div')
618
- copy.classList.add("code_copy_btn")
619
- copy.setAttribute('id', btnId)
620
- copy.style.display = 'flex'
621
- let i = document.createElement('i')
622
- i.classList.add("iconfont")
623
- i.classList.add("guoran-tongyichicun-fuzhi")
624
- let span = document.createElement('span')
625
- span.innerText = this.$t("dragonCommon.copy")
626
- copy.append(i)
627
- copy.append(span)
628
- copy.onclick = () =>{
629
- let fn = () =>{
630
- let id = copy.getAttribute("id")
631
- copy.style.display = "none"
632
- let cs = document.getElementById(id + "success")
633
- if(cs) {
634
- cs.style.display = "flex"
635
- }
636
- setTimeout(() =>{
637
- cs && (cs.style.display = "none")
638
- copy.style.display = "flex"
639
- }, 2000)
640
- }
641
- if (navigator.clipboard) {
642
- // 使用 Clipboard API 进行复制
643
- navigator.clipboard.writeText(text).then(() => { fn()})
644
- .catch((error) => {
645
- // 如果 Clipboard API 失败,尝试使用传统方法
646
- this.fallbackCopyTextToClipboard(text, fn, () =>{
647
- this.$message.error(this.$t("dragonCommon.copyError"))
648
- });
649
- });
650
- } else {
651
- this.fallbackCopyTextToClipboard(text, fn, () =>{
652
- this.$message.error(this.$t("dragonCommon.copyError"))
653
- });
654
- }
655
- }
656
- ele.append(copy)
657
- let copySuccess = document.createElement('div')
658
- copySuccess.classList.add("code_copy_btn")
659
- copySuccess.setAttribute('id', btnId + 'success')
660
- copySuccess.style.display = 'none'
661
- let csI = document.createElement('i')
662
- csI.classList.add("el-icon-check")
663
- let csSpan = document.createElement('span')
664
- csSpan.innerText = this.$t("dragonCommon.copySuccess")
665
- copySuccess.append(csI)
666
- copySuccess.append(csSpan)
667
- ele.append(copySuccess)
668
- }
669
- })
670
- },
671
- fallbackCopyTextToClipboard(text, success, fail) {
672
- // 创建一个临时的 input 元素
673
- const textArea = document.createElement('textarea');
674
- textArea.value = text;
675
- // 使 textarea 不在屏幕上显示
676
- textArea.style.position = 'fixed';
677
- textArea.style.top = 0;
678
- textArea.style.left = 0;
679
- textArea.style.width = '2em';
680
- textArea.style.height = '2em';
681
- textArea.style.padding = 0;
682
- textArea.style.border = 'none';
683
- textArea.style.outline = 'none';
684
- textArea.style.boxShadow = 'none';
685
- textArea.style.background = 'transparent';
686
-
687
- // 将 textarea 添加到文档中
688
- document.body.appendChild(textArea);
689
- textArea.focus();
690
- textArea.select();
691
-
692
- try {
693
- // 执行复制命令
694
- const successful = document.execCommand('copy');
695
- if (successful) {
696
- success()
697
- } else {
698
- fail()
699
- }
700
- } catch (err) {
701
- fail()
702
- }
703
- // 移除临时的 textarea
704
- document.body.removeChild(textArea);
705
- },
706
- },
707
- beforeDestroy() {
708
- if (this.eventSource) {
709
- this.eventSource.close();
710
- }
711
- }
712
- };
713
- </script>
714
-
715
- <style scoped lang="less">
716
- .blinking-cursor {
717
- display: inline-block;
718
- width: 2px;
719
- height: 1em;
720
- background-color: black;
721
- animation: blink 1s steps(2, start) infinite;
722
- }
723
-
724
- @keyframes blink {
725
- 0%, 100% {
726
- opacity: 1;
727
- }
728
- 50% {
729
- opacity: 0;
730
- }
731
- }
732
- .think {
733
- padding: 10px;
734
- background: #f2f5ff;
735
- border-radius: 8px;
736
- margin-bottom: 12px;
737
-
738
- .think_content {
739
- height: auto;
740
- white-space: pre-wrap;
741
- transition: all 2s ease;
742
-
743
- /deep/ br {
744
- line-height: 10px !important;
745
- }
746
- }
747
-
748
- .hide_think {
749
- height: 0px;
750
- overflow: hidden;
751
- transition: all 2s ease;
752
- }
753
-
754
- .think_title {
755
- width: 100%;
756
- height: 20px;
757
- display: flex;
758
- align-items: center;
759
- justify-content: space-between;
760
- cursor: pointer;
761
-
762
- .think_title_left {
763
- display: flex;
764
- align-items: center;
765
-
766
- i {
767
- font-size: 18px;
768
- }
769
-
770
- .guoran-icon-deepseek {
771
- font-size: 18px;
772
- }
773
- }
774
-
775
- .think_title_right {
776
- font-size: 18px;
777
- color: #366aff;
778
- }
779
-
780
- .hide_think_title {
781
- transform: rotate(180deg);
782
- }
783
- }
784
- }
785
-
786
- .mark_down {
787
- line-height: 24px;
788
- overflow-x: auto;
789
-
790
- /deep/code {
791
- font-family: Microsoft Yahei, Helvetica Neue, Helvetica, Arial, sans-serif !important;
792
- white-space: pre-wrap;
793
- font-size: 16px;
794
- position: relative;
795
- .code_copy_btn {
796
- position: absolute;
797
- right: 10px;
798
- top: 10px;
799
- display: flex;
800
- align-items: center;
801
- padding: 3px 12px;
802
- background: #EEF1FF;
803
- cursor: pointer;
804
- font-size: 14px;
805
- border-radius: 25px;
806
- i {
807
- font-size: 14px;
808
- margin-right: 6px;
809
- }
810
- }
811
- }
812
-
813
- /deep/ pre {
814
- // code {
815
- // background: #F3F3F3 !important;
816
- // color: #444 !important;
817
- // }
818
- .language-undefined {
819
- background: #FFFFFF;
820
- color: #000;
821
- }
822
- }
823
-
824
- /deep/p {
825
- margin-bottom: 7px;
826
- font-size: 16px;
827
- }
828
-
829
- /deep/p:only-child {
830
- margin: 0 !important;
831
- }
832
-
833
- /deep/p:last-child {
834
- margin-bottom: 0 !important;
835
- }
836
-
837
- /deep/ul {
838
- margin-bottom: 7px;
839
- list-style: disc;
840
- padding-left: 40px;
841
-
842
- li {
843
- margin: 7px 0 !important;
844
- font-size: 16px;
845
- // padding-left: 40px;
846
- }
847
- }
848
-
849
- /deep/ ol {
850
- list-style: auto;
851
- padding-left: 40px;
852
-
853
- li {
854
- margin: 7px 0 !important;
855
- font-size: 16px;
856
- // padding-left: 40px;
857
- }
858
- }
859
-
860
- /deep/img {
861
- max-width: 400px;
862
- }
863
-
864
- /deep/h3,
865
- /deep/h2,
866
- /deep/h1,
867
- /deep/h4,
868
- /deep/h5,
869
- /deep/h6 {
870
- color: #000000;
871
- font-weight: 500;
872
- line-height: 26px;
873
- margin: 7px 0;
874
- }
875
-
876
- /deep/h6 {
877
- font-size: 12px;
878
- }
879
-
880
- /deep/h5 {
881
- font-size: 14px;
882
- }
883
-
884
- /deep/h4 {
885
- font-size: 16px;
886
- }
887
-
888
- /deep/h3 {
889
- font-size: 20px;
890
- }
891
-
892
- /deep/h2 {
893
- font-size: 22px;
894
- }
895
-
896
- /deep/h1 {
897
- font-size: 24px;
898
- }
899
-
900
- // /deep/h3 {
901
- // font-size: 20px;
902
- // }
903
-
904
- /deep/hr {
905
- margin: 16px 0;
906
- }
907
-
908
- /deep/ table {
909
- border-spacing: 0;
910
- // border-radius: 8px;
911
- // overflow: hidden;
912
- border: solid 1px #e0e6f7;
913
- min-width: 800px;
914
- overflow-x: auto;
915
- margin: 16px 0;
916
- }
917
-
918
- /deep/ th {
919
- background: #EEF1FF;
920
- border: solid 1px #e0e6f7;
921
- padding: 8px;
922
- height: 38px;
923
- min-width: 100px;
924
- }
925
-
926
- /deep/ td {
927
- border: solid 1px #e0e6f7;
928
- padding: 8px;
929
- height: 28px;
930
- min-width: 100px;
931
- }
932
- }
933
-
934
- /* 圆圈的样式 */
935
- /deep/.loading-circle {
936
- width: 8px;
937
- height: 8px;
938
- display: inline-block;
939
- border-radius: 50%;
940
- background-color: #366aff;
941
- animation: loading 2s infinite ease-in-out;
942
- margin: 0 8px;
943
- /* 动画控制 */
944
- }
945
-
946
- /* 放大缩小的动画 */
947
- @keyframes loading {
948
-
949
- 0%,
950
- 100% {
951
- transform: scale(1);
952
- /* 原始大小 */
953
- }
954
-
955
- 50% {
956
- transform: scale(1.5);
957
- /* 放大 */
958
- }
959
- }
960
- </style>
1
+ <template>
2
+ <div @click="lookImage">
3
+ <div class="think" v-if="hasThinkValue">
4
+ <p class="think_title" :style="{ height: showThink ? '32px' : '20px' }" @click="showThink = !showThink">
5
+ <span class="think_title_left">
6
+ <i class="iconfont guoran-icon-deepseek"></i>
7
+ {{ $t('dragonCommon.deeplyPondered') }}
8
+ <span v-if="!isHistory">({{ $t('dragonCommon.time') }} {{ filterTime(thinkTimeNum) }})</span>
9
+ </span>
10
+ <span class="think_title_right" :class="showThink ? '' : 'hide_think_title'">
11
+ <i class="iconfont guoran-jiantouxiangshang"></i>
12
+ </span>
13
+ </p>
14
+ <div class="think_content" :class="showThink ? '' : 'hide_think'" v-html="parseThinkVal">
15
+ </div>
16
+ </div>
17
+ <!-- <vue-markdown
18
+ class="mark_down"
19
+ :source="typedContent"
20
+ :ref="'markdown' + msgId"
21
+ @click.native="clickMarkDown"
22
+ :plugins="plugins"
23
+ >
24
+ </vue-markdown> -->
25
+ <!-- v-show="hasContent" -->
26
+ <div
27
+ class="mark_down"
28
+ v-html="markParseText"
29
+ ref="markDown"
30
+ @click="clickMarkDown">
31
+ </div>
32
+ <!-- <div v-show="!hasThinkValue && !hasContent" class="blinking-cursor"></div> -->
33
+ <div v-if="showPreview">
34
+ <img-view :url-list="imgList" @closeViewer="closeViewer"></img-view>
35
+ </div>
36
+ </div>
37
+
38
+ </template>
39
+
40
+ <script>
41
+ /* eslint-disable */
42
+ // import VueMarkdown from 'vue-markdown';
43
+ import ImgView from "./imgView.vue";
44
+ // import Typed from "typed.js";
45
+ import hljs from 'highlight.js';
46
+ import 'highlight.js/styles/default.css';
47
+ import { v4 as uuidv4 } from "uuid";
48
+ const marked = require("marked")
49
+ export default {
50
+ name: "markDownText",
51
+ data() {
52
+ return {
53
+ eventSource: null,
54
+ plugins: [
55
+
56
+ ],
57
+ typedContent: "",
58
+ typingSpeed: 15,
59
+ showPreview: false,
60
+ imgList: [],
61
+ isManualScroll: false,
62
+ hasThinkValue: false,
63
+ chainThinkValue: "",
64
+ showThink: true,
65
+ streamThinkValueAddFlag: false,
66
+ timer: null,
67
+ generatingString: "",
68
+ generatingThinkString: "",
69
+ streamEnd: false,
70
+ thinkCount: 0,
71
+ thinkTimer: null,
72
+ thinkTimeNum: 0,
73
+ thinkEnd: true,
74
+ signSetFlag: false,
75
+ // 重试地址
76
+ retryUrl: "",
77
+ }
78
+ },
79
+ props: {
80
+ chainValues: {
81
+ type: String,
82
+ default: ""
83
+ },
84
+ msgId: {
85
+ type: String,
86
+ default: ""
87
+ },
88
+ isHistory: {
89
+ type: Boolean,
90
+ default: false
91
+ },
92
+ streamRequestUrl: {
93
+ type: String,
94
+ default: ""
95
+ },
96
+ whetherRequestStream: {
97
+ type: Boolean,
98
+ default: false
99
+ }
100
+ },
101
+ computed: {
102
+ hasContent() {
103
+ if (!this.markParseText) return false; // 如果 markParseText 为空,返回 false
104
+ const tempDiv = document.createElement("div");
105
+ tempDiv.innerHTML = this.markParseText;
106
+ const textContent = tempDiv.textContent || tempDiv.innerText || "";
107
+ return textContent.trim() !== ""; // 如果文本内容不为空,返回 true
108
+ },
109
+ markParseText() {
110
+ const regex = /```json\n{"content":"/g
111
+ const regex2 = /```json\n{\n "content": "/g
112
+ let str = this.typedContent
113
+ str = str.replace(regex, "")
114
+ str = str.replace(regex2, "")
115
+ str = str.replaceAll("\\n", "\n")
116
+ // 处理特殊情况,标题前的多个换行符补充一个空行
117
+ str = str.replaceAll("\n\n ###", "<p> <p/>\n ###")
118
+ let markdown = marked.marked(str, {
119
+ breaks: true
120
+ })
121
+ return this.parseCode(markdown)
122
+ },
123
+ parseThinkVal() {
124
+ let str = this.chainThinkValue;
125
+ str = str.replaceAll("\\n", "\n")
126
+ str = str.replaceAll("\n", "<br>")
127
+ return this.parseCode(str)
128
+ }
129
+ },
130
+ components: {
131
+ ImgView
132
+ },
133
+ mounted() {
134
+ this.$nextTick(() => {
135
+ console.log("this.askMassageStream: ", this.askMassageStream);
136
+ console.log("this.whetherRequestStream: ", this.whetherRequestStream);
137
+ console.log("this.streamRequestUrl: ", this.streamRequestUrl);
138
+ console.log("this.isHistory: ", this.isHistory);
139
+ if (this.isHistory) {
140
+ if (this.whetherRequestStream) {
141
+ console.log("71 新版 真 流 打字 效果");
142
+ window.addEventListener('wheel', this.handleScroll, true);
143
+ this.streamInfo(this.streamRequestUrl);
144
+ } else {
145
+ this.typedContent = this.filterThinkVal(this.chainValues);
146
+ this.$nextTick(() =>{
147
+ this.appendCopyBtn()
148
+ })
149
+ }
150
+ } else {
151
+ console.log("78 pre 新版 真 流 打字 效果");
152
+ // 新版 真 流 打字 效果
153
+ if (this.askMassageStream && this.whetherRequestStream && this.streamRequestUrl) {
154
+ console.log("81 新版 打字 效果");
155
+ window.addEventListener('wheel', this.handleScroll, true);
156
+ this.streamInfo(this.streamRequestUrl);
157
+ } else {
158
+ console.log("markdowncopy 旧版 模拟 流 打字 效果 sseOtherInfo");
159
+ this.$emit("sseOtherInfo", this.msgId, "start", "");
160
+ this.$emit("sseOtherInfo", this.msgId, "hideloading", "");
161
+ // 旧版 模拟 流 打字 效果
162
+ this.typedContent = this.filterThinkVal(this.chainValues);
163
+ this.$nextTick(() =>{
164
+ this.appendCopyBtn()
165
+ })
166
+ }
167
+ }
168
+ })
169
+ },
170
+ methods: {
171
+ closeMessageStream() {
172
+ this.eventSource.close();
173
+ setTimeout(() => {
174
+ let str = this.typedContent + this.generatingString
175
+ this.$set(this, "typedContent", str)
176
+ this.generatingString = "";
177
+ let str2 = this.filterText(this.chainThinkValue + this.generatingThinkString)
178
+ this.$set(this, "chainThinkValue", str2)
179
+ this.generatingThinkString = "";
180
+ this.closeTimeOut()
181
+ clearInterval(this.thinkTimer)
182
+ this.thinkTimer = null
183
+ this.thinkEnd = true
184
+ setTimeout(() => {
185
+ this.removeLoadingNode()
186
+ }, 200)
187
+ this.$emit("streamCallback", this.typedContent)
188
+ this.$emit("sseOtherInfo", this.msgId, "start", "");
189
+ }, 200);
190
+ },
191
+ filterTime(time) {
192
+ if (time < 60) {
193
+ return time + " 秒";
194
+ } else {
195
+ return Math.floor(time / 60) + " 分 " + time % 60 + " 秒";
196
+ }
197
+ },
198
+ // 处理历史记录中的 <think> 标签
199
+ filterThinkVal(text) {
200
+ // 用于匹配 <think> 标签的正则表达式
201
+ this.hasThinkValue = false;
202
+ this.chainThinkValue = "";
203
+ const thinkStartRegex = /<think>/;
204
+ // const thinkEndRegex = /<\/think>/;
205
+ const thinkEndRegex = /<\/think>/g;
206
+ // 查找第一个 <think> 标签的位置
207
+ const startIndex = text.search(thinkStartRegex);
208
+ if (startIndex === -1) {
209
+ // 如果没有找到 <think> 标签,直接返回原始文本和空内容
210
+ return text.replaceAll("\\n", "\n")
211
+ }
212
+ // 从第一个 <think> 标签之后开始查找最后一个 </think> 标签
213
+ let endIndex = -1;
214
+ let match;
215
+ // 循环查找所有匹配项
216
+ while ((match = thinkEndRegex.exec(text)) !== null) {
217
+ endIndex = match.index;
218
+ }
219
+ if (endIndex !== -1) {
220
+ console.log(`最后一个 </think> 的起始位置是: ${endIndex}`);
221
+ // 提取 <think> 标签中的内容
222
+ let thinkVal = text.slice(startIndex + '<think>'.length, endIndex);
223
+ // 从原始文本中剔除 <think> 标签及其中的内容
224
+ let typedContent = text.slice(0, startIndex) + text.slice(endIndex);
225
+ this.hasThinkValue = true;
226
+ this.chainThinkValue = thinkVal.replaceAll("<think>", "").replaceAll("</think>", "<br/>");
227
+ this.chainThinkValue = this.filterText(this.chainThinkValue)
228
+ return typedContent.replaceAll("\\n", "\n");
229
+ } else {
230
+ // text.replaceAll("\\n", "\n")
231
+ this.hasThinkValue = true;
232
+ this.chainThinkValue = text.replaceAll("<think>", "").replaceAll("</think>", "<br/>");
233
+ return ""
234
+ }
235
+ },
236
+ filterText(str) {
237
+ /* eslint-disable */
238
+ const regex = /\{\n "functionCall": \{/s
239
+ str = str.replace(regex, "")
240
+ str = str.replaceAll("```json", "")
241
+ return str
242
+ },
243
+ // 处理流式消息中的 <think> 标签
244
+ streamFilterThinkValue() {
245
+
246
+ },
247
+ // 打字效果已在MarkdownText文件中处理
248
+ typeNewWriter(text, keyId, msg) {
249
+ // setTimeout(() => {
250
+ // let ref = 'msgTyped' + keyId;
251
+ // const el = this.$refs[ref][0];
252
+ // new Typed(el, {
253
+ // strings: [text],
254
+ // typeSpeed: 15,
255
+ // showCursor: false,
256
+ // onComplete: () => {
257
+ // msg.isHistory = true
258
+ // this.$emit('openFirstPreview', msg)
259
+ // this.typingSuccess()
260
+ // },
261
+ // })
262
+ // }, 500)
263
+ },
264
+ handleScroll() {
265
+ console.log("this.isManualScroll = true;");
266
+ this.isManualScroll = true;
267
+ window.removeEventListener('wheel', this.handleScroll, true);
268
+ },
269
+ scrollMarkdown() {
270
+ !this.isManualScroll && this.$nextTick(() => {
271
+ let container = document.getElementById("list");
272
+ if (container) {
273
+ let scrollHeight = container.scrollHeight;
274
+ container.scrollTop = scrollHeight;
275
+ }
276
+ });
277
+ },
278
+ streamInfo(url) {
279
+ // 通知消息列表,当前还未建立sse,先不出空消息
280
+ if(this.retryUrl == ""){
281
+ this.$emit("sseOtherInfo", this.msgId, "init", "");
282
+ }
283
+ this.$emit("answerDocKnowledgeFn");
284
+ this.eventSource = new EventSource(url);
285
+ this.$emit("streamStatus", true, this.msgId);
286
+ // this.$set(this, 'showThink', true);
287
+ this.signSetFlag = false;
288
+ this.thinkEnd = false
289
+ let tempString = "";
290
+ // let generatingString = "";
291
+ // let timer = null;
292
+ let images = [];
293
+ let matched = [];
294
+ let recommendItems = [];
295
+ let isFirstStr = true;
296
+ const backslashOrNRegex = /[\\n]/; // 不能替换为 /\n/,会导致格式解析错误,在处理方法末尾做了修改
297
+ // 如果只有一次推送、并且推送字符串中带有 n
298
+ let isOnlyOnce = 0; // 标记是否只有一次推送
299
+ let hasN = false; // 标记是否有 n
300
+ const thinkStartRegex = /<think>/;
301
+ const thinkEndRegex = /<\/think>/
302
+ this.eventSource.onmessage = (event) => {
303
+ const data = JSON.parse(event.data);
304
+ if (event.lastEventId === 'finish') { // 不能删除 finish
305
+ console.log("finish+++++");
306
+ } else if (event.lastEventId === '[DONE]') {
307
+ this.streamEnd = true;
308
+ // 只有一次推送,且推送字符串中带有 n
309
+ // 用于特殊情况(只有一次内容推送 且 含有 n
310
+ if (isOnlyOnce == 1 && hasN) {
311
+ this.$emit("answerDocKnowledgeFn");
312
+ let repStr = tempString.replace(/\\n/g, '\n');
313
+ this.$set(this, "typedContent", repStr);
314
+ this.scrollMarkdown();
315
+ }
316
+ let DONETmier = setInterval(() => {
317
+ // 在所有文本内容打字效果完毕后 展示底部图片 文件 推荐等
318
+ if (this.generatingString == "") {
319
+ if (images.length != 0) {
320
+ setTimeout(() => {
321
+ this.$emit("sseOtherInfo", this.msgId, "images", images);
322
+ this.scrollMarkdown();
323
+ }, 50);
324
+ }
325
+ if (matched.length != 0) {
326
+ setTimeout(() => {
327
+ this.$emit("sseOtherInfo", this.msgId, "matched", matched);
328
+ this.scrollMarkdown();
329
+ }, 100);
330
+ }
331
+ if (recommendItems.length != 0) {
332
+ setTimeout(() => {
333
+ this.$emit("sseOtherInfo", this.msgId, "recommendItems", recommendItems);
334
+ this.scrollMarkdown();
335
+ }, 150);
336
+ }
337
+ this.closeTimeOut()
338
+ clearInterval(DONETmier);
339
+ if(this.thinkTimer != null) {
340
+ clearInterval(this.thinkTimer)
341
+ this.thinkTimer = null
342
+ this.thinkEnd = true
343
+ }
344
+ this.$emit("streamCallback", this.typedContent)
345
+ this.$emit("streamStatus", false, this.msgId);
346
+ this.appendCopyBtn()
347
+ setTimeout(() => {
348
+ this.removeLoadingNode()
349
+ }, 200);
350
+ this.$emit("sseOtherInfo", this.msgId, "start", "");
351
+ }
352
+
353
+ }, 100);
354
+ this.eventSource.close();
355
+ // 重试地址不为空,说明需要重试
356
+ console.log("需要重试 this.retryUrl: ", this.retryUrl);
357
+ if (this.retryUrl != "") {
358
+ console.log("this.retryUrl: ", this.retryUrl);
359
+ this.streamInfo(this.retryUrl);
360
+ // 将重试地址清空,用于在收到结束标识并结束后 判断是否需要重试
361
+ this.retryUrl = "";
362
+ this.$emit("sseOtherInfo", this.msgId, "changeHeaderStyleValue", this.$t('common.modelgeneration'));
363
+ }
364
+ } else if (event.lastEventId === 'matched') {
365
+ matched = data;
366
+ } else if (event.lastEventId === 'images') {
367
+ images = data;
368
+ } else if (event.lastEventId === 'recommendItems') {
369
+ recommendItems = data;
370
+ } else if (event.lastEventId === 'retry') {
371
+ this.retryUrl = data.content;
372
+ } else if (event.lastEventId === 'generated_image') {
373
+ let imgs = JSON.parse(data.content);
374
+ let imgsStr = "";
375
+ for (let i = 0; i < imgs.length; i++) {
376
+ imgsStr += `![](${imgs[i].oss_url})`;
377
+ }
378
+ this.$set(this, "typedContent", this.typedContent + imgsStr);
379
+
380
+ // 模拟图片推送
381
+ // let imgs = JSON.parse('[{"oss_url":"https://guoranopen-zjk.oss-cn-zhangjiakou.aliyuncs.com/generated-img/generated_img_865652a94b374337b2ac703466cdea94.png"}]');
382
+ // let imgsStr = "";
383
+ // for (let i = 0; i < imgs.length; i++) {
384
+ // imgsStr += `![](${imgs[i].oss_url})`;
385
+ // }
386
+ // this.$set(this, "typedContent", this.typedContent + imgsStr);
387
+
388
+ } else if (event.lastEventId === 'source') {
389
+ this.$emit("changedHeadStatusValue", this.msgId, data);
390
+ } else {
391
+ // console.log("start event.lastEventId: ", event.lastEventId);
392
+ // 设置当前知识答案id,用于loading取消和展示
393
+ this.$emit("sseOtherInfo", this.msgId, "start", "");
394
+ if(thinkStartRegex.test(data.content) && thinkEndRegex.test(data.content)) {
395
+ this.hasThinkValue = true;
396
+ this.thinkCount = 0
397
+ this.ThinkTime()
398
+ } else {
399
+ // 最终根据规则是以最后一个 </think> 标识当前深度思考结束,开始正常添加数据到 typedContent
400
+ if (thinkStartRegex.test(data.content)) {
401
+ if (this.thinkCount == 0) {
402
+ this.thinkCount = 0
403
+ } else {
404
+ this.thinkCount += 1;
405
+ }
406
+ this.hasThinkValue = true;
407
+ this.ThinkTime()
408
+ // this.streamThinkValueAddFlag = true;
409
+ }
410
+ if (thinkEndRegex.test(data.content)) {
411
+ this.thinkCount -= 1;
412
+ // this.streamThinkValueAddFlag = false;
413
+ }
414
+ }
415
+ // 字符串中存在特定字符,本次接收消息暂不展示
416
+ if (backslashOrNRegex.test(data.content) && !this.hasThinkValue) {
417
+ tempString += data.content;
418
+ // 用于特殊情况(只有一次内容推送 且 含有 n)
419
+ isOnlyOnce++;
420
+ hasN = true;
421
+ } else {
422
+ if (data.content.trim() != "") {
423
+ isOnlyOnce++;
424
+ }
425
+ // 此处判断的逻辑是识别到了深度思考 结束,开始处理回复的内容数据,关闭掉深度思考的手动计时,并收起深度思考的内容
426
+ let repStr = tempString.replace(/\\n/g, '\n');
427
+ if (this.hasThinkValue && this.thinkCount == 0) {
428
+ this.generatingThinkString = this.generatingThinkString + repStr + data.content;
429
+ this.thinkEnd = false
430
+ this.ThinkTime()
431
+ } else {
432
+ let str = data.content
433
+ if (thinkEndRegex.test(data.content)) {
434
+ let arr = data.content.split("</think>")
435
+ this.generatingThinkString = this.generatingThinkString + arr[0]
436
+ str = arr[1]
437
+ }
438
+ this.generatingString = this.generatingString + repStr + str;
439
+ }
440
+ tempString = "";
441
+ if (isFirstStr) {
442
+ isFirstStr = false;
443
+ this.timer = 0;
444
+ this.openTimeOutSetVal()
445
+ }
446
+ }
447
+ }
448
+ };
449
+ this.eventSource.onerror = (err) => {
450
+ this.typedContent += `\n${this.$t("dragonCommon.eventSourceError")}\n`;
451
+ this.closeTimeOut()
452
+ this.$emit("streamStatus", false, this.msgId);
453
+ this.$emit("streamErrorCallBack", this.msgId);
454
+ this.$emit("streamCallback", this.typedContent)
455
+ if(this.eventSource) {
456
+ try {
457
+ this.eventSource.close();
458
+ } catch (e) {
459
+ console.error('EventSource close failed:', e);
460
+ }
461
+ }
462
+ setTimeout(() => {
463
+ this.removeLoadingNode()
464
+ }, 200);
465
+ };
466
+ },
467
+ openTimeOutSetVal() {
468
+ if (this.timer != null) {
469
+ this.timer = setTimeout(() => {
470
+ this.closeThinkContent(this)
471
+ // 先把深度思考的值 push 完成后再执行正常内容的 push,最终以正常内容的值为空判断为本轮打字效果结束
472
+ if (this.generatingThinkString != "") {
473
+ let str = this.filterText(this.chainThinkValue + this.generatingThinkString.charAt(0))
474
+ this.$set(this, "chainThinkValue", str)
475
+ this.generatingThinkString = this.generatingThinkString.slice(1)
476
+ this.scrollMarkdown();
477
+ } else if (this.generatingString != "") {
478
+ this.$set(this, "typedContent", this.typedContent + this.generatingString.charAt(0))
479
+ this.generatingString = this.generatingString.slice(1)
480
+ this.scrollMarkdown();
481
+ if(this.thinkTimer != null) {
482
+ clearInterval(this.thinkTimer)
483
+ this.thinkTimer = null
484
+ this.thinkEnd = true
485
+ }
486
+ }
487
+ this.openTimeOutSetVal()
488
+ }, 20);
489
+ }
490
+ },
491
+ closeTimeOut() {
492
+ clearTimeout(this.timer);
493
+ this.timer = null;
494
+ },
495
+ ThinkTime() {
496
+ if(this.thinkTimer == null) {
497
+ this.thinkTimer = setInterval(() => {
498
+ this.thinkTimeNum += 1;
499
+ }, 1000)
500
+ }
501
+ },
502
+ startTypingEffect() {
503
+ // 使用正则表达式将 HTML 内容按 <table> 标签分割
504
+ const segments = this.chainValues.split(/(<\/?table[\s\S]*?>[\s\S]*?<\/table>)/i);
505
+ console.log("segments: ", segments);
506
+
507
+ let segmentIndex = 0;
508
+ let i = 0;
509
+ const processNextSegment = () => {
510
+ if (segmentIndex >= segments.length) {
511
+ this.$emit('openFirstPreview')
512
+ this.$emit('typingSuccess');
513
+ return;
514
+ }
515
+ const segment = segments[segmentIndex];
516
+ if (/^<\/?table[\s\S]*?>[\s\S]*?<\/table>$/i.test(segment)) {
517
+ // 如果当前段是 <table> 标签,直接添加到 typedContent 中
518
+ this.typedContent += segment;
519
+ segmentIndex++;
520
+ processNextSegment();
521
+ } else {
522
+ i = 0;
523
+ const updateContent = () => {
524
+ if (i < segment.length) {
525
+ this.typedContent += segment.charAt(i);
526
+ i++;
527
+ // 使用 requestAnimationFrame 替代 setInterval,防止卡顿
528
+ requestAnimationFrame(updateContent);
529
+ } else {
530
+ segmentIndex++;
531
+ processNextSegment();
532
+ }
533
+ };
534
+ updateContent();
535
+ }
536
+ };
537
+ processNextSegment();
538
+ },
539
+
540
+ lookImage(e) {
541
+ let previewImageUrl = ""
542
+ console.log('e.target', e.target)
543
+ if (e.target.localName == 'img') {
544
+ previewImageUrl = e.target.currentSrc;
545
+ this.showPreview = true;
546
+ let richtext = JSON.parse(JSON.stringify(this.typedContent))
547
+ this.imgList = [];
548
+ richtext.replace(/<img [^>]*src=['"]([^'"]+)[^>]*>/g, (match, capture) => {
549
+ this.imgList.push(capture);
550
+ });
551
+ /*当前点击的图片作为第一个图片*/
552
+ let index = this.imgList.indexOf(previewImageUrl);
553
+ this.imgList.splice(index, 1);
554
+ this.imgList.unshift(previewImageUrl);
555
+ }
556
+
557
+ },
558
+ closeViewer() {
559
+ this.showPreview = false;
560
+ },
561
+ clickMarkDown(e) {
562
+ if (e.target.tagName == "A") {
563
+ window.open(e.target.href, "_blank")
564
+ e.stopPropagation()
565
+ e.preventDefault()
566
+ return false
567
+ }
568
+ },
569
+ closeThinkContent: _.debounce(function (that) {
570
+ if (that.thinkEnd && that.generatingThinkString == "" && !that.signSetFlag) {
571
+ that.showThink = false;
572
+ // 标识已经自动关闭过一次深度思考内容,不再影响手动打开的效果
573
+ that.signSetFlag = true;
574
+ }
575
+ }, 500),
576
+ parseCode(markDownText) {
577
+ const parser = new DOMParser();
578
+ const doc = parser.parseFromString(markDownText, 'text/html');
579
+ doc.querySelectorAll('pre code').forEach((block) => {
580
+ hljs.highlightBlock(block);
581
+ });
582
+ doc.querySelectorAll('a').forEach((block) => {
583
+ block.setAttribute('target', '_blank');
584
+ block.setAttribute('rel', 'noopener noreferrer');
585
+ });
586
+ let node = this.getLastNode(doc)
587
+ if (node && !this.isHistory) {
588
+ let loading = document.createElement('span')
589
+ loading.classList.add("loading-circle")
590
+ loading.id = "loading-circle"
591
+ this.removeLoadingNode()
592
+ node.appendChild(loading)
593
+ }
594
+ return doc.documentElement.outerHTML
595
+ },
596
+ removeLoadingNode() {
597
+ let old = document.getElementById("loading-circle")
598
+ if(old) old.remove()
599
+ },
600
+ getLastNode(node) {
601
+ let filterList = ["STRONG", "BR", "SPAN", "I", "EM", "B", "A"]
602
+ if(node.lastElementChild && !filterList.includes(node.lastElementChild.tagName)) {
603
+ return this.getLastNode(node.lastElementChild)
604
+ } else {
605
+ return node
606
+ }
607
+ },
608
+ appendCopyBtn() {
609
+ let doc = this.$refs.markDown.$el ? this.$refs.markDown.$el : this.$refs.markDown
610
+ let codeList = doc.querySelectorAll('pre code')
611
+ codeList.forEach(ele =>{
612
+ let list = ele.getElementsByClassName("code_copy_btn")
613
+ if(list.length == 0 && ele.classList.contains("hljs") && !ele.classList.contains("hljs-undefined")) {
614
+ let text = ele.innerText
615
+ let btnId = uuidv4();
616
+ let copy = document.createElement('div')
617
+ copy.classList.add("code_copy_btn")
618
+ copy.setAttribute('id', btnId)
619
+ copy.style.display = 'flex'
620
+ let i = document.createElement('i')
621
+ i.classList.add("iconfont")
622
+ i.classList.add("guoran-tongyichicun-fuzhi")
623
+ let span = document.createElement('span')
624
+ span.innerText = this.$t("dragonCommon.copy")
625
+ copy.append(i)
626
+ copy.append(span)
627
+ copy.onclick = () =>{
628
+ let fn = () =>{
629
+ let id = copy.getAttribute("id")
630
+ copy.style.display = "none"
631
+ let cs = document.getElementById(id + "success")
632
+ if(cs) {
633
+ cs.style.display = "flex"
634
+ }
635
+ setTimeout(() =>{
636
+ cs && (cs.style.display = "none")
637
+ copy.style.display = "flex"
638
+ }, 2000)
639
+ }
640
+ if (navigator.clipboard) {
641
+ // 使用 Clipboard API 进行复制
642
+ navigator.clipboard.writeText(text).then(() => { fn()})
643
+ .catch((error) => {
644
+ // 如果 Clipboard API 失败,尝试使用传统方法
645
+ this.fallbackCopyTextToClipboard(text, fn, () =>{
646
+ this.$message.error(this.$t("dragonCommon.copyError"))
647
+ });
648
+ });
649
+ } else {
650
+ this.fallbackCopyTextToClipboard(text, fn, () =>{
651
+ this.$message.error(this.$t("dragonCommon.copyError"))
652
+ });
653
+ }
654
+ }
655
+ ele.append(copy)
656
+ let copySuccess = document.createElement('div')
657
+ copySuccess.classList.add("code_copy_btn")
658
+ copySuccess.setAttribute('id', btnId + 'success')
659
+ copySuccess.style.display = 'none'
660
+ let csI = document.createElement('i')
661
+ csI.classList.add("el-icon-check")
662
+ let csSpan = document.createElement('span')
663
+ csSpan.innerText = this.$t("dragonCommon.copySuccess")
664
+ copySuccess.append(csI)
665
+ copySuccess.append(csSpan)
666
+ ele.append(copySuccess)
667
+ }
668
+ })
669
+ },
670
+ fallbackCopyTextToClipboard(text, success, fail) {
671
+ // 创建一个临时的 input 元素
672
+ const textArea = document.createElement('textarea');
673
+ textArea.value = text;
674
+ // 使 textarea 不在屏幕上显示
675
+ textArea.style.position = 'fixed';
676
+ textArea.style.top = 0;
677
+ textArea.style.left = 0;
678
+ textArea.style.width = '2em';
679
+ textArea.style.height = '2em';
680
+ textArea.style.padding = 0;
681
+ textArea.style.border = 'none';
682
+ textArea.style.outline = 'none';
683
+ textArea.style.boxShadow = 'none';
684
+ textArea.style.background = 'transparent';
685
+
686
+ // 将 textarea 添加到文档中
687
+ document.body.appendChild(textArea);
688
+ textArea.focus();
689
+ textArea.select();
690
+
691
+ try {
692
+ // 执行复制命令
693
+ const successful = document.execCommand('copy');
694
+ if (successful) {
695
+ success()
696
+ } else {
697
+ fail()
698
+ }
699
+ } catch (err) {
700
+ fail()
701
+ }
702
+ // 移除临时的 textarea
703
+ document.body.removeChild(textArea);
704
+ },
705
+ },
706
+ beforeDestroy() {
707
+ if (this.eventSource) {
708
+ this.eventSource.close();
709
+ }
710
+ }
711
+ };
712
+ </script>
713
+
714
+ <style scoped lang="less">
715
+ .blinking-cursor {
716
+ display: inline-block;
717
+ width: 2px;
718
+ height: 1em;
719
+ background-color: black;
720
+ animation: blink 1s steps(2, start) infinite;
721
+ }
722
+
723
+ @keyframes blink {
724
+ 0%, 100% {
725
+ opacity: 1;
726
+ }
727
+ 50% {
728
+ opacity: 0;
729
+ }
730
+ }
731
+ .think {
732
+ padding: 10px;
733
+ background: #f2f5ff;
734
+ border-radius: 8px;
735
+ margin-bottom: 12px;
736
+
737
+ .think_content {
738
+ height: auto;
739
+ white-space: pre-wrap;
740
+ transition: all 2s ease;
741
+
742
+ /deep/ br {
743
+ line-height: 10px !important;
744
+ }
745
+ }
746
+
747
+ .hide_think {
748
+ height: 0px;
749
+ overflow: hidden;
750
+ transition: all 2s ease;
751
+ }
752
+
753
+ .think_title {
754
+ width: 100%;
755
+ height: 20px;
756
+ display: flex;
757
+ align-items: center;
758
+ justify-content: space-between;
759
+ cursor: pointer;
760
+
761
+ .think_title_left {
762
+ display: flex;
763
+ align-items: center;
764
+
765
+ i {
766
+ font-size: 18px;
767
+ }
768
+
769
+ .guoran-icon-deepseek {
770
+ font-size: 18px;
771
+ }
772
+ }
773
+
774
+ .think_title_right {
775
+ font-size: 18px;
776
+ color: #366aff;
777
+ }
778
+
779
+ .hide_think_title {
780
+ transform: rotate(180deg);
781
+ }
782
+ }
783
+ }
784
+
785
+ .mark_down {
786
+ line-height: 24px;
787
+ overflow-x: auto;
788
+
789
+ /deep/code {
790
+ font-family: Microsoft Yahei, Helvetica Neue, Helvetica, Arial, sans-serif !important;
791
+ white-space: pre-wrap;
792
+ font-size: 16px;
793
+ position: relative;
794
+ .code_copy_btn {
795
+ position: absolute;
796
+ right: 10px;
797
+ top: 10px;
798
+ display: flex;
799
+ align-items: center;
800
+ padding: 3px 12px;
801
+ background: #EEF1FF;
802
+ cursor: pointer;
803
+ font-size: 14px;
804
+ border-radius: 25px;
805
+ i {
806
+ font-size: 14px;
807
+ margin-right: 6px;
808
+ }
809
+ }
810
+ }
811
+
812
+ /deep/ pre {
813
+ // code {
814
+ // background: #F3F3F3 !important;
815
+ // color: #444 !important;
816
+ // }
817
+ .language-undefined {
818
+ background: #FFFFFF;
819
+ color: #000;
820
+ }
821
+ }
822
+
823
+ /deep/p {
824
+ margin-bottom: 7px;
825
+ font-size: 16px;
826
+ }
827
+
828
+ /deep/p:only-child {
829
+ margin: 0 !important;
830
+ }
831
+
832
+ /deep/p:last-child {
833
+ margin-bottom: 0 !important;
834
+ }
835
+
836
+ /deep/ul {
837
+ margin-bottom: 7px;
838
+ list-style: disc;
839
+ padding-left: 40px;
840
+
841
+ li {
842
+ margin: 7px 0 !important;
843
+ font-size: 16px;
844
+ // padding-left: 40px;
845
+ }
846
+ }
847
+
848
+ /deep/ ol {
849
+ list-style: auto;
850
+ padding-left: 40px;
851
+
852
+ li {
853
+ margin: 7px 0 !important;
854
+ font-size: 16px;
855
+ // padding-left: 40px;
856
+ }
857
+ }
858
+
859
+ /deep/img {
860
+ max-width: 400px;
861
+ }
862
+
863
+ /deep/h3,
864
+ /deep/h2,
865
+ /deep/h1,
866
+ /deep/h4,
867
+ /deep/h5,
868
+ /deep/h6 {
869
+ color: #000000;
870
+ font-weight: 500;
871
+ line-height: 26px;
872
+ margin: 7px 0;
873
+ }
874
+
875
+ /deep/h6 {
876
+ font-size: 12px;
877
+ }
878
+
879
+ /deep/h5 {
880
+ font-size: 14px;
881
+ }
882
+
883
+ /deep/h4 {
884
+ font-size: 16px;
885
+ }
886
+
887
+ /deep/h3 {
888
+ font-size: 20px;
889
+ }
890
+
891
+ /deep/h2 {
892
+ font-size: 22px;
893
+ }
894
+
895
+ /deep/h1 {
896
+ font-size: 24px;
897
+ }
898
+
899
+ // /deep/h3 {
900
+ // font-size: 20px;
901
+ // }
902
+
903
+ /deep/hr {
904
+ margin: 16px 0;
905
+ }
906
+
907
+ /deep/ table {
908
+ border-spacing: 0;
909
+ // border-radius: 8px;
910
+ // overflow: hidden;
911
+ border: solid 1px #e0e6f7;
912
+ min-width: 800px;
913
+ overflow-x: auto;
914
+ margin: 16px 0;
915
+ }
916
+
917
+ /deep/ th {
918
+ background: #EEF1FF;
919
+ border: solid 1px #e0e6f7;
920
+ padding: 8px;
921
+ height: 38px;
922
+ min-width: 100px;
923
+ }
924
+
925
+ /deep/ td {
926
+ border: solid 1px #e0e6f7;
927
+ padding: 8px;
928
+ height: 28px;
929
+ min-width: 100px;
930
+ }
931
+ }
932
+
933
+ /* 圆圈的样式 */
934
+ /deep/.loading-circle {
935
+ width: 8px;
936
+ height: 8px;
937
+ display: inline-block;
938
+ border-radius: 50%;
939
+ background-color: #366aff;
940
+ animation: loading 2s infinite ease-in-out;
941
+ margin: 0 8px;
942
+ /* 动画控制 */
943
+ }
944
+
945
+ /* 放大缩小的动画 */
946
+ @keyframes loading {
947
+
948
+ 0%,
949
+ 100% {
950
+ transform: scale(1);
951
+ /* 原始大小 */
952
+ }
953
+
954
+ 50% {
955
+ transform: scale(1.5);
956
+ /* 放大 */
957
+ }
958
+ }
959
+ </style>