浏览代码

feat(sidepanel): 优化聊天功能和智能填表

- 添加智能填表功能,实现表单信息抽取和填充
- 优化聊天界面,增加消息滚动和打字机效果
- 改进文件上传和解析流程
- 调整消息存储和展示逻辑
- 修复了一些与API交互相关的问题
chd 4 月之前
父节点
当前提交
0270eb078d

+ 2 - 0
package.json

@@ -19,10 +19,12 @@
     "@element-plus/icons-vue": "^2.3.1",
     "axios": "^1.7.9",
     "crypto-js": "^4.2.0",
+    "dompurify": "^3.2.5",
     "element-plus": "^2.9.1",
     "highlight.js": "^11.11.1",
     "jsencrypt": "^3.3.2",
     "lodash": "^4.17.21",
+    "marked": "^15.0.8",
     "moment": "^2.30.1",
     "openai": "^4.85.4",
     "pinia": "^3.0.1",

+ 8 - 0
src/api/modal.js

@@ -7,3 +7,11 @@ export function askQues(data) {
         data: data
     })
 }
+
+export function getFormKey(data) {
+    return request({
+        url: '/ai/model/callAlgorithm',
+        method: 'post',
+        data: data
+    })
+}

+ 158 - 112
src/entrypoints/sidepanel/Chat.vue

@@ -12,10 +12,6 @@
           <div class="loading-spinner"></div>
           <span>加载更多消息...</span>
         </div>
-        // 添加一个 ref 用于获取消息列表容器
-        const messagesContainer = ref(null)
-
-        // 修改模板中的消息列表 div,添加 ref
         <div class="messages" ref="messagesContainer">
           <div v-for="(message, index) in messages" :key="index"
             :class="['message-item', message.role === 'user'  ? 'self' : 'other']">
@@ -42,7 +38,7 @@
                 </span>
               </div>
               <div class="timestamp ">{{ moment(message.sortKey).format('MM-DD HH:mm') }}
-                <span v-if="message.add" style="cursor: pointer;" @click="handleInput">填充</span>
+                <!-- <span v-if="message.add" style="cursor: pointer;" @click="handleInput">填充</span> -->
               </div>
             </div>
           </div>
@@ -51,7 +47,7 @@
       <ScrollToBottom :target="scrollbar" ref="scrollToBottomRef" />
     </div>
 
-    <Tools @read-click="readClick" @upload-file="handleUpload" @handle-capture="handleCapture" @his-records="hisRecords"
+    <Tools @read-click="readClick" @upload-file="(file) => createFileObj(file)" @handle-capture="handleCapture" @his-records="hisRecords"
       @add-new-dialogue="addNewDialogue" @handle-current-change="handleCurrentChange"
       @handel-intelligent-filling-click="handelIntelligentFillingClick" />
 
@@ -69,7 +65,7 @@
 
               <div class="title-wrapper">
                 <span class="els title-scroller">{{ v?.title }}</span>
-                <span class="els url-scroller">{{ v?.url }}</span>
+                <span class="els url-scroller">{{ v.loading ? v?.state : v?.url }}</span>
               </div>
               <el-icon class="closeIcon" size="16px" color="#909399" @click="deletePageInfo(i)">
                 <CircleClose />
@@ -77,19 +73,22 @@
             </div>
           </div>
 
-          <div v-show="type !== FunctionList.Intelligent_Form_filling" class="card-btn">
-            <el-tooltip content="总结当前页面" placement="top">
-              <el-button round @click="handleSummary">总结</el-button>
+          <div class="card-btn">
+            <el-tooltip  content="总结当前页面" placement="top">
+              <el-button v-if="type !== FunctionList.Intelligent_Form_filling"  round @click="handleSummary">总结</el-button>
+            </el-tooltip>
+             <el-tooltip   content="抽取表单信息" placement="top">
+              <el-button v-if="type === FunctionList.Intelligent_Form_filling && pageInfoList.length" round @click="handleSummary">抽取</el-button>
             </el-tooltip>
           </div>
         </div>
 
         <el-input ref="textareaRef" v-model="inputMessage" type="textarea" :rows="3" placeholder="输入消息..."
-          @keyup.enter="handleAsk" />
+          @keyup.enter="() => handleAsk()" />
         <div class="chat_area_op">
           <el-button
             :style="`background-color:${inputMessage.trim() ? '#4d6bfe' : '#d6dee8'};border-color:${inputMessage.trim() ? '#4d6bfe' : '#d6dee8'}`"
-            v-if="inputMessage.trim() || !sendLoading" type="primary" circle @click="handleAsk"
+            v-if="inputMessage.trim() || !sendLoading" type="primary" circle @click="() => handleAsk()"
             :disabled="!inputMessage.trim() || sendLoading">
             <svg-icon icon-class="send" color="#000000" />
           </el-button>
@@ -139,7 +138,7 @@ import { getPageInfo, getXlsxValue, handleInput } from './utils/index.js'
 import { useMsgStore } from '@/store/modules/msg.ts'
 import { useUserStore } from '@/store/modules/user'
 import { putChat, uploadFile } from "@/api/index.js";
-import {askQues} from '@/api/modal.js'
+import {askQues,getFormKey} from '@/api/modal.js'
 import {debounce} from 'lodash'
 const userStore = useUserStore()
 import { getChatDetail } from '@/api/index.js'
@@ -227,7 +226,6 @@ const handleScrollToTop = debounce(async function () {
  * @param {string} type 发送的类型 document 、text
  * **/
 async function addMessage(msg, raw, type) {
-  console.log(msg);
   const newMessage = reactive({
     id:+new Date(),
     type: type || '',
@@ -238,7 +236,9 @@ async function addMessage(msg, raw, type) {
     sortKey: moment().valueOf(),
     role: 'user',
     conversationId:msgUuid.value,
-    addToHistory: `${!taklToHtml.value}`
+    addToHistory: `${!taklToHtml.value}`,
+    redisKey: undefined,
+    fileId:undefined
   })
   if (type === 'document') {
     newMessage.content = msg
@@ -246,60 +246,25 @@ async function addMessage(msg, raw, type) {
   }
   if (!msg) return
   messages.value.push(newMessage)
-  await nextTick(() => {
-    scrollbar.value?.setScrollTop(99999)
+  console.log(scrollbar.value);
+  scrollbar.value?.scrollTo(Number.MAX_VALUE)
+  nextTick(() => {
+    
   })
   return newMessage
 }
 
 const handleSummary = async () => {
-  let params = []
-  if (1) {
-    pageInfoList.value.forEach((pageInfo) => {
-      params.push({ role: 'system', content: `fileid://${pageInfo.fileId}` })
-    })
-    params.push({
-      role: 'user', content: `
-    请根据这几个文件的文本内容综合分析总结
-    要求:
-    1.抽取文件的文本内容
-    2.对每个文件中的文本内容进行分别总结
-    3.对步骤二总结的内容进行综合总结`
-    })
-  } else {
-    let tempStr = ''
-    pageInfoList.value.forEach((pageInfo, index) => {
-      pageInfo.handleText = getSummaryPrompt(pageInfo.content)
-      tempStr += `第${index + 1}段:${pageInfo.handleText}\n`
-    })
-    params = [{
-      role: 'user',
-      content: `请根据以下这几个内容综合总结出结果:
-      ${tempStr}要求:
-      1. 用简洁清晰的语言提取所有的核心要点
-      2. 保持客观中立的语气
-      3. 按重要性排序
-      4. 返回内容做好换行,以及展示样式
-      5. 请以"以下是对该文件内容的总结:"开头,然后用要点的形式列出主要内容。
-      6. 对这几个内容进行综合分析及联想`
-    }]
-  }
-  const msg = await addMessage(JSON.stringify(pageInfoList.value), JSON.stringify(params.map(_ => _.content)), 'document')
-  putChat(msg)
-  if (requestFlowFn) {
-    isShowPage.value = false
-    taklToHtml.value = false
-    pageInfoList.value = []
-    const res = await requestFlowFn(params)
-  }
+  if(sendLoading.value) return
+  handleAsk('请帮我总结当前文件')
 }
 
 async function handelIntelligentFillingClick() {
-  isShowPage.value = true
-  taklToHtml.value = true
-  const tempPageInfo = await getPageInfo()
-  pageInfo.value = tempPageInfo
-  pageInfoList.value = [tempPageInfo]
+  // isShowPage.value = true
+  // taklToHtml.value = true
+  // const tempPageInfo = await getPageInfo()
+  // pageInfo.value = tempPageInfo
+  // pageInfoList.value = [tempPageInfo]
   inputMessage.value = '/智能填表 '
   type.value = FunctionList.Intelligent_Form_filling
 }
@@ -330,7 +295,11 @@ async function handleCurrentData(e) {
   msgUuid.value = e
   chrome.storage.local.set({ msgUuid: msgUuid.value })
   await msgStore.initMsg()
-  scrollbar.value?.setScrollTop(99999)
+  nextTick(() => {
+  if (scrollbar.value && scrollbar.value.wrapRef) {
+    scrollbar.value.setScrollTop(scrollbar.value.wrapRef.scrollHeight)
+  }
+})
 }
 
 async function readClick() {
@@ -345,12 +314,10 @@ async function readClick() {
     return
   }
   const tempPageInfo = await getPageInfo()
-  if (AIModel.value.file === true) {
-    const blob = new Blob([tempPageInfo.content.mainContent], { type: 'text/plain' })
-    const file = new File([blob], tempPageInfo.title + '.txt', { type: 'text/plain' })
-    tempPageInfo.fileId = await modelFileUpload(file)
-  }
-  pageInfoList.value.unshift(tempPageInfo)
+  const blob = new Blob([tempPageInfo.content.mainContent], { type: 'text/plain' })
+  const file = new File([blob], tempPageInfo.title + '.txt', { type: 'text/plain' })
+  await createFileObj(file)
+  // await handleUpload(file,tempPageInfo.title + '.txt')
 }
 
 function deletePageInfo(i) {
@@ -374,56 +341,105 @@ function addNewDialogue() {
   page.value = 1
   pageInfoList.value = []
   msgUuid.value = uuidv4()
+  hasNext.value = false
   chrome.storage.local.set({ msgUuid: msgUuid.value })
 }
 
-async function handleAsk() {
-  const str = inputMessage.value.trim()
+async function handleAsk(value) {
+  const str = value ?? inputMessage.value.trim()
+  if (type.value === FunctionList.Intelligent_Form_filling) {
+     await addMessage(str)
+     const res = await fetchRes(str)
+    if (res.status === 'ok') {
+      formInfo.value = res.data
+      console.log(res, 55558)
+    } else {
+      type.value = FunctionList.File_Operation
+    }
+    return 
+  }
   inputMessage.value = ''
   if (sendLoading.value) return
-  const msg = await addMessage(str)
+  let msg = null
+  if (pageInfoList.value.length) {
+    msg = await addMessage(JSON.stringify([...pageInfoList.value, { type: 'text', value: str }]), str, 'document')
+    msg.fileId = pageInfoList.value[0].fileId
+    msg.redisKey = pageInfoList.value[0].redisKey
+  } else msg = await addMessage(str)
   await putChat(msg)
-      const obj = reactive({
-      type: type || '',
-      rawContent:'',
-      senderId: -1,
-      receiverId: userStore.userInfo.id, //大模型
-      content: '',
-      sortKey: moment().valueOf(),
-      role: 'system',
-      conversationId: msgUuid.value,
-      })
   sendLoading.value = true
-    
+  // messages.value.push(obj)
+  nextTick(() => {
+  if (scrollbar.value && scrollbar.value.wrapRef) {
+    scrollbar.value.setScrollTop(scrollbar.value.wrapRef.scrollHeight)
+  }
+})
+   const obj = reactive({
+    type: type || '',
+    rawContent: '',
+    senderId: -1,
+    receiverId: userStore.userInfo.id, //大模型
+    content: '',
+    sortKey: moment().valueOf(),
+    role: 'system',
+    conversationId: msgUuid.value,
+    })
   messages.value.push(obj)
-    scrollbar.value?.setScrollTop(99999)
   const res = await askQues({
     conversationId: msgUuid.value,
     modelName: '通义千问-Max',
     question: msg.rawContent,
-    id:'699637194561691650'
+    id: '699637194561691650',
+    redisKey:msg.redisKey
   })
   const result = res.data[res.data.length - 1].content
-  obj.content = formatMessage(result)
+  isShowPage.value = false
+  pageInfoList.value = []
+  // 保存原始内容
   obj.rawContent = result
-    putChat({
-      ...obj,
-      content:''
-    })
-  sendLoading.value = false
-    scrollbar.value?.setScrollTop(99999)
-  // if (type.value === FunctionList.Intelligent_Form_filling) {
-  //   const res = await fetchRes(str)
-  //   if (res.status === 'ok') {
-  //     formInfo.value = res.data
-  //     console.log(res, 55558)
-  //   } else {
-  //     type.value = FunctionList.File_Operation
-  //     isShowPage.value = false
-  //     taklToHtml.value = false
-  //     pageInfoList.value = []
-  //   }
-  // } else streamRes(taklToHtml.value)
+  
+  // 实现打字机效果
+  const formattedText = formatMessage(result)
+  const typewriterEffect = async (text) => {
+    let currentIndex = 0
+    const totalLength = text.length
+    const typingSpeed = 10 // 打字速度,可以调整
+    
+    // 清除之前的内容
+    obj.content = ''
+    
+    // 逐字添加内容
+    const typing = setInterval(() => {
+      if (currentIndex < totalLength) {
+        obj.content += text[currentIndex]
+        currentIndex++
+        // 滚动到底部,保持视图跟随
+        nextTick(() => {
+  if (scrollbar.value && scrollbar.value.wrapRef) {
+    scrollbar.value.setScrollTop(scrollbar.value.wrapRef.scrollHeight)
+  }
+})
+      } else {
+        clearInterval(typing)
+        sendLoading.value = false
+        // 打字完成后再次滚动到底部
+        nextTick(() => {
+  if (scrollbar.value && scrollbar.value.wrapRef) {
+    scrollbar.value.setScrollTop(scrollbar.value.wrapRef.scrollHeight)
+  }
+})
+      }
+    }, typingSpeed)
+  }
+  
+  // 开始打字机效果
+  await typewriterEffect(formattedText)
+  
+  // 保存到数据库
+  putChat({
+    ...obj,
+    content: '' // 保存完整内容
+  })
 }
 function handleCapture() {
   ElMessage({
@@ -448,23 +464,50 @@ const handleUpload = async (formData,name) => {
   isShowPage.value = true
   const obj = reactive({
     title: name,
-    url: '上传中...',
+    state: '上传中...',
     favIconUrl: fileLogo,
     loading: true,
+    url:'',
+    fileId: '',
+    redisKey:'',
     content: {
       mainContent: ''
     }
   })
-  pageInfoList.value.push(obj)
-  const res = await uploadFile(formData)
+  pageInfoList.value = [obj]
+  try {
+    const res = await uploadFile(formData)
   if (type.value === FunctionList.Intelligent_Form_filling) {
-    obj.url = '解析中...'
-
+    obj.state = '解析中...'
+    obj.redisKey = res.data.redisKey
+    obj.id = res.data.file.id
+    obj.url = res.data.file.url
+    console.log({
+      body: formInfo.value,
+      uploadFile:res.data.data
+    },888569);
+    
+    const result = await getFormKey({
+      body: formInfo.value,
+      uploadFile:res.data.data
+    })
+    console.log(result);
+    
   }
   if (type.value === FunctionList.File_Operation) {
     obj.loading = false
     obj.url = res.data.url
-
+    obj.redisKey = res.data.redisKey
+    obj.fileId = res.data.file.id
+    obj.url = res.data.file.url
+    }
+  obj.loading = false
+  } catch (error) {
+    console.log(error);
+    
+    ElMessage.error('上传出错')
+    pageInfoList.value = []
+    isShowPage.value = false
   }
 }
 async function getFileValue(file) {
@@ -492,14 +535,17 @@ onMounted(async () => {
       pageInfo.value = message.data
     }
   })
-  nextTick(() => {
-    scrollbar.value?.setScrollTop(99999)
-  })
+ nextTick(() => {
+  if (scrollbar.value && scrollbar.value.wrapRef) {
+    scrollbar.value.setScrollTop(scrollbar.value.wrapRef.scrollHeight)
+  }
+})
 })
 </script>
 
 <style lang="scss" scoped>
 @use '@/entrypoints/sidepanel/css/chat.scss';
+@use '@/entrypoints/sidepanel/css/markdown.scss';
 
 .loading-more-indicator {
   display: flex;

+ 2 - 2
src/entrypoints/sidepanel/component/ScrollToBottom.vue

@@ -19,7 +19,7 @@ defineExpose({
 
 <template>
   <el-button v-show="showButton" :icon="CaretBottom" circle
-             @click="props.target.setScrollTop(99999)"
+             @click="props.target.setScrollTop(props.target.wrapRef.scrollHeight)"
              class="back_bottom" />
 </template>
 
@@ -30,4 +30,4 @@ defineExpose({
   right: 12px;
   font-size: 20px
 }
-</style>
+</style>

+ 13 - 4
src/entrypoints/sidepanel/component/document.vue

@@ -1,4 +1,4 @@
-<script setup lang="ts">
+<script setup lang="js">
 import { onMounted, computed } from 'vue'
 
 const props = defineProps({
@@ -12,20 +12,29 @@ const props = defineProps({
   }
 })
 const docZil = computed(() => {
-  return JSON.parse(props.content)
+  const arr = JSON.parse(props.content)
+  arr.pop()
+  return arr
+})
+const value = computed(() => {
+   const arr = JSON.parse(props.content)
+ return  arr[arr.length - 1].value
 })
 </script>
 
 <template>
   <div class="document_r">
-    <div v-for="(item,i) in docZil" :key="i" class="document_content">
+    <div v-for="(item,i) in docZil" :key="i" class="document_content" @click="() => { 
+      console.log(item);
+      
+    }">
       <img class="document_img" :src="item.favIconUrl" alt="">
       <div class="document_text">
         <p class="els" :title="item.title" style="font-weight: 900">{{ item.title }}</p>
         <p class="els" :title="item.url" style="color: rgba(96,98,102,0.77)">{{ item.url }}</p>
       </div>
     </div>
-    <p class="document_content1">总结</p>
+    <p class="document_content1">{{value}}</p>
   </div>
 
 </template>

+ 3 - 4
src/entrypoints/sidepanel/component/historyComponent.vue

@@ -28,8 +28,7 @@ watch(drawer, (newVal) => {
       page:1,
       size:50
     }).then(res => {
-      dataList.value = res.list
-      console.log(res.list);
+      dataList.value = res.data.list
     }).finally(res => loading.value = false)
   }
 })
@@ -88,8 +87,8 @@ defineExpose({
     </template>
     <div style="height: 100%;overflow: hidden;" v-loading="loading">
       <div class="his_delete">
-        <el-input style="margin-right: 12px" v-model="input" placeholder="搜索" clearable :prefix-icon="Search"
-          :disabled="true" />
+        <!-- <el-input style="margin-right: 12px" v-model="input" placeholder="搜索" clearable :prefix-icon="Search"
+          :disabled="true" /> -->
         <!-- <el-tooltip effect="dark" content="删除全部" placement="top">
           <el-button :icon="Delete" circle @click="handleDeleteDB" />
         </el-tooltip> -->

+ 152 - 0
src/entrypoints/sidepanel/css/markdown.scss

@@ -0,0 +1,152 @@
+/* Markdown 样式 */
+.content {
+  font-size: 14px;
+  line-height: 1.6;
+  word-break: break-word;
+  
+  h1, h2, h3, h4, h5, h6 {
+    margin-top: 24px;
+    margin-bottom: 16px;
+    font-weight: 600;
+    line-height: 1.25;
+  }
+  
+  h1 { font-size: 2em; }
+  h2 { font-size: 1.5em; }
+  h3 { font-size: 1.25em; }
+  
+  p {
+    margin-top: 0;
+    margin-bottom: 16px;
+  }
+  
+  a {
+    color: #0366d6;
+    text-decoration: none;
+    
+    &:hover {
+      text-decoration: underline;
+    }
+  }
+  
+  img {
+    max-width: 100%;
+    box-sizing: border-box;
+  }
+  
+  blockquote {
+    padding: 0 1em;
+    color: #6a737d;
+    border-left: 0.25em solid #dfe2e5;
+    margin: 0 0 16px 0;
+  }
+  
+  ul, ol {
+    padding-left: 2em;
+    margin-top: 0;
+    margin-bottom: 16px;
+  }
+  
+  code {
+    font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
+    padding: 0.2em 0.4em;
+    margin: 0;
+    font-size: 85%;
+    background-color: rgba(27, 31, 35, 0.05);
+    border-radius: 3px;
+  }
+  
+  pre {
+    margin-top: 0;
+    margin-bottom: 16px;
+    padding: 16px;
+    overflow: auto;
+    font-size: 85%;
+    line-height: 1.45;
+    background-color: #f6f8fa;
+    border-radius: 3px;
+    
+    code {
+      padding: 0;
+      margin: 0;
+      font-size: 100%;
+      word-break: normal;
+      white-space: pre;
+      background: transparent;
+      border: 0;
+    }
+  }
+  
+  .code-block {
+    position: relative;
+    margin: 16px 0;
+    border-radius: 6px;
+    overflow: hidden;
+    
+    .code-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 8px 16px;
+      background-color: #f1f1f1;
+      border-bottom: 1px solid #ddd;
+      
+      .code-language {
+        font-size: 12px;
+        font-weight: 600;
+        color: #333;
+      }
+      
+      .copy-button {
+        padding: 2px 8px;
+        font-size: 12px;
+        background-color: #fff;
+        border: 1px solid #ddd;
+        border-radius: 4px;
+        cursor: pointer;
+        
+        &:hover {
+          background-color: #f9f9f9;
+        }
+      }
+    }
+  }
+  
+  .table-container {
+    overflow-x: auto;
+    margin-bottom: 16px;
+    
+    .responsive-table {
+      border-collapse: collapse;
+      width: 100%;
+      
+      th, td {
+        padding: 8px 13px;
+        border: 1px solid #dfe2e5;
+      }
+      
+      th {
+        background-color: #f6f8fa;
+        font-weight: 600;
+      }
+      
+      tr:nth-child(even) {
+        background-color: #f8f8f8;
+      }
+    }
+  }
+  
+  .math-inline {
+    font-style: italic;
+    padding: 0 3px;
+  }
+  
+  .math-block {
+    display: block;
+    margin: 16px 0;
+    padding: 16px;
+    text-align: center;
+    background-color: #f9f9f9;
+    border-radius: 4px;
+  }
+}

+ 3 - 1
src/entrypoints/sidepanel/hook/useMsg.ts

@@ -122,7 +122,9 @@ export function useMsg(scrollbar?: any) {
   })
     messages.value.push(obj)
     msg = msg.split('/智能填表')[1]
-    nextTick(() => scrollbar.value?.setScrollTop(99999))
+    if (scrollbar.value && scrollbar.value.wrapRef) {
+      scrollbar.value.setScrollTop(scrollbar.value.wrapRef.scrollHeight)
+    }
     if (!msg) {
       sendLoading.value = false
       const res = await awaitFindForm(obj)

+ 2 - 4
src/store/modules/msg.ts

@@ -28,12 +28,10 @@ export const useMsgStore = defineStore('msg', {
         this.msgUuid = msgUuid
         const res = await getChatDetail({
           page: this.page,
-          size: 10,
+          size: 100,
           sort: 'sortKey,desc',
           conversationId: this.msgUuid
         })
-        console.log(res);
-        
         this.messages = res.data.list.reverse().map(_ => ({
           id:_.id,
           type: _.type,
@@ -46,7 +44,7 @@ export const useMsgStore = defineStore('msg', {
           addToHistory: _.addToHistory,
           content: _.role === 'system' ?  formatMessage(_.rawContent) : _.content
         })).concat(this.messages)
-        if (this.messages.length < res.total) {
+        if (this.messages.length < res.data.total) {
           this.hasNext = true
           this.page++
         }

+ 1 - 1
src/store/modules/user.ts

@@ -125,7 +125,7 @@ export const useUserStore = defineStore('user', {
           ElMessage.success('登陆成功')
           return true
         } else {
-          ElMessage.error(res.message || '登录失败')
+          ElMessage.error(res.msg || '登录失败')
           return false
         }
       } catch (error: any) {

+ 1 - 1
src/utils/request.js

@@ -92,7 +92,7 @@ service.interceptors.response.use(
       // }
       ElMessage.error('您的登录状态已过期,请重新登录。')
       const userStore = useUserStore()
-      userStore.logout()
+      userStore.logout(false)
       return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
     } else if (code === 500) {
       ElMessage({ message: msg, type: 'error', grouping: true })