Procházet zdrojové kódy

Merge branch 'chd' of http://192.168.1.202:8087/chd/pvs_agent into wzg

wzg před 4 měsíci
rodič
revize
b2aac0a1fb
33 změnil soubory, kde provedl 3278 přidání a 547 odebrání
  1. 4 1
      .env
  2. 5 1
      package.json
  3. 104 0
      src/api/index.js
  4. binární
      src/assets/images/user.png
  5. 79 14
      src/entrypoints/background.js
  6. 156 107
      src/entrypoints/content.js
  7. 26 81
      src/entrypoints/sidepanel/App.vue
  8. 142 94
      src/entrypoints/sidepanel/Chat.vue
  9. 554 0
      src/entrypoints/sidepanel/component/Login.vue
  10. 114 0
      src/entrypoints/sidepanel/component/UserDropdown.vue
  11. 153 0
      src/entrypoints/sidepanel/component/UserProfile.vue
  12. 296 0
      src/entrypoints/sidepanel/component/Verify/Verify/VerifyPoints.vue
  13. 456 0
      src/entrypoints/sidepanel/component/Verify/Verify/VerifySlide.vue
  14. 377 0
      src/entrypoints/sidepanel/component/Verify/index.vue
  15. 5 5
      src/entrypoints/sidepanel/component/document.vue
  16. 43 140
      src/entrypoints/sidepanel/component/historyComponent.vue
  17. 6 7
      src/entrypoints/sidepanel/component/tools.vue
  18. 9 0
      src/entrypoints/sidepanel/directives/index.js
  19. 35 0
      src/entrypoints/sidepanel/directives/permission/hasPerm.js
  20. 33 0
      src/entrypoints/sidepanel/directives/permission/hasRole.js
  21. 57 41
      src/entrypoints/sidepanel/hook/useMsg.ts
  22. 2 1
      src/entrypoints/sidepanel/main.ts
  23. 3 1
      src/entrypoints/sidepanel/utils/ai-service.js
  24. 39 0
      src/entrypoints/sidepanel/utils/encrypt.js
  25. 2 2
      src/entrypoints/sidepanel/utils/index.js
  26. 39 0
      src/entrypoints/sidepanel/utils/verify.js
  27. 50 4
      src/store/modules/msg.ts
  28. 219 0
      src/store/modules/user.ts
  29. 108 0
      src/utils/contentUtils.js
  30. 6 0
      src/utils/errorCode copy.js
  31. 110 1
      src/utils/page-analyzer.js
  32. 45 46
      src/utils/request.js
  33. 1 1
      wxt.config.ts

+ 4 - 1
.env

@@ -1,2 +1,5 @@
 VITE_OPENAI_API_KEY_TONG=sk-e9855234f47346049809ce23ed3ebe3f
-VITE_MAX_FILE_NUMBER=10
+VITE_MAX_FILE_NUMBER=10
+VITE_APP_BASE_API='http://192.168.1.166:7777'
+# 终端ID
+VITE_CLIENT_ID = '765be25e4e78b101b896cb3ecac39b1b'

+ 5 - 1
package.json

@@ -18,17 +18,21 @@
   "dependencies": {
     "@element-plus/icons-vue": "^2.3.1",
     "axios": "^1.7.9",
+    "crypto-js": "^4.2.0",
     "element-plus": "^2.9.1",
     "highlight.js": "^11.11.1",
     "html2canvas": "^1.4.1",
+    "jsencrypt": "^3.3.2",
     "lodash": "^4.17.21",
     "moment": "^2.30.1",
     "openai": "^4.85.4",
     "pinia": "^3.0.1",
     "sass": "^1.85.1",
+    "uuid": "^11.1.0",
     "vant": "^4.9.17",
     "vite-plugin-svg-icons": "^2.0.1",
     "vue": "^3.5.12",
+    "vue-qrcode": "^2.2.2",
     "xlsx": "^0.18.5"
   },
   "devDependencies": {
@@ -42,4 +46,4 @@
     "vue-tsc": "^2.1.10",
     "wxt": "^0.19.13"
   }
-}
+}

+ 104 - 0
src/api/index.js

@@ -0,0 +1,104 @@
+import request from '@/utils/request'
+
+export function login(data) {
+    return request({
+        url: '/auth/login',
+        method: 'post',
+        data: data
+    })
+}
+export function loginOut(data) {
+    return request({
+        url: '/auth/logout',
+        method: 'post',
+        data: data
+    })
+}
+export function getUserInfo() {
+    return request({
+        url: '/auth/user/info',
+        method: 'get',
+    })
+}
+export function getChatList(data) {
+    return request({
+        url: '/messages/history/selConversationList',
+        method: 'get',
+        data: data
+    })
+}
+export function getChatDetail(data) {
+    return request({
+        url: '/messages/history/pageList',
+        method: 'get',
+        data: data
+    })
+}
+
+export function deleteChat(data) {
+    return request({
+        url: '/messages/history/delMessages',
+        method: 'post',
+        data: data
+    })
+}
+
+export function putChat(data) {
+    return request({
+        url: '/messages/history',
+        method: 'post',
+        data: data
+    })
+}
+
+export function getQRcodeResult(data) {
+    return request({
+        url: '/statistics/frontList',
+        method: 'put',
+        data: data
+    })
+}
+export function getQRcode(data) {
+    return request({
+        url: '/statistics/frontList',
+        method: 'put',
+        data: data
+    })
+}
+export function getCode(data) {
+    return request({
+        url: '/statistics/frontList',
+        method: 'put',
+        data: data
+    })
+}
+
+export function getWeChatLoginCode({source}) {
+    return request({
+        url: `/auth/${source}`,
+        method: 'get',
+    })
+}
+
+export function getBehaviorCaptcha(data) {
+    return request({
+        url: `/captcha/behavior`,
+        method: 'get',
+        data: data
+    })
+}
+export function checkBehaviorCaptcha(data) {
+    return request({
+        url: `/captcha/behavior`,
+        method: 'post',
+        data: data
+    })
+}
+
+export function getSmsCaptcha(data) {
+    return request({
+        url: `/captcha/sms`,
+        method: 'get',
+        data
+    })
+}

binární
src/assets/images/user.png


+ 79 - 14
src/entrypoints/background.js

@@ -1,10 +1,33 @@
 export default defineBackground(() => {
   let currentTabId
+  let arr = []
+  let token = ''
+  let userId = ''
+  const url = 'http://192.168.1.166:7777/behavior/user/adds'
   // Executed when background is loaded
   chrome.sidePanel
     .setPanelBehavior({ openPanelOnActionClick: true })
     .catch((error) => console.error(error))
   chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
+    if (message.type === 'CLICK_EVENT') {
+      if (token) {
+        arr.push({
+          ...message.data,
+          userId: token
+        })
+        if (arr.length > 10) {
+          fetch(url, {
+            method: 'POST',
+            headers: {
+              'Content-Type': 'application/json',
+              'Authorization': 'Bearer ' + token
+            },
+            body: JSON.stringify(arr.map(_ => ({ ..._, userId })))
+          }).then(res => 1)
+          arr = []
+        }
+      }
+    }
     if (message.type === 'PAGE_INFO') {
       // 转发到侧边栏
       chrome.runtime.sendMessage({
@@ -12,17 +35,12 @@ export default defineBackground(() => {
       })
     }
     if (message.type === 'FROM_CONTENT_TO_SEND_PAGE_FORM') {
-      console.log(message)
       chrome.runtime.sendMessage({
         type: 'TO_SIDE_PANEL_FORM_INFO', data: message.data
       })
-      return true
     }
     if (message.type === 'FROM_SIDE_PANEL_TO_ACTION') {
-      console.log(565888)
       chrome.tabs.query({ active: true }, (tabs) => {
-        console.log(tabs)
-
         if (tabs.length === 0) return // 确保有活动标签页
         const tabId = tabs[0].id // 获取当前活动的 tabId
         chrome.tabs.sendMessage(tabId, { type: 'GET_TAG_ACTION', data: message.data }, (response) => {
@@ -41,16 +59,19 @@ export default defineBackground(() => {
       chrome.tabs.query({ active: true }, (tabs) => {
         if (tabs.length === 0) return // 确保有活动标签页
         const tabId = tabs[0].id // 获取当前活动的 tabId
-        chrome.tabs.sendMessage(tabId, { type: 'GET_PAGE_FORM', data: 'Hello from background!' }, (response) => {
-          if (chrome.runtime.lastError) {
-            console.error('消息发送失败:', chrome.runtime.lastError.message)
-          } else {
-            console.log('收到 content script 响应:', response)
-            sendResponse(response)
+        chrome.tabs.sendMessage(
+          tabId,
+          { type: 'GET_PAGE_FORM', data: 'Hello from background!' },
+          (response) => {
+            if (chrome.runtime.lastError) {
+              console.error('消息发送失败:', chrome.runtime.lastError.message)
+            } else {
+              console.log('收到 content script 响应:', response)
+              sendResponse(response)
+            }
+            return true
           }
-
-          return true
-        })
+        )
       })
       return true
     }
@@ -102,6 +123,50 @@ export default defineBackground(() => {
       return true
     }
   })
+  let timer
+  // chrome.tabs.onRemoved.addListener(function (...a) {
+  //   clearTimeout(timer)
+  //   timer = setTimeout(() => {
+  //     chrome.tabs.query({}, function (tabs) {
+  //       fetch(url, {
+  //         method: 'POST',
+  //         headers: {
+  //           'Content-Type': 'application/json',
+  //         },
+  //         body: JSON.stringify(arr)
+  //       }).then(res => 1)
+  //       arr = []
+  //     });
+  //   }, 0)
+  // });
+  chrome.storage.onChanged.addListener((changes, areaName) => {
+    console.log('[Storage Changed] Area:', areaName);
+    // 遍历所有被修改的键
+    for (const [key, { oldValue, newValue }] of Object.entries(changes)) {
+      console.log(key);
+
+      if (key === 'token') {
+        token = newValue
+        if (!newValue) {
+          chrome.runtime.sendMessage({
+            type: 'USER_LOGOUT',
+          }, function (response) {
+          });
+          break
+        }
+      }
+      if (key === 'userInfo') {
+        if (!newValue) {
+          chrome.runtime.sendMessage({
+            type: 'USER_LOGOUT',
+          }, function (response) {
+          });
+          break
+        }
+        userId = JSON.parse(newValue).id
+      }
+    }
+  });
   chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
     if (changeInfo.status === 'complete' && tab.url) {
       chrome.runtime.sendMessage({

+ 156 - 107
src/entrypoints/content.js

@@ -9,7 +9,8 @@ import {
   findLabelForTag,
   findLabelForSpan,
   findLabelForTextarea,
-  getPageInfo
+  getPageInfo,
+  cleanPage2
 } from '../utils/contentUtils'
 import { defineContentScript } from 'wxt/sandbox'
 import { domToCanvas } from '@/entrypoints/sidepanel/utils/index.js'
@@ -26,124 +27,172 @@ export default defineContentScript({
     let form = null
     let formChildren = []
     let excelDataA = {}
-    chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
-      if (message.type === 'GET_PAGE_INFO') {
-        sendResponse({
-          data: getPageInfo()
-        })
-      }
-      let dom = null
-      if (message.type === 'GET_TAG_ACTION') {
-        console.log(message.data)
-        const data = message.data
-        if (0) {
-          dom = document.getElementById(data.id)
-        } else {
-          dom = [...document.getElementsByTagName(data.tag.toLowerCase())]
-            // .filter(_ => _.className.includes(data.class))
-            .filter((_) => _.innerText.includes(data.text))[0]
-        }
-        console.log(dom)
-        if (!dom) {
-          sendResponse({ data: '未找到元素,请重试', statue: 'error' })
-          return
+    let timer
+    function handleMouseover(e) {
+      clearTimeout(timer)
+      timer = setTimeout(() => {
+        if (tag === e.target.outerHTML && innerText === e.target.innerText) return
+        if (e.target.outerHTML.length > 1000) return
+        tag = e.target.outerHTML
+        innerText = e.target.innerText
+        console.log(e.target.outerHTML.length, e.target.outerHTML);
+        chrome.runtime.sendMessage({
+          type: 'CLICK_EVENT',
+          data: {
+            outerHtml: cleanPage2(e.target),
+            innerText: e.target.innerText,
+            url: window.location.href,
+            tag: e.target.tagName.toLowerCase(),
+            clickTime: +new Date()
+          }
+        }, function (response) {
+        });
+      }, 1000);
+    }
+
+    let tag
+    let innerText
+    function handleClick(e) {
+      if (tag === e.target.outerHTML && innerText === e.target.innerText) return
+      if (e.target.outerHTML.length > 1000) return
+      tag = e.target.outerHTML
+      innerText = e.target.innerText
+      console.log(e.target.outerHTML.length, e.target.outerHTML);
+      clearTimeout(timer)
+      chrome.runtime.sendMessage({
+        type: 'CLICK_EVENT',
+        data: {
+          outerHtml: cleanPage2(e.target),
+          innerText: e.target.innerText,
+          url: window.location.href,
+          tag: e.target.tagName.toLowerCase(),
+          clickTime: +new Date()
         }
-        // 添加红色边框,500ms后移除边框,然后点击
-        const originalBorder = dom.style.border
-        dom.style.border = '2px solid red'
-        setTimeout(() => {
-          dom.style.border = originalBorder
-          dom.click()
-        }, 1000)
-        sendResponse({ data: '完成' })
-      }
-      if (message.type === 'GET_PAGE_FORM') {
-        const forms = document.querySelectorAll('form')
-        if (forms.length === 0) {
+      }, function (response) {
+      });
+    }
+    document.addEventListener('click', handleClick)
+    document.addEventListener('mouseover', handleMouseover)
+    chrome.runtime.onMessage.addListener(
+      async (message, sender, sendResponse) => {
+        if (message.type === 'GET_PAGE_INFO') {
           sendResponse({
-            status: 'error', message: '没有找到表单'
+            data: getPageInfo()
           })
-          return
         }
-        if (forms.length === 1) {
-          form = forms[0]
-          const cloneForm = forms[0].cloneNode(true)
-          cloneForm.querySelectorAll('svg').forEach((el) => el.remove())
-          formChildren = [...form.elements]
-          sendResponse({
-            status: 'ok', data: cloneForm.outerHTML
-          })
-          return
+        let dom = null
+        if (message.type === 'GET_TAG_ACTION') {
+          console.log(message.data)
+          const data = message.data
+          if (0) {
+            dom = document.getElementById(data.id)
+          } else {
+            dom = [...document.getElementsByTagName(data.tag.toLowerCase())]
+              // .filter(_ => _.className.includes(data.class))
+              .filter((_) => _.innerText.includes(data.text))[0]
+          }
+          console.log(dom)
+          if (!dom) {
+            sendResponse({ data: '未找到元素,请重试', statue: 'error' })
+            return
+          }
+          // 添加红色边框,500ms后移除边框,然后点击
+          const originalBorder = dom.style.border
+          dom.style.border = '2px solid red'
+          setTimeout(() => {
+            dom.style.border = originalBorder
+            dom.click()
+          }, 1000)
+          sendResponse({ data: '完成' })
         }
-        sendResponse({
-          status: 'select'
-        })
-        for (const item of forms) {
-          item.style.border = '2px solid red'
-
-          function handleClick(e) {
-            e.stopPropagation()
-            console.log(this.outerHTML)
-            for (const form of forms) {
-              form.style.border = 'none'
-              form.removeEventListener('click', handleClick, true)
-            }
-            form = this
-            console.log(form, 5855)
-            const cloneForm = this.cloneNode(true)
+        if (message.type === 'GET_PAGE_FORM') {
+          const forms = document.querySelectorAll('form')
+          if (forms.length === 0) {
+            sendResponse({
+              status: 'error',
+              message: '没有找到表单'
+            })
+            return
+          }
+          if (forms.length === 1) {
+            form = forms[0]
+            const cloneForm = forms[0].cloneNode(true)
             cloneForm.querySelectorAll('svg').forEach((el) => el.remove())
             formChildren = [...form.elements]
-            // sendResponse({
-            //   type: 'FROM_CONTENT_TO_SEND_PAGE_FORM',
-            //   data: this.outerHTML
-            // })
-            chrome.runtime.sendMessage({
-              type: 'FROM_CONTENT_TO_SEND_PAGE_FORM', data: cloneForm.outerHTML
+            sendResponse({
+              status: 'ok',
+              data: cloneForm.outerHTML
             })
+            return
+          }
+          sendResponse({
+            status: 'select'
+          })
+          for (const item of forms) {
+            item.style.border = '2px solid red'
+            function handleClick(e) {
+              e.stopPropagation()
+              console.log(this.outerHTML)
+              for (const form of forms) {
+                form.style.border = 'none'
+                form.removeEventListener('click', handleClick, true)
+              }
+              form = this
+              console.log(form, 5855)
+              const cloneForm = this.cloneNode(true)
+              cloneForm.querySelectorAll('svg').forEach((el) => el.remove())
+              formChildren = [...form.elements]
+              // sendResponse({
+              //   type: 'FROM_CONTENT_TO_SEND_PAGE_FORM',
+              //   data: this.outerHTML
+              // })
+              chrome.runtime.sendMessage({
+                type: 'FROM_CONTENT_TO_SEND_PAGE_FORM',
+                data: cloneForm.outerHTML
+              })
+            }
+            item.addEventListener('click', handleClick, true)
           }
+          // if (!form) {
+          //   sendResponse({
+          //     status: 'error',
+          //     message: '没有找到表单'
+          //   })
+          //   return
+          // }
+          // sendResponse({
+          //   status: 'ok',
+          //   data: form.outerHTML
+          // })
+          // if (!form && document.querySelector("input")) {
+          //     const arr = []
+          //     const inputs = document.querySelectorAll("input")
+          //     formChildren = [...inputs]
 
-          item.addEventListener('click', handleClick, true)
+          //     for (const element of inputs) {
+          //         arr.push(element.outerHTML)
+          //     }
+          //     form = { outerHTML: JSON.stringify(arr) }
+          // }
+        }
+        if (message.type === 'INPUT_FORM') {
+          const { formData, excelData } = message.data
+          excelDataA = excelData
+          console.log('12', formData, excelDataA)
+            (async () => {
+              await handleFillInput(formData, 0)
+            })()
+        }
+        if (message.type === 'SCREENSHOT') {
+          domToCanvas().then(res => {
+            sendResponse({ status: 'ok', data: res })
+          }).catch(err => {
+            sendResponse({ status: 'error', message: err })
+          })
+          return true
         }
-        // if (!form) {
-        //   sendResponse({
-        //     status: 'error',
-        //     message: '没有找到表单'
-        //   })
-        //   return
-        // }
-        // sendResponse({
-        //   status: 'ok',
-        //   data: form.outerHTML
-        // })
-        // if (!form && document.querySelector("input")) {
-        //     const arr = []
-        //     const inputs = document.querySelectorAll("input")
-        //     formChildren = [...inputs]
-
-        //     for (const element of inputs) {
-        //         arr.push(element.outerHTML)
-        //     }
-        //     form = { outerHTML: JSON.stringify(arr) }
-        // }
-      }
-      if (message.type === 'INPUT_FORM') {
-        const { formData, excelData } = message.data
-        excelDataA = excelData
-        console.log('12',formData, excelDataA)
-        (async () => {
-          await handleFillInput(formData, 0)
-        })()
-      }
-      if (message.type === 'SCREENSHOT') {
-        domToCanvas().then(res => {
-          sendResponse({ status: 'ok', data: res })
-        }).catch(err => {
-          sendResponse({ status: 'error', message: err })
-        })
         return true
-      }
-      return true
-    })
+      })
     const handleFillInput = async (data, index) => {
       console.log(data, 85888)
       console.log(formChildren, form)

+ 26 - 81
src/entrypoints/sidepanel/App.vue

@@ -1,103 +1,48 @@
 <template>
-  <!--  <div>-->
-  <!--    <a href="https://wxt.dev" target="_blank">-->
-  <!--      <img src="/wxt.svg" class="logo" alt="WXT logo" />-->
-  <!--    </a>-->
-  <!--    <a href="https://vuejs.org/" target="_blank">-->
-  <!--      <img src="@/assets/vue.svg" class="logo vue" alt="Vue logo" />-->
-  <!--    </a>-->
-  <!--  </div>-->
-  <!--  <HelloWorld msg="WXT + Vue" />-->
-  <Chat />
-  <!-- <div @click="() => openFile()">按钮</div> -->
-
+  <div class="app-container" v-if="userStore.userInfo">
+    <UserDropdown />
+    <Chat />
+  </div>
+  <Login v-else />
 </template>
 
 <script lang="ts" setup>
 import { provide } from 'vue'
 import Chat from '@/entrypoints/sidepanel/Chat.vue'
+import Login from '@/entrypoints/sidepanel/component/Login.vue'
+import UserDropdown from '@/entrypoints/sidepanel/component/UserDropdown.vue'
 import axios from '@/utils/request'
-import { onBeforeUnmount, onMounted } from 'vue'
+import { onBeforeUnmount, onMounted,onBeforeMount } from 'vue'
 import { useIndexedDB } from '@/entrypoints/sidepanel/hook/useIndexedDB'
+import { useUserStore } from '@/store/modules/user'
 
-const { openDB, ...args } = useIndexedDB({
-  dbName: 'chatDB',
-  version: localStorage.getItem('dbVersion') ? Number(localStorage.getItem('dbVersion')) : 1
-})
-
+const userStore = useUserStore()
 // 提供实例给子组件
-provide('indexedDBHook', args)
-
-
-async function openFile() {
-  try {
-    const [fileHandle] = await window.showOpenFilePicker()
-    const file = await fileHandle.getFile()
-    const contents = await file.text()
-    console.log(file)
-  } catch (err) {
-    console.error('Error accessing file:', err)
-  }
-}
 
-onMounted(() => {
+onMounted(async () => {
+  await userStore.initStore()
+  console.log(userStore.userInfo,884562)
+  chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
+    if (message.type === 'USER_LOGOUT') {
+      console.log(7777);
+      
+      userStore.isLogin &&  userStore.clearUserData()
+    }
+  })
   // 初始化数据库连接(不包含任何Store)
-  openDB()
   chrome.runtime.sendMessage({
     type: 'GET_CONTENT_INFO'
   })
-
 })
-
-async function saveFile() {
-  try {
-    const options = {
-      types: [
-        {
-          description: 'Text Files',
-          accept: { 'text/plain': ['.txt'] }
-        }
-      ]
-    }
-    const handle = await window.showSaveFilePicker(options)
-    const writable = await handle.createWritable()
-    await writable.write('Hello, World!')
-    await writable.close()
-    console.log('File saved successfully!')
-  } catch (err) {
-    console.error('Error saving file:', err)
-  }
-}
-
-async function openDirectory() {
-  try {
-    // 显示文件夹选择对话框
-    const directoryHandle = await window.showDirectoryPicker()
-    console.log(`Selected directory: ${directoryHandle.name}`)
-
-    // 读取文件夹内容
-    for await (const [name, handle] of directoryHandle.entries()) {
-      if (handle.kind === 'file') {
-        console.log(`File: ${name}`)
-        // 读取文件内容(可选)
-        const file = await handle.getFile()
-        console.log(`File content: ${await file.text()}`)
-      } else if (handle.kind === 'directory') {
-        console.log(`Sub-directory: ${name}`)
-      }
-    }
-  } catch (err) {
-    console.error('Error accessing directory:', err)
-  }
-}
-
-// 调用函数
-
-
 </script>
 
-
 <style scoped>
+.app-container {
+  position: relative;
+  width: 100%;
+  height: 100%;
+}
+
 .logo {
   height: 6em;
   padding: 1.5em;

+ 142 - 94
src/entrypoints/sidepanel/Chat.vue

@@ -7,12 +7,22 @@
     <!-- 消息列表 -->
     <div class="message-list w-full" v-else>
       <el-scrollbar ref="scrollbar" @scroll="handleScroll">
-        <div class="messages">
+        <!-- 加载更多指示器 -->
+        <div v-if="isLoadingMore" class="loading-more-indicator">
+          <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.isSelf ? 'self' : 'other']">
-            <el-avatar :size="32" :src="message.avatar" />
+            :class="['message-item', message.role === 'user' ? 'self' : 'other']">
+            <el-avatar :size="32" :src="message.role === 'user' ? userAvatar : avatar" />
             <div class="message-content">
-              <div class="content" v-if="!message.isSelf" :class="{ 'loading-content': message.content === '' }">
+              <div class="content" v-if="message.role === 'system'"
+                :class="{ 'loading-content': message.content === '' }">
                 <formTable v-if="message.type === 'form'" :content="message.rawContent" />
                 <span v-else v-html="message.content"></span>
                 <span class="loading-indicator" v-if="sendLoading && index === messages.length - 1">
@@ -21,17 +31,17 @@
                   <span class="dot"></span>
                 </span>
               </div>
-              <document v-else-if="message.type === 'document' && message.isSelf" :content="message.content"
-                        :rawContent="message.rawContent" />
+              <document v-else-if="message.type === 'document' && message.role === 'user'" :content="message.content"
+                :rawContent="message.rawContent" />
               <div v-else class="content">
                 <span v-if="message.type === ''">{{ message.content }}</span>
                 <span class="loading-indicator" v-if="sendLoading && index === messages.length - 1">
-                <span class="dot"></span>
-                <span class="dot"></span>
-                <span class="dot"></span>
-              </span>
+                  <span class="dot"></span>
+                  <span class="dot"></span>
+                  <span class="dot"></span>
+                </span>
               </div>
-              <div class="timestamp ">{{ moment(message.timestamp).format('MM-DD HH:mm') }}
+              <div class="timestamp ">{{ moment(message.sortKey).format('MM-DD HH:mm') }}
                 <span v-if="message.add" style="cursor: pointer;" @click="handleInput">填充</span>
               </div>
             </div>
@@ -41,32 +51,19 @@
       <ScrollToBottom :target="scrollbar" ref="scrollToBottomRef" />
     </div>
 
-    <Tools @read-click="readClick"
-           @upload-file="handleUpload"
-           @handle-capture="handleCapture"
-           @his-records="hisRecords"
-           @add-new-dialogue="addNewDialogue"
-           @handle-current-change="handleCurrentChange"
-           @handel-intelligent-filling-click="handelIntelligentFillingClick" />
+    <Tools @read-click="readClick" @upload-file="handleUpload" @handle-capture="handleCapture" @his-records="hisRecords"
+      @add-new-dialogue="addNewDialogue" @handle-current-change="handleCurrentChange"
+      @handel-intelligent-filling-click="handelIntelligentFillingClick" />
 
     <div class="w-full max-w-[720px] p-[0_12px_12px]">
       <!-- 输入区域 -->
       <div class="input-area w-full">
         <div v-show="isShowPage" style="border-bottom: 1px solid #F0F0F0;">
           <div class="card_list">
-            <template v-for="(v,i) in pageInfoList" :key="i">
+            <template v-for="(v, i) in pageInfoList" :key="i">
               <div class="card_image " v-if="v.type === 'image'">
-                <el-image
-                  v-loading="v.isUpload"
-                  class="w-full h-full p-0.5 object-cover"
-                  :src="v.url"
-                  :zoom-rate="1.2"
-                  :max-scale="7"
-                  :min-scale="0.2"
-                  :preview-src-list="[v.url]"
-                  :initial-index="4"
-                  fit="cover"
-                />
+                <el-image v-loading="v.isUpload" class="w-full h-full p-0.5 object-cover" :src="v.url" :zoom-rate="1.2"
+                  :max-scale="7" :min-scale="0.2" :preview-src-list="[v.url]" :initial-index="4" fit="cover" />
                 <el-icon class="closeIcon" size="16px" color="#909399" @click="deletePageInfo(i)">
                   <CircleClose />
                 </el-icon>
@@ -93,7 +90,7 @@
         </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'}`"
@@ -102,7 +99,7 @@
             <svg-icon icon-class="send" color="#000000" />
           </el-button>
           <el-button style="background-color:#ffffff;border:2px solid rgb(134 143 153);" v-else circle
-                     @click="handleStopAsk">
+            @click="handleStopAsk">
             <svg-icon icon-class="stop" color="red" />
           </el-button>
 
@@ -113,7 +110,7 @@
 
 
     <!--  历史记录  -->
-    <historyComponent :msgUuid="msgUuid" ref="historyComponentRef" @currentData="(e)=>handleCurrentData(e)" />
+    <historyComponent :msgUuid="msgUuid" ref="historyComponentRef" @currentData="(e) => handleCurrentData(e)" />
   </div>
 </template>
 
@@ -121,7 +118,6 @@
 import { ref, onMounted, nextTick, inject, useTemplateRef, reactive } from 'vue'
 import { ElScrollbar, ElAvatar, ElInput, ElButton } from 'element-plus'
 import moment from 'moment'
-import { cloneDeep } from 'lodash'
 import fileLogo from '@/assets/svg/file.svg'
 import {
   buildExcelUnderstandingPrompt,
@@ -140,12 +136,20 @@ import document from '@/entrypoints/sidepanel/component/document.vue'
 import pageMask from '@/entrypoints/sidepanel/component/pageMask.vue'
 import ScrollToBottom from '@/entrypoints/sidepanel/component/ScrollToBottom.vue'
 import formTable from '@/entrypoints/sidepanel/component/formTable.vue'
-
+import userAvatar from '@/assets/images/user.png'
+import avatar from '@/public/icon/32.png'
 import { mockData, startMsg, mockData2, options, FunctionList } from '@/entrypoints/sidepanel/mock'
 import { useAutoResizeTextarea } from '@/entrypoints/sidepanel/hook/useAutoResizeTextarea.ts'
 import { getPageInfo, getXlsxValue, handleInput } from './utils/index.js'
 import { useMsgStore } from '@/store/modules/msg.ts'
-
+import { useUserStore } from '@/store/modules/user'
+import { putChat } from "@/api/index.js";
+import { debounce } from 'lodash'
+const userStore = useUserStore()
+import { getChatDetail } from '@/api/index.js'
+// 在其他状态变量附近添加
+const isLoadingMore = ref(false)
+import { v4 as uuidv4 } from 'uuid';
 // 滚动条引用
 const scrollbar = ref(null)
 const scrollToBottomRef = ref(null)
@@ -154,9 +158,8 @@ const isShowPage = ref(false)
 const tareRef = useTemplateRef('textareaRef')
 const drawerRef = useTemplateRef('historyComponentRef')
 // 获取父组件提供的 Hook 实例
-const { registerStore, useStore } = inject('indexedDBHook')
 const msgStore = useMsgStore()
-const { pageInfoList, messages, msgUuid, AIModel } = storeToRefs(useMsgStore())
+const { pageInfoList, messages, msgUuid, AIModel, page, hasNext } = storeToRefs(useMsgStore())
 const formInfo = ref('')
 
 const {
@@ -170,8 +173,6 @@ const {
 } = useMsg(scrollbar)
 const inputMessage = ref('')
 const pageInfo = ref('')
-
-
 function handleStopAsk() {
   if (type.value === FunctionList.Intelligent_Form_filling) {
     inputMessage.value = ''
@@ -185,46 +186,69 @@ function handleStopAsk() {
   sendLoading.value = false
 }
 
-function handleScroll({ scrollTop }) {
-  // scrollTop 滚动条的位置
+function handleScroll(a) {
   const { wrapRef } = scrollbar.value
   const { scrollHeight, clientHeight } = wrapRef
-  scrollToBottomRef.value.showButton = scrollHeight - scrollTop - clientHeight >= 350
+  scrollToBottomRef.value.showButton = scrollHeight - a.scrollTop - clientHeight >= 350
+
+  // 检测是否滚动到顶部
+  if (a.scrollTop <= 10 && hasNext.value) {
+    handleScrollToTop()
+  }
 }
+const messagesContainer = ref(null)
+let innerText = ''
+// 处理滚动到顶部的事件
+const handleScrollToTop = debounce(async function () {
+  console.log('滚动到顶部,可以加载更多历史消息')
+  if (!sendLoading.value && !isLoadingMore.value) {
+    isLoadingMore.value = true
+    try {
+      // 记录当前第一条消息的位置
+      const firstMessage = messagesContainer.value?.firstElementChild.querySelector('.timestamp')
+      innerText = firstMessage.innerText
+      const oldHeight = firstMessage?.offsetTop || 0
+      await msgStore.changePage()
+      setTimeout(() => {
+        if (firstMessage) {
+          console.log([...messagesContainer.value.querySelectorAll('.timestamp')].find(_ => _.innerText === innerText).offsetTop, innerText, 778);
+          const newHeight = [...messagesContainer.value.querySelectorAll('.timestamp')].find(_ => _.innerText === innerText).offsetTop
+          const scrollOffset = newHeight - oldHeight
+          scrollbar.value?.setScrollTop(scrollOffset)
+        }
+      }, 400);
+      // 恢复滚动位置
 
+    } finally {
+      isLoadingMore.value = false
+    }
+  }
+}, 500, { leading: false, trailing: true })
 /**
  * @param {string} msg
  * @param {string} raw
  * @param {string} type 发送的类型 document 、text
  * **/
 async function addMessage(msg, raw, type) {
-  // 添加indexDB Store配置
-  if (msgUuid.value === '') {
-    msgUuid.value = 'D' + Date.now().toString()
-    await registerStore({
-      name: msgUuid.value,
-      keyPath: 'id'
-    })
-  }
+  console.log(msg);
   const newMessage = reactive({
-    id: messages.value.length + 1,
+    id: +new Date(),
     type: type || '',
-    username: '我',
     rawContent: raw ?? msg,
+    senderId: userStore.userInfo.id,
+    receiverId: -1, //大模型
     content: msg,
-    timestamp: moment().format('YYYY-MM-DD HH:mm:ss'),
-    isSelf: true,
-    avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
-    addToHistory: !taklToHtml.value
+    sortKey: moment().valueOf(),
+    role: 'user',
+    conversationId: msgUuid.value,
+    addToHistory: `${!taklToHtml.value}`
   })
   if (type === 'document') {
     newMessage.content = msg
     newMessage.rawContent = raw
   }
   if (!msg) return
-
   messages.value.push(newMessage)
-  useStore(msgUuid.value).add(cloneDeep(newMessage))
   await nextTick(() => {
     scrollbar.value?.setScrollTop(99999)
   })
@@ -253,17 +277,18 @@ const handleSummary = async () => {
     })
     params = [{
       role: 'user',
-      content: `请根据以下这几内容综合总结出结果:
+      content: `请根据以下这几内容综合总结出结果:
       ${tempStr}要求:
       1. 用简洁清晰的语言提取所有的核心要点
       2. 保持客观中立的语气
       3. 按重要性排序
       4. 返回内容做好换行,以及展示样式
       5. 请以"以下是对该文件内容的总结:"开头,然后用要点的形式列出主要内容。
-      6. 对这几内容进行综合分析及联想`
+      6. 对这几内容进行综合分析及联想`
     }]
   }
-  await addMessage(pageInfoList.value, '总结', 'document')
+  const msg = await addMessage(JSON.stringify(pageInfoList.value), JSON.stringify(params.map(_ => _.content)), 'document')
+  putChat(msg)
   if (requestFlowFn) {
     isShowPage.value = false
     taklToHtml.value = false
@@ -297,20 +322,18 @@ function handleCurrentChange(e) {
   }
 }
 
-function handleCurrentData(e) {
+async function handleCurrentData(e) {
   drawerRef.value.drawer = false
   if (!e) {
     addNewDialogue()
     return
   }
-  // 添加indexDB Store配置
+  messages.value = []
+  page.value = 1
   msgUuid.value = e
-  useStore(e).getAll().then((res) => {
-    messages.value = res
-    nextTick(() => {
-      scrollbar.value?.setScrollTop(99999)
-    })
-  })
+  chrome.storage.local.set({ msgUuid: msgUuid.value })
+  await msgStore.initMsg()
+  scrollbar.value?.setScrollTop(99999)
 }
 
 async function readClick() {
@@ -344,29 +367,30 @@ function deletePageInfo(i) {
 }
 
 function addNewDialogue() {
-  if (msgUuid.value === '') {
+  if (messages.value.length === 0) {
     ElMessage.warning('已经是新对话')
     return
   }
   isShowPage.value = false
   taklToHtml.value = false
   messages.value = []
+  page.value = 1
   pageInfoList.value = []
-  msgUuid.value = ''
+  msgUuid.value = uuidv4()
+  chrome.storage.local.set({ msgUuid: msgUuid.value })
 }
 
 async function handleAsk() {
   const str = inputMessage.value.trim()
   inputMessage.value = ''
   if (sendLoading.value) return
-  await addMessage(str)
+  const msg = await addMessage(str)
+  putChat(msg)
   if (type.value === FunctionList.Intelligent_Form_filling) {
     const res = await fetchRes(str)
-    console.log(res)
-
     if (res.status === 'ok') {
       formInfo.value = res.data
-      console.log(res)
+      console.log(res, 55558)
     } else {
       type.value = FunctionList.File_Operation
       isShowPage.value = false
@@ -374,7 +398,6 @@ async function handleAsk() {
       pageInfoList.value = []
     }
   } else streamRes(taklToHtml.value)
-  inputMessage.value = ''
 }
 
 function handleCapture() {
@@ -398,7 +421,6 @@ function handleCapture() {
 function hisRecords() {
   drawerRef.value.drawer = true
 }
-
 const handleUpload = async (file) => {
   if (AIModel.value.file === true) {
     const id = await modelFileUpload(file)
@@ -420,7 +442,8 @@ const handleUpload = async (file) => {
         // if (!xlsxData.value[header]) xlsxData.value[header] = []
         xlsxData.value[header] = readData[1][i]
       })
-      addMessage(`已上传文件:${file.name}`, buildExcelUnderstandingPrompt(readData[0], file?.name, formInfo.value))
+      const msg = addMessage(`已上传文件:${file.name}`, buildExcelUnderstandingPrompt(readData[0], file?.name, formInfo.value))
+      putChat(msg)
       const { rawContent, status } = await streamRes()
       if (status === 'ok') {
         let form = []
@@ -429,8 +452,13 @@ const handleUpload = async (file) => {
         handleInput(xlsxData.value, form)
       }
     } else {
+      const msg = await addMessage(`文件上传中`)
       try {
-        const { data, msg } = await getFileValue(file)
+        sendLoading.value = true
+        putChat(msg)
+        const data = await getFileValue(file)
+        msg.content = `已上传文件:${file.name}`
+        msg.rawContent = data
         const res2 = await getFormKeyAndValue(data, formInfo.value)
         xlsxData.value = res2.data
         msg.rawContent = buildObjPrompt(res2.data, formInfo.value)
@@ -445,6 +473,10 @@ const handleUpload = async (file) => {
         msg.content = '文件解析出错'
       } finally {
         sendLoading.value = false
+        // putChat({
+        //   id: msgUuid.value,
+        //   msg: msg
+        // })
       }
     }
     type.value = FunctionList.File_Operation
@@ -468,25 +500,17 @@ const handleUpload = async (file) => {
     isShowPage.value = true
   }
 }
-
 async function getFileValue(file) {
-  const msg = addMessage(`文件上传中`)
-  sendLoading.value = true
   let formData = new FormData()
   formData.append('file', file)
   const res = await getFileContent(formData)
-  sendLoading.value = false
-  msg.content = `已上传文件:${file.name}`
-  msg.rawContent = res.data
-  return {
-    data: res.data,
-    msg
-  }
+  return res.data
 }
-
+let a = null
 // 组件挂载时滚动到底部
-onMounted(() => {
-  msgStore.updateAIModel({ ...options[0].options[0], api_sk: options[0]['api_sk'], api_url: options[0]['api_url'] })
+onMounted(async () => {
+  msgStore.updateAIModel(options[0].options[0])
+  await msgStore.initMsg()
   useAutoResizeTextarea(tareRef, inputMessage)
   chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
     if (message.type === 'TO_SIDE_PANEL_PAGE_INFO') {
@@ -501,7 +525,6 @@ onMounted(() => {
       pageInfo.value = message.data
     }
   })
-
   nextTick(() => {
     scrollbar.value?.setScrollTop(99999)
   })
@@ -510,4 +533,29 @@ onMounted(() => {
 
 <style lang="scss" scoped>
 @use '@/entrypoints/sidepanel/css/chat.scss';
+
+.loading-more-indicator {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 10px 0;
+  color: #909399;
+  font-size: 14px;
+
+  .loading-spinner {
+    width: 20px;
+    height: 20px;
+    margin-right: 8px;
+    border: 2px solid #e6e6e6;
+    border-top-color: #4d6bfe;
+    border-radius: 50%;
+    animation: spin 1s linear infinite;
+  }
+}
+
+@keyframes spin {
+  to {
+    transform: rotate(360deg);
+  }
+}
 </style>

+ 554 - 0
src/entrypoints/sidepanel/component/Login.vue

@@ -0,0 +1,554 @@
+<template>
+    <div class="login-container">
+        <div class="login-header">
+            <img :src="logo" alt="Logo" class="logo">
+            <h1>欢迎登录</h1>
+        </div>
+        <div class="login-tabs">
+            <div class="tab" :class="{ active: activeTab === 'phone' }" @click="activeTab = 'phone'">手机号登录</div>
+            <!-- <div class="tab" :class="{ active: activeTab === 'wechat' }" @click="activeTab = 'wechat'">微信登录</div> -->
+        </div>
+
+        <div class="login-content">
+            <div class="login-form phone-form" v-show="activeTab === 'phone'">
+                <div class="form-group">
+                    <div class="input-with-icon">
+                        <el-icon>
+                            <Iphone />
+                        </el-icon>
+                        <el-input v-model="phone" placeholder="请输入手机号" maxlength="11" />
+                    </div>
+                </div>
+                <div class="form-group">
+                    <div class="input-with-icon verification-code">
+                        <el-icon>
+                            <Lock />
+                        </el-icon>
+                        <el-input v-model="captcha" placeholder="请输入验证码" maxlength="6" />
+                        <el-button class="code-btn" :loading="captchaLoading" :disabled="captchaDisable"
+                            @click="getVerificationCode">
+                            {{ captchaBtnName }}
+                        </el-button>
+                    </div>
+                </div>
+                <div class="form-group">
+                    <el-button class="login-btn" type="primary" :loading="loading" @click="handlePhoneLogin">
+                        登录
+                    </el-button>
+                </div>
+            </div>
+            <div class="login-form wechat-form" v-show="activeTab === 'wechat'">
+                <div class="qrcode-container">
+                    <div class="qrcode" :class="{ expired: isQrcodeExpired }"
+                        @click="isQrcodeExpired && refreshQrcode()">
+                        <!-- 使用vue-qrcode组件 -->
+                        <qrcode-vue :value="qrcodeValue" :width="200" />
+                        <!-- 过期遮罩层 -->
+                        <div v-if="isQrcodeExpired" class="expired-mask">
+                            <el-icon :size="40">
+                                <Refresh />
+                            </el-icon>
+                            <p>点击刷新</p>
+                        </div>
+                    </div>
+                    <p class="qrcode-tip">请使用微信扫一扫登录</p>
+                    <p class="qrcode-refresh">二维码已失效?<a href="javascript:;" @click="refreshQrcode">点击刷新</a></p>
+                </div>
+            </div>
+            <div class="agreement">
+                <el-checkbox v-model="agreement">
+                    我已阅读并同意<a href="javascript:;">用户协议</a>和<a href="javascript:;">隐私政策</a>
+                </el-checkbox>
+            </div>
+        </div>
+
+        <div class="login-footer">
+
+            <!-- <div class="other-login">
+                <p>其他登录方式</p>
+                <div class="other-login-icons">
+                    <el-icon class="icon-item">
+                        <Connection />
+                    </el-icon>
+                    <el-icon class="icon-item">
+                        <Share />
+                    </el-icon>
+                </div>
+            </div> -->
+        </div>
+        <Verify ref="VerifyRef" :captcha-type="captchaType" :mode="captchaMode"
+            :img-size="{ width: '330px', height: '155px' }" @success="getCaptcha" />
+    </div>
+</template>
+
+<script setup>
+import { ref, onMounted, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+import { Iphone, Lock, Connection, Share } from '@element-plus/icons-vue'
+import QrcodeVue from 'vue-qrcode'
+import logo from '@/public/icon/icon128.png'
+import Verify from './Verify/index.vue'
+import { useUserStore } from '@/store/modules/user'
+import { getCode, getQRcode, getQRcodeResult, getWeChatLoginCode, getSmsCaptcha } from '@/api/index.js'
+// 获取user仓库实例
+const userStore = useUserStore()
+const VerifyRef = ref()
+const captchaMode = ref('pop')
+const captchaType = ref('blockPuzzle')
+const captchaTimer = ref()
+const captchaTime = ref(60)
+const captchaBtnName = ref('获取验证码')
+const captchaDisable = ref(false)
+const onCaptcha = async () => {
+    VerifyRef.value.instance.refresh()
+    VerifyRef.value.show()
+}
+const getCaptcha = async (captchaReq) => {
+    try {
+        captchaLoading.value = true
+        captchaBtnName.value = '发送中...'
+        await getSmsCaptcha({ phone: phone.value })
+        captchaLoading.value = false
+        captchaDisable.value = true
+        captchaBtnName.value = `获取验证码(${(captchaTime.value -= 1)}s)`
+        ElMessage.success('短信发送成功')
+        captchaTimer.value = window.setInterval(() => {
+            captchaTime.value -= 1
+            captchaBtnName.value = `获取验证码(${captchaTime.value}s)`
+            if (captchaTime.value <= 0) {
+                resetCaptcha()
+            }
+        }, 1000)
+    } catch (error) {
+        resetCaptcha()
+    } finally {
+        captchaLoading.value = false
+    }
+}
+const resetCaptcha = () => {
+    window.clearInterval(captchaTimer.value)
+    captchaTime.value = 60
+    captchaBtnName.value = '获取验证码'
+    captchaDisable.value = false
+}
+// 状态变量
+const activeTab = ref('phone')
+const isQrcodeExpired = ref(false)  // 添加二维码过期状态
+const phone = ref('')
+const captcha = ref('')
+const agreement = ref(false)
+const loading = ref(false)
+const captchaLoading = ref(false)
+
+// 二维码值
+const qrcodeValue = ref('https://example.com/login?token=' + Date.now())
+let countdown = 60
+let timer1 = null
+let timer2 = null
+// 刷新二维码
+const refreshQrcode = async () => {
+    // const result = await getQRcode()
+    qrcodeValue.value = 'https://example.com/login?token=' + Date.now()
+}
+
+// 获取验证码
+const getVerificationCode = async () => {
+    if (!validatePhone()) return
+    onCaptcha()
+}
+
+// 手机号登录
+const handlePhoneLogin = async () => {
+    if (!validatePhone()) return
+    if (!captcha.value) {
+        ElMessage.warning('请输入验证码')
+        return
+    }
+    if (!agreement.value) {
+        ElMessage.warning('请阅读并同意用户协议和隐私政策')
+        return
+    }
+    loading.value = true
+    try {
+        // 调用user仓库的登录方法
+        const loginResult = await userStore.login({
+            phone: phone.value,
+            captcha: captcha.value,
+            authType: "PHONE",
+            clientId: "ef51c9a3e9046c4f2ea45142c8a8344a"
+        })
+     
+    } catch (error) {
+        console.error('登录失败:', error)
+        ElMessage.error('登录失败,请稍后重试')
+    } finally {
+        loading.value = false
+    }
+}
+
+// 微信扫码登录处理
+const handleWechatLogin = async (token) => {
+    try {
+        loading.value = true
+        // 调用user仓库的登录方法,使用微信登录类型
+        const loginResult = await userStore.login({
+            type: 'wechat',
+            // 这里可以传递微信登录相关的参数
+            wechatToken: token || qrcodeValue.value.split('token=')[1]
+        })
+
+        if (loginResult) {
+            ElMessage.success('微信登录成功')
+            // 登录成功后可以执行其他操作
+        }
+    } catch (error) {
+        console.error('微信登录失败:', error)
+        ElMessage.error('微信登录失败,请稍后重试')
+    } finally {
+        loading.value = false
+    }
+}
+const checkQRcodeResult = async () => {
+    const res = await getQRcodeResult(qrcodeValue.value)
+    if (res.code === 200) {
+        await handleWechatLogin(res.data)
+    } else {
+        setTimeout(() => {
+            checkQRcodeResult()
+        }, 5000);
+    }
+}
+// 验证手机号
+const validatePhone = () => {
+    if (!phone.value) {
+        ElMessage.warning('请输入手机号')
+        return false
+    }
+    if (!/^1[3-9]\d{9}$/.test(phone.value)) {
+        ElMessage.warning('请输入正确的手机号码')
+        return false
+    }
+    return true
+}
+watch(activeTab, (newTab) => {
+  if (newTab === 'wechat') {
+    getWeChatLoginCode({source:'wechat_mp'}).then(res => {
+       qrcodeValue.value = res.authorizeUrl
+    })
+    //   qrcodeValue.value = 'https://example.com/login?token=' + Date.now()
+    //     setTimeout(() => {
+    //         checkQRcodeResult()
+    //     }, 1000);
+  }
+})
+
+// 组件挂载时的处理
+onMounted(() => {
+})
+
+</script>
+
+<style lang="scss" scoped>
+.login-container {
+    max-width: 100%;
+    width: 100%;
+    background-color: #f9fafc;
+    padding: 1rem;
+    display: flex;
+    flex-direction: column;
+}
+
+.login-header {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    margin-bottom: 36px;
+    padding-top: 30px;
+
+    .logo {
+        width: 70px;
+        height: 70px;
+        margin-bottom: 16px;
+        border-radius: 12px;
+        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+    }
+
+    h1 {
+        font-size: 24px;
+        font-weight: 600;
+        color: #333;
+        letter-spacing: 0.5px;
+    }
+}
+
+.login-tabs {
+    display: flex;
+    border-bottom: 1px solid #eaedf2;
+    background-color: #fff;
+    border-radius: 8px 8px 0 0;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
+
+    .tab {
+        flex: 1;
+        text-align: center;
+        padding: 16px 0;
+        font-size: 16px;
+        color: #666;
+        cursor: pointer;
+        position: relative;
+        transition: all 0.3s ease;
+
+        &.active {
+            color: #4361ee;
+            font-weight: 500;
+
+            &:after {
+                content: '';
+                position: absolute;
+                bottom: -1px;
+                left: 50%;
+                transform: translateX(-50%);
+                width: 40%;
+                height: 3px;
+                background-color: #4361ee;
+                border-radius: 3px 3px 0 0;
+                transition: all 0.3s ease;
+            }
+        }
+
+        &:hover:not(.active) {
+            color: #4361ee;
+            background-color: rgba(67, 97, 238, 0.04);
+        }
+    }
+}
+
+.login-content {
+    background-color: #fff;
+    border-radius: 0 0 8px 8px;
+    padding: 24px;
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
+    margin-bottom: 24px;
+}
+
+.login-form {
+    .form-group {
+        margin-bottom: 24px;
+    }
+
+    .input-with-icon {
+        display: flex;
+        align-items: center;
+        border: 1px solid #e0e5ec;
+        border-radius: 8px;
+        padding: 0 16px;
+        transition: all 0.3s ease;
+        background-color: #f9fafc;
+
+        &:focus-within {
+            border-color: #4361ee;
+            box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.1);
+        }
+
+        :deep(.el-input__wrapper) {
+            box-shadow: none;
+            padding: 0;
+            background-color: transparent;
+        }
+
+        :deep(.el-input__inner) {
+            height: 48px;
+            font-size: 15px;
+        }
+
+        .el-icon {
+            color: #a0aec0;
+            font-size: 18px;
+            margin-right: 8px;
+        }
+    }
+
+    .verification-code {
+        .code-btn {
+            border: none;
+            background: none;
+            color: #4361ee;
+            padding: 0 16px;
+            font-size: 14px;
+            font-weight: 500;
+            white-space: nowrap;
+            transition: all 0.3s ease;
+
+            &:hover:not(:disabled) {
+                color: #2d3fd9;
+            }
+
+            &:disabled {
+                color: #a0aec0;
+            }
+        }
+    }
+
+    .login-btn {
+        width: 100%;
+        height: 48px;
+        font-size: 16px;
+        font-weight: 500;
+        border-radius: 8px;
+        background: linear-gradient(135deg, #4361ee, #3a56d4);
+        border: none;
+        transition: all 0.3s ease;
+
+        &:hover {
+            transform: translateY(-2px);
+            box-shadow: 0 6px 15px rgba(67, 97, 238, 0.3);
+        }
+
+        &:active {
+            transform: translateY(0);
+        }
+    }
+}
+
+.qrcode-container {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    padding: 30px 0;
+
+    .qrcode {
+        width: 200px;
+        height: 200px;
+        margin-bottom: 20px;
+        padding: 10px;
+        background-color: #fff;
+        border-radius: 12px;
+        box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
+        img {
+            width:100%
+        }
+    }
+
+    .qrcode-tip {
+        font-size: 16px;
+        color: #333;
+        margin-bottom: 12px;
+        font-weight: 500;
+    }
+
+    .qrcode-refresh {
+        font-size: 14px;
+        color: #718096;
+
+        a {
+            color: #4361ee;
+            text-decoration: none;
+            font-weight: 500;
+            transition: all 0.3s ease;
+
+            &:hover {
+                color: #2d3fd9;
+                text-decoration: underline;
+            }
+        }
+    }
+}
+
+.login-footer {
+    margin-top: auto;
+    padding-top: 24px;
+
+    .agreement {
+        margin-bottom: 24px;
+        font-size: 14px;
+        color: #718096;
+        display: flex;
+        align-items: center;
+
+        :deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
+            background-color: #4361ee;
+            border-color: #4361ee;
+        }
+
+        a {
+            color: #4361ee;
+            text-decoration: none;
+            transition: all 0.3s ease;
+
+            &:hover {
+                color: #2d3fd9;
+                text-decoration: underline;
+            }
+        }
+    }
+
+    .other-login {
+        text-align: center;
+
+        p {
+            color: #a0aec0;
+            font-size: 14px;
+            margin-bottom: 20px;
+            position: relative;
+
+            &:before,
+            &:after {
+                content: '';
+                position: absolute;
+                top: 50%;
+                width: 70px;
+                height: 1px;
+                background-color: #e0e5ec;
+            }
+
+            &:before {
+                left: 40px;
+            }
+
+            &:after {
+                right: 40px;
+            }
+        }
+
+        .other-login-icons {
+            display: flex;
+            justify-content: center;
+            gap: 36px;
+
+            .icon-item {
+                font-size: 24px;
+                color: #718096;
+                cursor: pointer;
+                transition: all 0.3s ease;
+                background-color: #f9fafc;
+                width: 48px;
+                height: 48px;
+                border-radius: 50%;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+
+                &:hover {
+                    color: #4361ee;
+                    transform: translateY(-3px);
+                    box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1);
+                }
+            }
+        }
+    }
+}
+
+@media (min-width: 768px) {
+    // .login-container {
+    //     max-width: 420px;
+    //     margin: 0 auto;
+    //     box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+    //     border-radius: 12px;
+    //     background-color: #fff;
+    //     min-height: auto;
+    //     margin-top: 40px;
+    //     margin-bottom: 40px;
+    // }
+}
+</style>

+ 114 - 0
src/entrypoints/sidepanel/component/UserDropdown.vue

@@ -0,0 +1,114 @@
+<template>
+  <div class="user-dropdown">
+    <el-dropdown trigger="hover" @command="handleCommand">
+      <div class="avatar-container">
+        <el-icon :size="18"><Menu /></el-icon>
+        <!-- <span class="menu-text">菜单</span> -->
+      </div>
+      <template #dropdown>
+        <el-dropdown-menu>
+          <el-dropdown-item command="profile">
+            <el-icon><User /></el-icon>
+            <span>个人信息</span>
+          </el-dropdown-item>
+          <el-dropdown-item divided command="logout">
+            <el-icon><SwitchButton /></el-icon>
+            <span>退出登录</span>
+          </el-dropdown-item>
+        </el-dropdown-menu>
+      </template>
+    </el-dropdown>
+    
+    <UserProfile ref="userProfileRef" />
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue'
+import { useUserStore } from '@/store/modules/user'
+import { ElMessageBox } from 'element-plus'
+import { Menu, User, SwitchButton } from '@element-plus/icons-vue'
+import UserProfile from './UserProfile.vue'
+
+const userStore = useUserStore()
+const userProfileRef = ref()
+
+// 处理下拉菜单命令
+const handleCommand = (command: string) => {
+  if (command === 'logout') {
+    ElMessageBox.confirm(
+      '确定要退出登录吗?',
+      '提示',
+      {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      }
+    ).then(async () => {
+      await userStore.logout()
+    }).catch(() => {
+      // 取消退出
+    })
+  } else if (command === 'profile') {
+    userProfileRef.value?.open()
+  }
+}
+</script>
+
+<style scoped>
+.user-dropdown {
+  position: absolute;
+  top: 16px;
+  right: 16px;
+  z-index: 100;
+}
+
+.avatar-container {
+  display: flex;
+  align-items: center;
+  padding: 6px 12px;
+  cursor: pointer;
+  border-radius: 6px;
+  transition: all 0.3s;
+  background-color: #f5f7fa;
+  border: 1px solid transparent;
+}
+
+.avatar-container:hover {
+  background-color: #ecf5ff;
+  border-color: #c6e2ff;
+  color: #409EFF;
+}
+
+.menu-text {
+  margin-left: 6px;
+  font-size: 14px;
+  color: #606266;
+}
+
+.avatar-container:hover .menu-text {
+  color: #409EFF;
+}
+
+:deep(.el-dropdown-menu__item) {
+  display: flex;
+  align-items: center;
+  padding: 8px 20px;
+}
+
+:deep(.el-dropdown-menu__item .el-icon) {
+  margin-right: 8px;
+  font-size: 16px;
+}
+
+:deep(.el-dropdown-menu__item:not(.is-disabled):hover) {
+  background-color: #ecf5ff;
+  color: #409EFF;
+}
+
+:deep(.el-dropdown-menu) {
+  padding: 5px 0;
+  border-radius: 8px;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+}
+</style>

+ 153 - 0
src/entrypoints/sidepanel/component/UserProfile.vue

@@ -0,0 +1,153 @@
+<template>
+    <el-dialog title="个人信息" v-model="visible" width="360px" :close-on-click-modal="false" destroy-on-close>
+        <div class="user-profile">
+            <div class="profile-avatar">
+                <el-avatar :size="80" :src="userInfo?.avatar || defaultAvatar">
+                    {{ userInfo.username?.substring(0, 1) }}
+                </el-avatar>
+                <div class="user-name">{{ userInfo.username }}</div>
+            </div>
+            <div class="profile-info">
+                <div class="profile-item">
+                    <el-icon>
+                        <User />
+                    </el-icon>
+                    <span class="label">用户昵称:</span>
+                    <span class="value">{{ userInfo.nickname }}</span>
+                </div>
+                <div class="profile-item">
+                    <el-icon>
+                        <Phone />
+                    </el-icon>
+                    <span class="label">手机号码:</span>
+                    <span class="value">{{ userInfo.phone || '未设置' }}</span>
+                </div>
+                <div class="profile-item">
+                    <el-icon>
+                        <Male />
+                    </el-icon>
+                    <span class="label">性别:</span>
+                    <span class="value">{{ userInfo.gender === 1 ? '男' : userInfo.gender === 2 ? '女' : '未设置' }}</span>
+                </div>
+                <div class="profile-item">
+                    <el-icon>
+                        <Calendar />
+                    </el-icon>
+                    <span class="label">注册时间:</span>
+                    <span class="value">{{ userInfo.registrationDate || '未知' }}</span>
+                </div>
+            </div>
+        </div>
+    </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive } from 'vue'
+import moment from 'moment'
+import { useUserStore } from '@/store/modules/user'
+import { User, Phone, Male, Calendar } from '@element-plus/icons-vue'
+
+const visible = ref(false)
+const { userInfo } = useUserStore()
+    
+const defaultAvatar = 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
+
+// 格式化时间
+const formatTime = (time: string) => {
+  return time ? moment(time).format('YYYY-MM-DD HH:mm:ss') : '未知'
+}
+
+// 打开弹框时获取用户信息
+const open = async () => {
+  visible.value = true
+}
+
+// 暴露方法给父组件
+defineExpose({
+  open
+})
+</script>
+
+<style scoped>
+.user-profile {
+  /* padding: 10px; */
+  min-width: 340px;
+}
+
+.profile-avatar {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin-bottom: 30px;
+  padding-bottom: 20px;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.user-name {
+  margin-top: 12px;
+  font-size: 18px;
+  font-weight: 500;
+  color: #303133;
+}
+
+.profile-info {
+  padding: 0 15px;
+}
+
+.profile-item {
+  margin-bottom: 20px;
+  display: flex;
+  align-items: center;
+  padding: 10px 15px;
+  border-radius: 8px;
+  background-color: #f8f9fa;
+  transition: all 0.3s;
+}
+
+.profile-item:hover {
+  background-color: #ecf5ff;
+  transform: translateY(-2px);
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+}
+
+.profile-item .el-icon {
+  margin-right: 10px;
+  font-size: 18px;
+  color: #409EFF;
+}
+
+.label {
+  width: 100px;
+  color: #606266;
+  font-weight: 500;
+}
+
+.value {
+  color: #303133;
+  flex: 1;
+}
+
+:deep(.el-dialog__header) {
+  padding: 20px;
+  margin-right: 0;
+  text-align: center;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+:deep(.el-dialog__title) {
+  font-size: 18px;
+  font-weight: 600;
+  color: #303133;
+}
+
+:deep(.el-dialog__body) {
+  padding: 20px;
+}
+
+:deep(.el-dialog) {
+  border-radius: 8px;
+  overflow: hidden;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+  min-width: 340px;
+}
+</style>

+ 296 - 0
src/entrypoints/sidepanel/component/Verify/Verify/VerifyPoints.vue

@@ -0,0 +1,296 @@
+<template>
+  <div style="position: relative">
+    <div class="verify-img-out">
+      <div
+        class="verify-img-panel"
+        :style="{
+          'width': setSize.imgWidth,
+          'height': setSize.imgHeight,
+          'background-size': `${setSize.imgWidth} ${setSize.imgHeight}`,
+          'margin-bottom': `${vSpace}px`,
+        }"
+      >
+        <div
+          v-show="showRefresh"
+          class="verify-refresh"
+          style="z-index: 3"
+          @click="refresh"
+        >
+          <i class="iconfont icon-refresh"></i>
+        </div>
+        <img
+          ref="canvas"
+          :src="`data:image/png;base64,${pointBackImgBase}`"
+          alt=""
+          style="width: 100%; height: 100%; display: block"
+          @click="bindingClick ? canvasClick($event) : undefined"
+        />
+
+        <div
+          v-for="(tempPoint, index) in tempPoints"
+          :key="index"
+          class="point-area"
+          :style="{
+            'background-color': '#1abd6c',
+            'color': '#fff',
+            'z-index': 9999,
+            'width': '20px',
+            'height': '20px',
+            'text-align': 'center',
+            'line-height': '20px',
+            'border-radius': '50%',
+            'position': 'absolute',
+            'top': `${parseInt(`${tempPoint.y - 10}`)}px`,
+            'left': `${parseInt(`${tempPoint.x - 10}`)}px`,
+          }"
+        >
+          {{ index + 1 }}
+        </div>
+      </div>
+    </div>
+
+    <div
+      class="verify-bar-area"
+      :style="{
+        'width': setSize.imgWidth,
+        'color': barAreaColor,
+        'border-color': barAreaBorderColor,
+        'line-height': barSize.height,
+      }"
+    >
+      <span class="verify-msg">{{ text }}</span>
+    </div>
+  </div>
+</template>
+
+<script type="text/babel">
+import {
+  getCurrentInstance,
+  nextTick,
+  onMounted,
+  reactive,
+  ref,
+  toRefs,
+} from 'vue'
+import {
+  checkBehaviorCaptcha,
+  getBehaviorCaptcha,
+} from '@/api/index'
+import { resetSize } from '@/entrypoints/sidepanel/utils/verify'
+import { encryptByAes } from '@/entrypoints/sidepanel/utils/encrypt'
+
+export default {
+  name: 'VerifyPoints',
+  props: {
+    // 弹出式pop,固定fixed
+    mode: {
+      type: String,
+      default: '',
+    },
+    captchaType: {
+      type: String,
+    },
+    // 间隔
+    vSpace: {
+      type: Number,
+      default: 5,
+    },
+    imgSize: {
+      type: Object,
+      default() {
+        return {
+          width: '310px',
+          height: '155px',
+        }
+      },
+    },
+    barSize: {
+      type: Object,
+      default() {
+        return {
+          width: '310px',
+          height: '40px',
+        }
+      },
+    },
+  },
+  setup(props) {
+    const { mode, captchaType } = toRefs(props)
+    const { proxy } = getCurrentInstance()
+    const secretKey = ref('') // 后端返回的ase加密秘钥
+    const checkNum = ref(3) // 默认需要点击的字数
+    const fontPos = reactive([]) // 选中的坐标信息
+    const checkPosArr = reactive([]) // 用户点击的坐标
+    const num = ref(1) // 点击的记数
+    const pointBackImgBase = ref('') // 后端获取到的背景图片
+    const poinTextList = reactive([]) // 后端返回的点击字体顺序
+    const backToken = ref('') // 后端返回的token值
+    const setSize = reactive({
+      imgHeight: 0,
+      imgWidth: 0,
+      barHeight: 0,
+      barWidth: 0,
+    })
+    const tempPoints = reactive([])
+    const text = ref('')
+    const barAreaColor = ref()
+    const barAreaBorderColor = ref()
+    const showRefresh = ref(true)
+    const bindingClick = ref(true)
+
+    // 请求背景图片和验证图片
+    function getPicture() {
+      const data = {
+        captchaType: captchaType.value,
+      }
+      getBehaviorCaptcha(data).then((res) => {
+        pointBackImgBase.value = res.data.originalImageBase64
+        backToken.value = res.data.token
+        secretKey.value = res.data.secretKey
+        poinTextList.push(res.data.wordList)
+        text.value = `请依次点击【${poinTextList.join(',')}】`
+        poinTextList.length = 0
+      })
+    }
+
+    // 获取坐标
+    const getMousePos = function (e) {
+      const x = e.offsetX
+      const y = e.offsetY
+      return { x, y }
+    }
+
+    // 创建坐标点
+    const createPoint = function (pos) {
+      tempPoints.push({ ...pos })
+      return num.value + 1
+    }
+
+    // 坐标转换函数
+    const pointTransform = function (pointArr, imgSize) {
+      return pointArr.map((p) => {
+        const x = Math.round((310 * p.x) / Number.parseInt(imgSize.imgWidth, 10))
+        const y = Math.round((155 * p.y) / Number.parseInt(imgSize.imgHeight, 10))
+        return { x, y }
+      })
+    }
+
+    const init = () => {
+      // 加载页面
+      fontPos.splice(0, fontPos.length)
+      checkPosArr.splice(0, checkPosArr.length)
+      num.value = 1
+      getPicture()
+      nextTick(() => {
+        const { imgHeight, imgWidth, barHeight, barWidth } = resetSize(proxy)
+        setSize.imgHeight = imgHeight
+        setSize.imgWidth = imgWidth
+        setSize.barHeight = barHeight
+        setSize.barWidth = barWidth
+        proxy.$parent.$emit('ready', proxy)
+      })
+    }
+    onMounted(() => {
+      // 禁止拖拽
+      init()
+      proxy.$el.onselectstart = function () {
+        return false
+      }
+    })
+
+    const refresh = function () {
+      tempPoints.splice(0, tempPoints.length)
+      barAreaColor.value = '#000'
+      barAreaBorderColor.value = '#ddd'
+      bindingClick.value = true
+      fontPos.splice(0, fontPos.length)
+      checkPosArr.splice(0, checkPosArr.length)
+      num.value = 1
+      getPicture()
+      text.value = '验证失败'
+      showRefresh.value = true
+    }
+
+    const canvas = ref(null)
+    const canvasClick = (e) => {
+      checkPosArr.push(getMousePos(e))
+      if (num.value === checkNum.value) {
+        num.value = createPoint(getMousePos(e))
+        // 按比例转换坐标值
+        const arr = pointTransform(checkPosArr, setSize)
+        checkPosArr.length = 0
+        checkPosArr.push(...arr)
+        // 等创建坐标执行完
+        setTimeout(() => {
+          // 发送后端请求
+          const captchaVerification = secretKey.value
+            ? encryptByAes(
+                  `${backToken.value}---${JSON.stringify(checkPosArr)}`,
+                  secretKey.value,
+            )
+            : `${backToken.value}---${JSON.stringify(checkPosArr)}`
+          const data = {
+            captchaType: captchaType.value,
+            pointJson: secretKey.value
+              ? encryptByAes(JSON.stringify(checkPosArr), secretKey.value)
+              : JSON.stringify(checkPosArr),
+            token: backToken.value,
+          }
+          checkBehaviorCaptcha(data).then((res) => {
+            if (res.success && res.data.repCode === '0000') {
+              barAreaColor.value = '#4cae4c'
+              barAreaBorderColor.value = '#5cb85c'
+              text.value = '验证成功'
+              bindingClick.value = false
+              if (mode.value === 'pop') {
+                setTimeout(() => {
+                  proxy.$parent.clickShow = false
+                  refresh()
+                }, 1500)
+              }
+              proxy.$parent.$emit('success', { captchaVerification })
+            } else {
+              proxy.$parent.$emit('error', proxy)
+              barAreaColor.value = '#d9534f'
+              barAreaBorderColor.value = '#d9534f'
+              text.value = res.data.repMsg
+              setTimeout(() => {
+                refresh()
+              }, 700)
+            }
+          })
+        }, 400)
+      }
+      if (num.value < checkNum.value) {
+        num.value = createPoint(getMousePos(e))
+      }
+    }
+
+    return {
+      secretKey,
+      checkNum,
+      fontPos,
+      checkPosArr,
+      num,
+      pointBackImgBase,
+      poinTextList,
+      backToken,
+      setSize,
+      tempPoints,
+      text,
+      barAreaColor,
+      barAreaBorderColor,
+      showRefresh,
+      bindingClick,
+      init,
+      canvas,
+      canvasClick,
+      getMousePos,
+      createPoint,
+      refresh,
+      getPicture,
+      pointTransform,
+    }
+  },
+}
+</script>

+ 456 - 0
src/entrypoints/sidepanel/component/Verify/Verify/VerifySlide.vue

@@ -0,0 +1,456 @@
+<template>
+  <div style="position: relative">
+    <div
+      v-if="type === '2'"
+      class="verify-img-out"
+      :style="{ height: `${parseInt(setSize.imgHeight) + vSpace}px` }"
+    >
+      <div
+        class="verify-img-panel"
+        :style="{ width: setSize.imgWidth, height: setSize.imgHeight }"
+      >
+        <img
+          :src="`data:image/png;base64,${backImgBase}`"
+          alt=""
+          style="width: 100%; height: 100%; display: block"
+        />
+        <div v-show="showRefresh" class="verify-refresh" @click="refresh">
+          <i class="iconfont icon-refresh"></i>
+        </div>
+        <transition name="tips">
+          <span
+            v-if="tipWords"
+            class="verify-tips"
+            :class="passFlag ? 'suc-bg' : 'err-bg'"
+          >{{ tipWords }}</span>
+        </transition>
+      </div>
+    </div>
+    <!-- 公共部分 -->
+    <div
+      class="verify-bar-area"
+      :style="{
+        'width': setSize.imgWidth,
+        'height': barSize.height,
+        'line-height': barSize.height,
+      }"
+    >
+      <span class="verify-msg" v-text="text"></span>
+      <div
+        class="verify-left-bar"
+        :style="{
+          'width': leftBarWidth !== undefined ? leftBarWidth : barSize.height,
+          'height': barSize.height,
+          'border-color': leftBarBorderColor,
+          'transaction': transitionWidth,
+        }"
+      >
+        <span class="verify-msg" v-text="finishText"></span>
+        <div
+          class="verify-move-block"
+          :style="{
+            'width': barSize.height,
+            'height': barSize.height,
+            'background-color': moveBlockBackgroundColor,
+            'left': moveBlockLeft,
+            'transition': transitionLeft,
+          }"
+          @touchstart="start"
+          @mousedown="start"
+        >
+          <i
+            class="verify-icon iconfont" :class="[iconClass]"
+            :style="{ color: iconColor }"
+          ></i>
+          <div
+            v-if="type === '2'"
+            class="verify-sub-block"
+            :style="{
+              'width':
+                `${Math.floor((parseInt(setSize.imgWidth) * 47) / 310)}px`,
+              'height': setSize.imgHeight,
+              'top': `-${parseInt(setSize.imgHeight) + vSpace}px`,
+              'background-size': `${setSize.imgWidth} ${setSize.imgHeight}`,
+            }"
+          >
+            <img
+              :src="`data:image/png;base64,${blockBackImgBase}`"
+              alt=""
+              style="
+                width: 100%;
+                height: 100%;
+                display: block;
+                -webkit-user-drag: none;
+              "
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script type="text/babel">
+import {
+  computed,
+  getCurrentInstance,
+  nextTick,
+  onMounted,
+  reactive,
+  ref,
+  toRefs,
+  watch,
+} from 'vue'
+import {
+  checkBehaviorCaptcha,
+  getBehaviorCaptcha,
+} from '@/api/index'
+import { resetSize } from '@/entrypoints/sidepanel/utils/verify'
+import { encryptByAes } from '@/entrypoints/sidepanel/utils/encrypt'
+
+export default {
+  name: 'VerifySlide',
+  props: {
+    captchaType: {
+      type: String,
+    },
+    type: {
+      type: String,
+      default: '1',
+    },
+    // 弹出式pop,固定fixed
+    mode: {
+      type: String,
+      default: 'fixed',
+    },
+    vSpace: {
+      type: Number,
+      default: 5,
+    },
+    explain: {
+      type: String,
+      default: '向右滑动完成验证',
+    },
+    imgSize: {
+      type: Object,
+      default() {
+        return {
+          width: '310px',
+          height: '155px',
+        }
+      },
+    },
+    blockSize: {
+      type: Object,
+      default() {
+        return {
+          width: '50px',
+          height: '50px',
+        }
+      },
+    },
+    barSize: {
+      type: Object,
+      default() {
+        return {
+          width: '310px',
+          height: '40px',
+        }
+      },
+    },
+  },
+  setup(props) {
+    const { mode, captchaType, type, blockSize, explain } = toRefs(props)
+    const { proxy } = getCurrentInstance()
+    const secretKey = ref() // 后端返回的ase加密秘钥
+    const passFlag = ref() // 是否通过的标识
+    const backImgBase = ref() // 验证码背景图片
+    const blockBackImgBase = ref() // 验证滑块的背景图片
+    const backToken = ref() // 后端返回的唯一token值
+    const startMoveTime = ref() // 移动开始的时间
+    const endMovetime = ref() // 移动结束的时间
+    const tipsBackColor = ref() // 提示词的背景颜色
+    const tipWords = ref()
+    const text = ref()
+    const finishText = ref()
+    const setSize = reactive({
+      imgHeight: 0,
+      imgWidth: 0,
+      barHeight: 0,
+      barWidth: 0,
+    })
+    const top = ref(0)
+    const left = ref(0)
+    const moveBlockLeft = ref()
+    const leftBarWidth = ref()
+    // 移动中样式
+    const moveBlockBackgroundColor = ref()
+    const leftBarBorderColor = ref('#ddd')
+    const iconColor = ref()
+    const iconClass = ref('icon-right')
+    const status = ref(false) // 鼠标状态
+    const isEnd = ref(false) // 是够验证完成
+    const showRefresh = ref(true)
+    const transitionLeft = ref('')
+    const transitionWidth = ref('')
+    const startLeft = ref(0)
+
+    // 请求背景图片和验证图片
+    function getPicture() {
+      const data = {
+        captchaType: captchaType.value,
+      }
+      getBehaviorCaptcha(data).then((res) => {
+        backImgBase.value = res.data.originalImageBase64
+        blockBackImgBase.value = res.data.jigsawImageBase64
+        backToken.value = res.data.token
+        secretKey.value = res.data.secretKey
+      })
+    }
+    const barArea = computed(() => {
+      return proxy.$el.querySelector('.verify-bar-area')
+    })
+    // 鼠标移动
+    function move(e) {
+      e = e || window.event
+      if (status.value && isEnd.value === false) {
+        let x
+        if (!e.touches) {
+          // 兼容PC端
+          x = e.clientX
+        } else {
+          // 兼容移动端
+          x = e.touches[0].pageX
+        }
+        const bar_area_left = barArea.value.getBoundingClientRect().left
+        let move_block_left = x - bar_area_left // 小方块相对于父元素的left值
+        if (
+          move_block_left
+          >= barArea.value.offsetWidth
+          - Number.parseInt(blockSize.value.width, 10) / 2 - 2
+        ) {
+          move_block_left
+              = barArea.value.offsetWidth
+              - Number.parseInt(blockSize.value.width, 10) / 2 - 2
+        }
+        if (move_block_left <= 0) {
+          move_block_left = Number.parseInt(blockSize.value.width, 10) / 2
+        }
+        // 拖动后小方块的left值
+        moveBlockLeft.value = `${move_block_left - startLeft.value}px`
+        leftBarWidth.value = `${move_block_left - startLeft.value}px`
+      }
+    }
+
+    const refresh = () => {
+      showRefresh.value = true
+      finishText.value = ''
+
+      transitionLeft.value = 'left .3s'
+      moveBlockLeft.value = 0
+
+      leftBarWidth.value = undefined
+      transitionWidth.value = 'width .3s'
+
+      leftBarBorderColor.value = '#ddd'
+      moveBlockBackgroundColor.value = '#fff'
+      iconColor.value = '#000'
+      iconClass.value = 'icon-right'
+      isEnd.value = false
+
+      getPicture()
+      setTimeout(() => {
+        transitionWidth.value = ''
+        transitionLeft.value = ''
+        text.value = explain.value
+      }, 300)
+    }
+
+    // 鼠标松开
+    function end() {
+      endMovetime.value = +new Date()
+      // 判断是否重合
+      if (status.value && isEnd.value === false) {
+        let moveLeftDistance = Number.parseInt(
+          (moveBlockLeft.value || '').replace('px', ''),
+          10,
+        )
+        moveLeftDistance
+            = (moveLeftDistance * 310) / Number.parseInt(`${setSize.imgWidth}`, 10)
+        const data = {
+          captchaType: captchaType.value,
+          pointJson: secretKey.value
+            ? encryptByAes(
+              JSON.stringify({ x: moveLeftDistance, y: 5.0 }),
+              secretKey.value,
+            )
+            : JSON.stringify({ x: moveLeftDistance, y: 5.0 }),
+          token: backToken.value,
+        }
+        checkBehaviorCaptcha(data).then((res) => {
+          if (res.success && res.data.repCode === '0000') {
+            moveBlockBackgroundColor.value = '#5cb85c'
+            leftBarBorderColor.value = '#5cb85c'
+            iconColor.value = '#fff'
+            iconClass.value = 'icon-check'
+            showRefresh.value = false
+            isEnd.value = true
+            if (mode.value === 'pop') {
+              setTimeout(() => {
+                proxy.$parent.clickShow = false
+                refresh()
+              }, 1500)
+            }
+            passFlag.value = true
+            tipWords.value = `${(
+                (endMovetime.value - startMoveTime.value)
+                / 1000
+            ).toFixed(2)}s验证成功`
+            const captchaVerification = secretKey.value
+              ? encryptByAes(
+                    `${backToken.value}---${JSON.stringify({
+                      x: moveLeftDistance,
+                      y: 5.0,
+                    })}`,
+                    secretKey.value,
+              )
+              : `${backToken.value}---${JSON.stringify({
+                  x: moveLeftDistance,
+                  y: 5.0,
+                })}`
+            setTimeout(() => {
+              tipWords.value = ''
+              proxy.$parent.closeBox()
+              proxy.$parent.$emit('success', { captchaVerification })
+            }, 1000)
+          } else {
+            moveBlockBackgroundColor.value = '#d9534f'
+            leftBarBorderColor.value = '#d9534f'
+            iconColor.value = '#fff'
+            iconClass.value = 'icon-close'
+            passFlag.value = false
+            setTimeout(() => {
+              refresh()
+            }, 1000)
+            proxy.$parent.$emit('error', proxy)
+            tipWords.value = res.data.repMsg
+            setTimeout(() => {
+              tipWords.value = ''
+            }, 1000)
+          }
+        })
+        status.value = false
+      }
+    }
+
+    function init() {
+      text.value = explain.value
+      // getPicture()
+      nextTick(() => {
+        const { imgHeight, imgWidth, barHeight, barWidth } = resetSize(proxy)
+        setSize.imgHeight = imgHeight
+        setSize.imgWidth = imgWidth
+        setSize.barHeight = barHeight
+        setSize.barWidth = barWidth
+        proxy.$parent.$emit('ready', proxy)
+      })
+
+      window.removeEventListener('touchmove', (e) => {
+        move(e)
+      })
+      window.removeEventListener('mousemove', (e) => {
+        move(e)
+      })
+
+      // 鼠标松开
+      window.removeEventListener('touchend', () => {
+        end()
+      })
+      window.removeEventListener('mouseup', () => {
+        end()
+      })
+
+      window.addEventListener('touchmove', (e) => {
+        move(e)
+      })
+      window.addEventListener('mousemove', (e) => {
+        move(e)
+      })
+
+      // 鼠标松开
+      window.addEventListener('touchend', () => {
+        end()
+      })
+      window.addEventListener('mouseup', () => {
+        end()
+      })
+    }
+    watch(type, () => {
+      init()
+    })
+    onMounted(() => {
+      // 禁止拖拽
+      init()
+      proxy.$el.onselectstart = function () {
+        return false
+      }
+    })
+    // 鼠标按下
+    function start(e) {
+      e = e || window.event
+      let x
+      if (!e.touches) {
+        // 兼容PC端
+        x = e.clientX
+      } else {
+        // 兼容移动端
+        x = e.touches[0].pageX
+      }
+      startLeft.value = Math.floor(
+        x - barArea.value.getBoundingClientRect().left,
+      )
+      startMoveTime.value = +new Date() // 开始滑动的时间
+      if (isEnd.value === false) {
+        text.value = ''
+        moveBlockBackgroundColor.value = '#337ab7'
+        leftBarBorderColor.value = '#337AB7'
+        iconColor.value = '#fff'
+        e.stopPropagation()
+        status.value = true
+      }
+    }
+
+    return {
+      secretKey, // 后端返回的ase加密秘钥
+      passFlag, // 是否通过的标识
+      backImgBase, // 验证码背景图片
+      blockBackImgBase, // 验证滑块的背景图片
+      backToken, // 后端返回的唯一token值
+      startMoveTime, // 移动开始的时间
+      endMovetime, // 移动结束的时间
+      tipsBackColor, // 提示词的背景颜色
+      tipWords,
+      text,
+      finishText,
+      setSize,
+      top,
+      left,
+      moveBlockLeft,
+      leftBarWidth,
+      // 移动中样式
+      moveBlockBackgroundColor,
+      leftBarBorderColor,
+      iconColor,
+      iconClass,
+      status, // 鼠标状态
+      isEnd, // 是够验证完成
+      showRefresh,
+      transitionLeft,
+      transitionWidth,
+      barArea,
+      refresh,
+      start,
+    }
+  },
+}
+</script>

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 377 - 0
src/entrypoints/sidepanel/component/Verify/index.vue


+ 5 - 5
src/entrypoints/sidepanel/component/document.vue

@@ -3,16 +3,16 @@ import { onMounted, computed } from 'vue'
 
 const props = defineProps({
   content: {
-    type: [String, Object],
+    type: String,
     default: ''
   },
   rawContent: {
-    type: [String, Object],
+    type: String,
     default: ''
   }
 })
 const docZil = computed(() => {
-  return props.content
+  return JSON.parse(props.content)
 })
 </script>
 
@@ -25,7 +25,7 @@ const docZil = computed(() => {
         <p class="els" :title="item.url" style="color: rgba(96,98,102,0.77)">{{ item.url }}</p>
       </div>
     </div>
-    <p class="document_content1">{{ props.rawContent }}</p>
+    <p class="document_content1">总结</p>
   </div>
 
 </template>
@@ -73,4 +73,4 @@ const docZil = computed(() => {
   border-radius: 6px;
   background-color: rgba(114, 118, 139, .06);
 }
-</style>
+</style>

+ 43 - 140
src/entrypoints/sidepanel/component/historyComponent.vue

@@ -2,6 +2,9 @@
 import { ref, inject } from 'vue'
 import { Search, Delete, Paperclip } from '@element-plus/icons-vue'
 import { ElMessageBox, ElMessage } from 'element-plus'
+import { getChatList,deleteChat } from '@/api/index.js'
+import { storeToRefs } from 'pinia';
+import { useMsgStore } from '@/store/modules/msg.ts'
 
 const drawer = ref(false)
 const count = ref(0)
@@ -9,9 +12,8 @@ const input = ref('')
 const loading = ref(false)
 const dataList = ref<any[]>([])
 // 获取父组件提供的 Hook 实例
-const { db, useStore, deleteDB } = inject('indexedDBHook') as any
 const emit = defineEmits(['currentData'])
-
+const { msgUuid, messages ,page} = storeToRefs(useMsgStore())
 const props = defineProps({
   msgUuid: {
     type: String,
@@ -20,98 +22,18 @@ const props = defineProps({
 })
 watch(drawer, (newVal) => {
   if (newVal) {
+    // console.log(getChatList)
     loading.value = true
-    getFirstTwoRecordsFromEachStore(db.value).then((data: any) => {
-      dataList.value = data.filter((item: any) => item !== null).reverse()
-      count.value = dataList.value.length || 0
-      loading.value = false
-      // console.log('First two records from each store:', data);
-    }).catch((error) => {
-      console.error('Error:', error)
-    })
+    getChatList({
+      page:1,
+      size:50
+    }).then(res => {
+      dataList.value = res.list
+      console.log(res.list);
+    }).finally(res => loading.value = false)
   }
 })
-
-function handleDeleteDB() {
-  ElMessageBox.confirm(
-    '此操作无法撤销。',
-    '删除全部?',
-    {
-      confirmButtonText: '确认',
-      cancelButtonText: '取消',
-      showClose: false,
-      type: 'warning',
-      center: true
-    }
-  ).then(() => {
-    deleteDB().then((res: any) => {
-      console.log('Database deleted successfully.', res)
-      // // 重新加载页面
-      // window.location.reload();
-    }).catch((error: any) => {
-      console.error('Error deleting database:', error)
-    })
-  }).catch(() => {
-  })
-}
-
-function getFirstTwoRecordsFromObjectStoreWithCursor(db: any, storeName: any) {
-  return new Promise((resolve, reject) => {
-    const transaction = db.transaction([storeName], 'readonly')
-    const objectStore = transaction.objectStore(storeName)
-    const request = objectStore.openCursor()
-    const results: any[] = []
-    let count = 0
-
-    request.onerror = (event: any) => {
-      reject(`Failed to get data from object store ${storeName}: ${event.target.error}`)
-    }
-
-    request.onsuccess = (event: any) => {
-      const cursor = event.target.result
-      if (cursor && count < 2) {
-        results.push(cursor.value)
-        count++
-        cursor.continue()
-      } else {
-        if (results.length) {
-          resolve({
-            storeName: storeName,
-            data: results
-          })
-        } else {
-          resolve(null)
-        }
-      }
-    }
-  })
-}
-
-async function getFirstTwoRecordsFromEachStore(db: any) {
-  return new Promise((resolve, reject) => {
-    // 获取所有对象存储的名称
-    const storeNames = Array.from(db.objectStoreNames)
-    const result: any[] = []
-
-    // 遍历每个对象存储并获取前 2 条数据
-    const promises = storeNames.map((storeName) => {
-      return getFirstTwoRecordsFromObjectStoreWithCursor(db, storeName)
-    })
-
-    // 等待所有对象存储的数据读取完成
-    Promise.all(promises).then((dataArrays) => {
-      // 将每个对象存储的数据合并到结果数组中
-      dataArrays.forEach((data: any) => {
-        result.push(data)
-      })
-      resolve(result)
-    }).catch((error) => {
-      reject(error)
-    })
-  })
-}
-
-function handleDeleteStore(e: any, item: any) {
+function handleDeleteStore(e: any, item: any,name:string) {
   e.stopPropagation()
   if (dataList.value.length < 2) {
     ElMessage({
@@ -123,7 +45,7 @@ function handleDeleteStore(e: any, item: any) {
   }
   ElMessageBox.confirm(
     '此操作无法撤销。',
-    '要删除此对话吗?',
+    `要删【${name}】对话吗?`,
     {
       confirmButtonText: '确认',
       cancelButtonText: '取消',
@@ -132,76 +54,57 @@ function handleDeleteStore(e: any, item: any) {
       center: true
     }
   ).then(() => {
-    useStore(item).clearAll().then((res: any) => {
+    loading.value = true
+    deleteChat([item]).then((res: any) => {
       loading.value = true
-      getFirstTwoRecordsFromEachStore(db.value).then((data: any) => {
-        dataList.value = data.filter((item: any) => item !== null).reverse()
-        count.value = dataList.value.length || 0
-        loading.value = false
-        // if (item === props.msgUuid) {
-        //   const result = getNextOrPreviousId(dataList.value, item);
-        //   emit('currentData', result)
-        // }
-      }).catch((error) => {
-        console.error('Error:', error)
-      })
+      if (item === msgUuid.value) {
+        messages.value = []
+        page.value = 1
+      }
+      getChatList({
+        page: 1,
+        size: 50
+      }).then(res => {
+        dataList.value = res.list
+      }).finally(res => loading.value = false)
+    }).catch(() => {
     })
-  }).catch(() => {
-  })
 
 
+  })
 }
-
-
-function getNextOrPreviousId(array: any, currentId: any) {
-  const currentIndex = array.findIndex((item: any) => item.storeName === currentId)
-
-  if (currentIndex === -1) {
-    throw new Error('当前 storeName 不在数组中')
-  }
-
-  if (currentIndex < array.length - 1) {
-    return array[currentIndex + 1].storeName // 返回下一个对象的 id
-  } else if (currentIndex > 0) {
-    return array[currentIndex - 1].storeName // 返回上一个对象的 id
-  }
-
-  return null // 没有下一个或上一个对象
-}
-
 defineExpose({
   drawer
 })
 </script>
 
 <template>
-  <el-drawer style="height: 70%" v-model="drawer" direction="btt" :show-close="true"
-             :close-on-click-modal="false" :destroy-on-close="true"
-             :close-on-press-escape="false" class="custom_drawer">
+  <el-drawer style="height: 70%" v-model="drawer" direction="btt" :show-close="true" :close-on-click-modal="false"
+    :destroy-on-close="true" :close-on-press-escape="false" class="custom_drawer">
 
     <template #header>
-      <div class="his_flex"><span class="his_title">历史聊天</span><span class="his_count">({{ count }})</span></div>
+      <div class="his_flex"><span class="his_title">历史聊天</span><span class="his_count">({{ dataList.length }})</span>
+      </div>
     </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-tooltip effect="dark" content="删除全部" placement="top">
+          :disabled="true" />
+        <!-- <el-tooltip effect="dark" content="删除全部" placement="top">
           <el-button :icon="Delete" circle @click="handleDeleteDB" />
-        </el-tooltip>
+        </el-tooltip> -->
       </div>
       <div class="his_content">
-        <template v-for="item in dataList" :key="item.storeName">
-          <div :class="`his_list ${msgUuid === item.storeName ? 'his_list_change' : '' }`"
-               @click="emit('currentData',item.storeName)">
-            <p class="ellipsis" style="color:#000000;font-weight: 900;">{{ item.data[0]?.content ?? '--' }}</p>
-            <p class="ellipsis" style="color: #888888">{{ item.data[1] ? item.data[1].content : '--' }}</p>
+        <template v-for="item in dataList" :key="item.conversationId">
+          <div :class="`his_list ${msgUuid === item.conversationId ? 'his_list_change' : '' }`"
+            @click="emit('currentData', item.conversationId)">
+            <p class="ellipsis" style="color:#000000;font-weight: 900;">{{ item?.content ?? '--' }}</p>
             <p class="his_list_op">
-              <span>{{ item.data[0]?.timestamp }}</span>
+              <span style="color: #000">{{ item?.createTime }}</span>
               <el-tooltip effect="dark" content="删除" placement="top">
-                <el-button :icon="Delete" link @click="(e:any)=>handleDeleteStore(e,item.storeName)" />
+                <el-button :disabled="msgUuid === item.conversationId" :icon="Delete" link
+                  @click="(e: any) => handleDeleteStore(e, item.conversationId, item?.content)" />
               </el-tooltip>
-
             </p>
           </div>
         </template>
@@ -299,4 +202,4 @@ defineExpose({
     background-color: rgba(122, 89, 255, .2) !important;
   }
 }
-</style>
+</style>

+ 6 - 7
src/entrypoints/sidepanel/component/tools.vue

@@ -19,24 +19,23 @@ watchEffect(() => {
   <div class="px-3 py-2 flex justify-between items-center w-full max-w-[720px]">
     <div class="flex items-center">
       <el-select placement="top" v-model="selectInput" placeholder="选择"
-                 @change="(e: any)=>emit('handleCurrentChange',e)"
-                 style="width: 120px;margin-right: 2px">
+        @change="(e: any) => emit('handleCurrentChange', e)" style="width: 120px;margin-right: 2px">
         <el-option-group v-for="group in options" :key="group.label" :label="group.label">
           <el-option v-for="item in group.options" :key="item.value" :label="item.label" :value="item.value" />
         </el-option-group>
       </el-select>
       <el-tooltip effect="dark" content="阅读此页,开启后将会根据左侧网页中的内容做出回答" placement="top">
-        <el-button class="tools_btn" link :icon="Reading" @click="emit('readClick')" />
+        <el-button v-permission="['agent:summary']" class="tools_btn" link :icon="Reading" @click="emit('readClick')" />
       </el-tooltip>
       <el-upload style="display:inline-block" :before-upload="(file: any) => emit('uploadFile', file)" :multiple="false"
-                 name="file" :show-file-list="false" :accept="'.xlsx,.pdf,.doc,.docx'">
+        name="file" :show-file-list="false" :accept="'.xlsx,.pdf,.doc,.docx'">
         <el-tooltip effect="dark" content="文件上传" placement="top">
-          <el-button class="tools_btn" link :icon="Paperclip" />
+          <el-button v-permission="['agent:upload']" class="tools_btn" link :icon="Paperclip" />
         </el-tooltip>
       </el-upload>
-      <el-tooltip effect="dark" content="截屏" placement="top">
+      <!-- <el-tooltip effect="dark" content="截屏" placement="top">
         <el-button class="tools_btn" link :icon="Scissor" @click="emit('handleCapture')" />
-      </el-tooltip>
+      </el-tooltip> -->
       <el-tooltip effect="dark" content="智能填表:选择后,在输入框描述填表流程" placement="top">
         <el-button class="tools_btn" link @click="emit('handelIntelligentFillingClick')">
           <span class="iconfont icon-zhinengtianxie"></span>

+ 9 - 0
src/entrypoints/sidepanel/directives/index.js

@@ -0,0 +1,9 @@
+import hasPerm from './permission/hasPerm'
+import hasRole from './permission/hasRole'
+
+export default {
+  install(Vue) {
+    Vue.directive('permission', hasPerm)
+    Vue.directive('role', hasRole)
+  },
+}

+ 35 - 0
src/entrypoints/sidepanel/directives/permission/hasPerm.js

@@ -0,0 +1,35 @@
+import { useUserStore } from '@/store/modules/user'
+
+/**
+ * @desc v-permission 操作权限处理
+ * @desc 使用 v-permission="['system:user:add']"
+ */
+function checkPermission(el, binding) {
+  const userStore = useUserStore()
+  const { value } = binding
+  const all_permission = '*:*:*'
+
+  if (value && Array.isArray(value) && value.length) {
+    const permissionValues = value
+    
+    const hasPermission = userStore.permissions.some((perm) => {
+      return all_permission === perm || permissionValues.includes(perm)
+    })
+    if (!hasPermission) {
+      el.parentNode && el.parentNode.removeChild(el)
+    }
+  } else {
+    throw new Error(`need permission! Like v-hasPerm="['home:btn:edit','home:btn:delete']"`)
+  }
+}
+
+const directive = {
+  mounted(el, binding) {
+    checkPermission(el, binding)
+  },
+  updated(el, binding) {
+    checkPermission(el, binding)
+  },
+}
+
+export default directive

+ 33 - 0
src/entrypoints/sidepanel/directives/permission/hasRole.js

@@ -0,0 +1,33 @@
+import { useUserStore } from '@/store/modules/user'
+
+/**
+ * @desc v-hasRole 角色权限处理
+ * @desc 使用 v-hasRole="['admin', 'user]"
+ */
+function checkRole(el, binding) {
+  const userStore = useUserStore()
+  const { value } = binding
+  const super_admin = 'role_admin'
+  if (value && Array.isArray(value) && value.length) {
+    const roleValues = value
+    const hasRole = userStore.roles.some((role) => {
+      return super_admin === role || roleValues.includes(role)
+    })
+    if (!hasRole) {
+      el.parentNode && el.parentNode.removeChild(el)
+    }
+  } else {
+    throw new Error(`need role! Like v-hasRole="['admin','user']"`)
+  }
+}
+
+const directive = {
+  mounted(el, binding) {
+    checkRole(el, binding)
+  },
+  updated(el, binding) {
+    checkRole(el, binding)
+  },
+}
+
+export default directive

+ 57 - 41
src/entrypoints/sidepanel/hook/useMsg.ts

@@ -2,7 +2,6 @@ import { ref, reactive, nextTick, inject } from 'vue'
 import { storeToRefs } from 'pinia'
 import avator from '@/public/icon/32.png'
 import moment from 'moment'
-import { cloneDeep } from 'lodash'
 import {
   getFormKey,
   hepl,
@@ -10,13 +9,16 @@ import {
   sendMessage
 } from '../utils/ai-service.js'
 import { ElMessage } from 'element-plus'
-import { getPageInfo } from '../utils/index.js'
+import { getPageInfo, getFileValue } from '../utils/index.js'
 import { mockData, mockData2 } from '../mock'
 import { useMsgStore } from '@/store/modules/msg'
 import { FunctionList } from '../mock'
+import { putChat } from '@/api/index.js'
+import { useUserStore } from '@/store/modules/user'
 
 export function useMsg(scrollbar?: any) {
-  const { messages, msgUuid, AIModel, cancelQueue } = storeToRefs(useMsgStore())
+  const { messages, msgUuid } = storeToRefs(useMsgStore())
+  const userStore = useUserStore()
   const indexTemp = ref(0)
   const taklToHtml = ref<any>(false)
   const sendLoading = ref(false)
@@ -24,8 +26,6 @@ export function useMsg(scrollbar?: any) {
   const type = ref(FunctionList.File_Operation)
   const formMap = ref([])
   // 获取父组件提供的 Hook 实例
-  const { useStore } = inject('indexedDBHook') as any
-
   const getFormKeyAndValue = async (file: any, form?: any) => {
     // const obj = reactive({
     //   id: moment(),
@@ -37,7 +37,6 @@ export function useMsg(scrollbar?: any) {
     //   avatar: avator,
     //   addToHistory: !taklToHtml.value
     // })
-
     try {
       sendLoading.value = true
       // messages.value.push(obj)
@@ -52,6 +51,7 @@ export function useMsg(scrollbar?: any) {
       sendLoading.value = false
     }
   }
+
   // 发送消息
   // const handleSend = async (msg: any) => {
   //   if ( msg?.startsWith('/')) {
@@ -105,18 +105,19 @@ export function useMsg(scrollbar?: any) {
       )
     })
   }
-
   const fetchRes = async (msg: any) => {
     indexTemp.value = 0
     sendLoading.value = true
     const obj: any = reactive({
-      id: messages.value.length + 1,
-      username: '用户1',
+      type: type || '',
+      rawContent: '',
+      senderId: -1,
+      receiverId: userStore.userInfo.id, //大模型
       content: '',
-      timestamp: moment().format('YYYY-MM-DD HH:mm:ss'),
-      isSelf: false,
-      avatar: avator,
-      addToHistory: !taklToHtml.value
+      sortKey: moment().valueOf(),
+      role: 'system',
+      conversationId: msgUuid.value,
+      addToHistory: `${!taklToHtml.value}`
     })
     messages.value.push(obj)
     msg = msg.split('/智能填表')[1]
@@ -137,7 +138,6 @@ export function useMsg(scrollbar?: any) {
         )
         const res = await awaitFindForm(obj)
         console.log(res, 34444)
-
         return res
       }
     } catch (error) {
@@ -145,6 +145,11 @@ export function useMsg(scrollbar?: any) {
       return { status: 'error' }
     } finally {
       sendLoading.value = false
+      console.log(messages.value);
+      putChat({
+        ...msg,
+        content: '',
+      })
     }
   }
   let str = ''
@@ -222,16 +227,18 @@ export function useMsg(scrollbar?: any) {
     pageInfo.value = await getPageInfo()
     sendLoading.value = true
     const obj = reactive<any>({
-      id: messages.value.length + 1,
-      username: '用户1',
+      id: +new Date(),
+      type: type || '',
+      rawContent: '',
+      senderId: -1,
+      receiverId: userStore.userInfo.id,
       content: '',
-      type: '', // form 用于展示抽取的内容
-      rawContent: '', // 存储原始内容
-      timestamp: moment().format('YYYY-MM-DD HH:mm:ss'),
-      isSelf: false,
-      avatar: avator,
-      addToHistory: !taklToHtml.value
+      sortKey: moment().valueOf(),
+      role: 'system',
+      conversationId: msgUuid.value,
+      addToHistory: `${!taklToHtml.value}`
     })
+
     let history = []
     if (taklToHtml.value) {
       if (addHtml) {
@@ -246,25 +253,23 @@ export function useMsg(scrollbar?: any) {
       })
     } else {
       history = messages.value
-        .filter((item: any) => item.addToHistory)
+        .filter((item: any) => item.addToHistory === 'true')
         .slice(-20)
         .map((item: any) => ({
-          role: item.isSelf ? 'user' : 'assistant',
+          role: item.role,
           content: item.rawContent
         }))
     }
-    if (AIModel.value.startSystemMsg === true) {
-      history.unshift({
-        role: 'system',
-        content: '你好'
-      })
-    }
     messages.value.push(obj)
     nextTick(() => {
       scrollbar.value?.setScrollTop(99999)
     })
     try {
+      console.log(history, 565666);
+
       const iterator = await sendMessage(history)
+      console.log(iterator);
+
       for await (const chunk of iterator) {
         if (chunk) {
           const decodedChunk = chunk.choices[0].delta.content
@@ -282,11 +287,16 @@ export function useMsg(scrollbar?: any) {
       }
       return { rawContent: obj.rawContent, status: 'ok' }
     } catch (error) {
+      console.log(error);
+
       obj.content = '网络出错'
       return { rawContent: obj.rawContent, status: 'error' }
     } finally {
-      //添加到存储历史
-      useStore(msgUuid.value).add(cloneDeep(obj))
+      console.log(messages.value);
+      putChat({
+        ...obj,
+        content: '',
+      })
       // 处理最终内容
       sendLoading.value = false
       nextTick(() => {
@@ -304,14 +314,16 @@ export function useMsg(scrollbar?: any) {
   async function requestFlowFn(data: any[]) {
     sendLoading.value = true
     const obj = reactive<any>({
-      id: messages.value.length + 1,
-      username: '用户1',
+      id: +new Date(),
+      type: type || '',
+      rawContent: '',
+      senderId: -1,
+      receiverId: userStore.userInfo.id, //大模型
       content: '',
-      rawContent: '', // 存储原始内容
-      timestamp: moment().format('YYYY-MM-DD HH:mm:ss'),
-      isSelf: false,
-      avatar: avator,
-      addToHistory: !taklToHtml.value
+      sortKey: moment().valueOf(),
+      role: 'system',
+      conversationId: msgUuid.value,
+      addToHistory: `${!taklToHtml.value}`
     })
     messages.value.push(obj)
     scrollbar.value?.setScrollTop(99999)
@@ -335,8 +347,12 @@ export function useMsg(scrollbar?: any) {
         scrollbar.value?.setScrollTop(99999)
       }
     }
-    //添加到存储历史
-    useStore(msgUuid.value).add(cloneDeep(obj))
+
+    console.log(messages.value);
+    putChat({
+      ...obj,
+      content: '',
+    })
     // 处理最终内容
     sendLoading.value = false
     await nextTick(() => {

+ 2 - 1
src/entrypoints/sidepanel/main.ts

@@ -11,10 +11,11 @@ import '@/assets/icon/iconfont'
 import '@/assets/icon/iconfont.css'
 import 'virtual:svg-icons-register'
 import SvgIcon from '@/entrypoints/sidepanel/components/SvgIcon/index.vue'
-
+import directives from './directives'
 const app = createApp(App)
 app.use(ElementPlus, { locale: locale, size: 'small' })
 app.use(store)
+app.use(directives)
 for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
   app.component(key, component)
 }

+ 3 - 1
src/entrypoints/sidepanel/utils/ai-service.js

@@ -15,7 +15,9 @@ export async function sendMessage(message) {
     throw new Error('No active Pinia instance found')
   }
   const store = useMsgStore(pinia)
-  const { openai, AIModel, msgUuid } = store
+
+  const { openai, AIModel } = store
+
   try {
     const controller = new AbortController()
     controller.id = msgUuid // 为每个控制器添加唯一 ID

+ 39 - 0
src/entrypoints/sidepanel/utils/encrypt.js

@@ -0,0 +1,39 @@
+import Base64 from 'crypto-js/enc-base64'
+import UTF8 from 'crypto-js/enc-utf8'
+import { JSEncrypt } from 'jsencrypt'
+import md5 from 'crypto-js/md5'
+import CryptoJS from 'crypto-js'
+
+export function encodeByBase64(txt) {
+  return UTF8.parse(txt).toString(Base64)
+}
+
+export function decodeByBase64(txt) {
+  return Base64.parse(txt).toString(UTF8)
+}
+
+export function encryptByMd5(txt) {
+  return md5(txt).toString()
+}
+
+const publicKey
+  = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAM51dgYtMyF+tTQt80sfFOpSV27a7t9u'
+  + 'aUVeFrdGiVxscuizE7H8SMntYqfn9lp8a5GH5P1/GGehVjUD2gF/4kcCAwEAAQ=='
+
+export function encryptByRsa(txt) {
+  const encryptor = new JSEncrypt()
+  encryptor.setPublicKey(publicKey) // 设置公钥
+  return encryptor.encrypt(txt) // 对数据进行加密
+}
+
+const defaultKeyWork = 'XwKsGlMcdPMEhR1B'
+
+export function encryptByAes(word, keyWord = defaultKeyWork) {
+  const key = CryptoJS.enc.Utf8.parse(keyWord)
+  const arcs = CryptoJS.enc.Utf8.parse(word)
+  const encrypted = CryptoJS.AES.encrypt(arcs, key, {
+    mode: CryptoJS.mode.ECB,
+    padding: CryptoJS.pad.Pkcs7,
+  })
+  return encrypted.toString()
+}

+ 2 - 2
src/entrypoints/sidepanel/utils/index.js

@@ -8,9 +8,9 @@ export function getPageInfo() {
       {
         type: 'FROM_SIDE_PANEL_TO_GET_PAGE_INFO'
       },
-      async (response) => {
+      (response) => {
         if (chrome.runtime.lastError) {
-          console.error('消息发送错误:', chrome.runtime.lastError)
+          console.error('消息发送错误:', chrome.runtime.lastError, 555)
           rej(chrome.runtime.lastError)
         } else {
           res(response.data)

+ 39 - 0
src/entrypoints/sidepanel/utils/verify.js

@@ -0,0 +1,39 @@
+export function resetSize(vm) {
+  let img_width
+  let img_height
+  let bar_width
+  let bar_height // 图片的宽度、高度,移动条的宽度、高度
+
+  const parentWidth = vm.$el.parentNode.offsetWidth || window.innerWidth
+  const parentHeight = vm.$el.parentNode.offsetHeight || window.innerHeight
+  if (vm.imgSize.width.includes('%')) {
+    img_width = `${(Number.parseInt(vm.imgSize.width, 10) / 100) * parentWidth}px`
+  } else {
+    img_width = vm.imgSize.width
+  }
+
+  if (vm.imgSize.height.includes('%')) {
+    img_height = `${(Number.parseInt(vm.imgSize.height, 10) / 100) * parentHeight}px`
+  } else {
+    img_height = vm.imgSize.height
+  }
+
+  if (vm.barSize.width.includes('%')) {
+    bar_width = `${(Number.parseInt(vm.barSize.width, 10) / 100) * parentWidth}px`
+  } else {
+    bar_width = vm.barSize.width
+  }
+
+  if (vm.barSize.height.includes('%')) {
+    bar_height = `${(Number.parseInt(vm.barSize.height, 10) / 100) * parentHeight}px`
+  } else {
+    bar_height = vm.barSize.height
+  }
+
+  return {
+    imgWidth: img_width,
+    imgHeight: img_height,
+    barWidth: bar_width,
+    barHeight: bar_height,
+  }
+}

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

@@ -1,22 +1,68 @@
 import { defineStore } from 'pinia'
 import OpenAI from 'openai'
-
+import { getChatDetail } from '@/api/index'
+import {
+  formatMessage,
+} from '@/entrypoints/sidepanel/utils/ai-service.js'
 export const useMsgStore = defineStore('msg', {
   state: () => ({
     msgUuid: <string>'',
     messages: <any[]>[],
+    hasNext: true,
+    page: 1,
     pageInfoList: [],
     AIModel: <any>{},
     openai: <any>null,
-    cancelQueue:<any>{}
+    cancelQueue: <any>{}
   }),
   actions: {
+    async initMsg() {
+      console.log(this.page, this.hasNext, this.messages);
+      const { msgUuid } = await new Promise<any>((resolve) => {
+        chrome.storage.local.get(['msgUuid'], (result) => {
+          resolve(result)
+        })
+      })
+      if (msgUuid) {
+        console.log(85556);
+        this.msgUuid = msgUuid
+        const res = await getChatDetail({
+          page: this.page,
+          size: 10,
+          sort: 'sortKey,desc',
+          conversationId: this.msgUuid
+        })
+        this.messages = res.list.reverse().map(_ => ({
+          id: _.id,
+          type: _.type,
+          rawContent: _.rawContent,
+          senderId: _.senderId,
+          receiverId: _.receiverId,
+          sortKey: _.sortKey,
+          role: _.role,
+          conversationId: _.conversationId,
+          addToHistory: _.addToHistory,
+          content: _.role === 'system' ? formatMessage(_.rawContent) : _.content
+        })).concat(this.messages)
+        if (this.messages.length < res.total) {
+          this.hasNext = true
+          this.page++
+        }
+        else this.hasNext = false
+      }
+    },
+
+    async changePage() {
+      if (this.hasNext) {
+        await this.initMsg()
+      }
+    },
     updateAIModel(model: any) {
       this.AIModel = model
       this.openai = new OpenAI({
         // apiKey: import.meta.env.VITE_OPENAI_API_KEY_TONG,
-        apiKey: this.AIModel.api_sk,
-        baseURL: this.AIModel.api_url,
+        apiKey: import.meta.env.VITE_OPENAI_API_KEY_TONG,
+        baseURL: this.AIModel.url,
         // timeout: 5000,
         dangerouslyAllowBrowser: true
       })

+ 219 - 0
src/store/modules/user.ts

@@ -0,0 +1,219 @@
+import { defineStore, storeToRefs } from 'pinia'
+import { ElMessage } from 'element-plus'
+import { useMsgStore } from './msg'
+import { login, getUserInfo as getUserInfoApi, loginOut } from '@/api'
+// 定义用户信息接口
+interface UserInfo {
+  id: string
+  username: string
+  avatar: string
+  phone: string
+  email: string
+  token: string
+  roles: string[]
+  permissions: string[]
+}
+
+// 定义登录参数接口
+interface LoginParams {
+  username?: string
+  password?: string
+  phone?: string
+  code?: string
+  type: 'account' | 'phone' | 'wechat'
+}
+
+// 从 chrome.storage.local 获取 token
+const getStoredToken = async (): Promise<string> => {
+  if (typeof chrome !== 'undefined' && chrome.storage) {
+    return new Promise((resolve) => {
+      chrome.storage.local.get('token', (result) => {
+        resolve(result.token || '')
+      })
+    })
+  }
+  return localStorage.getItem('token') || '' // 降级方案
+}
+
+// 初始化 token 为空字符串,后续异步获取
+export const useUserStore = defineStore('user', {
+  state: () => ({
+    token: '',
+    userInfo: <UserInfo | null>null,
+    roles: <string[]>[],
+    permissions: <string[]>[],
+    loginLoading: false
+  }),
+  
+  getters: {
+    isLogin: (state) => !!state.token,
+    hasRole: (state) => (role: string) => state.roles.includes(role),
+    hasPermission: (state) => (permission: string) => state.permissions.includes(permission)
+  },
+  
+  actions: {
+    // 初始化 store,从 chrome.storage 加载数据
+    async initStore() {
+      if (typeof chrome !== 'undefined' && chrome.storage) {
+        const data = await new Promise<any>((resolve) => {
+          chrome.storage.local.get(['token', 'userInfo', 'roles', 'permissions'], (result) => {
+            resolve(result)
+          })
+        })
+        
+        if (data.token) {
+          this.token = data.token
+          console.log(6565656);
+          
+          await this.getUserInfo()
+        }
+        // if (data.userInfo) this.userInfo = JSON.parse(data.userInfo)
+        // if (data.roles) this.roles = JSON.parse(data.roles)
+        // if (data.permissions) this.permissions = JSON.parse(data.permissions)
+      } else {
+        // 降级方案:使用 localStorage
+        this.token = localStorage.getItem('token') || ''
+        const userInfo = localStorage.getItem('userInfo')
+        if (userInfo) this.userInfo = JSON.parse(userInfo)
+        const roles = localStorage.getItem('roles')
+        if (roles) this.roles = JSON.parse(roles)
+        const permissions = localStorage.getItem('permissions')
+        if (permissions) this.permissions = JSON.parse(permissions)
+      }
+    },
+    
+    // 设置Token
+    async setToken(token: string) {
+      this.token = token
+      if (typeof chrome !== 'undefined' && chrome.storage) {
+        await chrome.storage.local.set({ token })
+        chrome.runtime.sendMessage({
+          type: 'SET_TOKEN',
+          data: {
+            token
+          }
+        }, function (response) {
+        });
+      } else {
+        localStorage.setItem('token', token)
+      }
+    },
+    
+    // 清除Token
+    async clearToken() {
+      this.token = ''
+      if (typeof chrome !== 'undefined' && chrome.storage) {
+        await chrome.storage.local.remove('token')
+      } else {
+        localStorage.removeItem('token')
+      }
+    },
+    
+    // 登录
+    async login(params: LoginParams) {
+      try {
+        this.loginLoading = true
+        
+        // 这里替换为实际的API调用
+        const res = await login({
+          ...params,
+          clientId: import.meta.env.VITE_CLIENT_ID
+        })
+        if (res.code === '0') {
+          await this.setToken(res.data.token)
+          await this.getUserInfo()
+          ElMessage.success('登陆成功')
+          return true
+        } else {
+          ElMessage.error(res.message || '登录失败')
+          return false
+        }
+      } catch (error: any) {
+        // ElMessage.error(error.message || '登录出错')
+        return false
+      } finally {
+        this.loginLoading = false
+      }
+    },
+    
+    // 退出登录
+    async logout(flag = true) {
+      try {
+        // 这里替换为实际的API调用
+        flag && await loginOut()
+        // 清除用户数据
+        await this.clearUserData()
+        return true
+      } catch (error: any) {
+        ElMessage.error(error.message || '退出登录出错')
+        return false
+      }
+    },
+    
+    // 获取用户信息
+    async getUserInfo() {
+      try {
+        if (!this.token) return false
+        
+        // 这里替换为实际的API调用
+        const res = await getUserInfoApi()
+        console.log(res,8888);
+
+        // 模拟获取用户信息
+        if (res.code === '0') {
+          this.userInfo = res.data
+          this.roles = res.data.roles
+          this.permissions = res.data.permissions
+          // 存储用户信息到 chrome.storage
+          if (typeof chrome !== 'undefined' && chrome.storage) {
+            await chrome.storage.local.set({
+              userInfo: JSON.stringify(this.userInfo),
+              roles: JSON.stringify(this.roles),
+              permissions: JSON.stringify(this.permissions)
+            })
+          } else {
+            // 降级方案:使用 localStorage
+            localStorage.setItem('userInfo', JSON.stringify(this.userInfo))
+            localStorage.setItem('roles', JSON.stringify(this.roles))
+            localStorage.setItem('permissions', JSON.stringify(this.permissions))
+          }
+          
+          return true
+        } else {
+          await this.clearUserData()
+          ElMessage.error(res.message || '获取用户信息失败')
+          return false
+        }
+      } catch (error: any) {
+        await this.clearUserData()
+        ElMessage.error(error.message || '获取用户信息出错')
+        return false
+      }
+    },
+    
+    // 清除用户数据
+    async clearUserData() {
+      await this.clearToken()
+      const { messages, page, msgUuid } = storeToRefs(useMsgStore())
+      console.log(messages, page, msgUuid);
+      
+      page.value = 1
+      msgUuid.value = ''
+      this.userInfo = null
+      this.roles = []
+      this.permissions = []
+      messages.value = []
+      console.log(6565);
+      
+      if (typeof chrome !== 'undefined' && chrome.storage) {
+        await chrome.storage.local.remove(['userInfo', 'roles', 'permissions','msgUuid'])
+      } else {
+        localStorage.removeItem('userInfo')
+        localStorage.removeItem('roles')
+        localStorage.removeItem('permissions')
+      }
+    }
+    
+    // 其他方法保持不变...
+  }
+})

+ 108 - 0
src/utils/contentUtils.js

@@ -372,3 +372,111 @@ export function getFavicon() {
   const url = new URL(window.location.href)
   return `${url.protocol}//${url.hostname}/favicon.ico`
 }
+export function cleanPage2(body) {
+  const cloneBody = body.cloneNode(true)
+  // 移除所有行内样式
+  const elementsWithInlineStyle = cloneBody.querySelectorAll('[style]')
+  elementsWithInlineStyle.forEach((element) => {
+    element.removeAttribute('style')
+  })
+
+  // 移除所有注释节点
+  const comments = []
+  const walk = document.createTreeWalker(
+    cloneBody,
+    NodeFilter.SHOW_COMMENT,
+    null,
+    false
+  )
+  while (walk.nextNode()) {
+    comments.push(walk.currentNode)
+  }
+  comments.forEach((comment) => comment.remove())
+
+  // 移除所有SVG图标
+  cloneBody
+    .querySelectorAll('script, style,  svg')
+    .forEach((svg) => svg.remove())
+
+  // 移除所有图片的src属性
+  const images = cloneBody.querySelectorAll('img')
+  images.forEach((img) => {
+    img.removeAttribute('src')
+  })
+
+  // 可选择移除其他无关内容,比如脚本和广告等
+  // const scripts = body.querySelectorAll('script');
+  // scripts.forEach(script => script.remove());
+
+  // const iframes = body.querySelectorAll('iframe');
+  // iframes.forEach(iframe => iframe.remove());
+
+  const ads = cloneBody.querySelectorAll('.ad, .advertisement, .ads')
+  ads.forEach((ad) => ad.remove())
+
+  const regex = /\s+/g
+
+  // 定义标准 HTML 属性集合 排除id class style href src target
+  const standardAttributes = new Set([
+    'id',
+    'class',
+    'alt',
+    'title',
+    'type',
+    'value',
+    'name',
+    'placeholder',
+    'disabled',
+    'checked',
+    'selected',
+    'readonly',
+    'required',
+    'maxlength',
+    'min',
+    'max',
+    'step',
+    'pattern',
+    'autocomplete',
+    'autofocus',
+    'multiple',
+    'rows',
+    'cols',
+    'rel',
+    'aria-*'
+  ])
+
+  // 创建一个临时容器
+  const temp = document.createElement('div')
+  temp.innerHTML = cloneBody.outerHTML
+
+
+  // 删除 <link> 标签
+  const linkTags = temp.querySelectorAll('link')
+  linkTags.forEach((link) => {
+    link.remove()
+  })
+
+  // 遍历所有元素
+  const elements = temp.querySelectorAll('*')
+  elements.forEach((element) => {
+    // 获取所有属性
+    const attributes = Array.from(element.attributes)
+    attributes.forEach((attr) => {
+      // 如果属性不是标准属性,则移除
+      if (
+        !standardAttributes.has(attr.name) &&
+        !attr.name.startsWith('aria-')
+      ) {
+        element.removeAttribute(attr.name)
+      }
+    })
+  })
+
+  // 获取处理后的 HTML 字符串
+  const cleanedHtml = temp.innerHTML.trim().replace(regex, ' ')
+
+  // 销毁临时容器
+  temp.remove()
+  // content.outerHTML.trim().replace(/\s+/g, " ");
+  return cleanedHtml
+}

+ 6 - 0
src/utils/errorCode copy.js

@@ -0,0 +1,6 @@
+export default {
+  '401': '认证失败,无法访问系统资源',
+  '403': '当前操作没有权限',
+  '404': '访问资源不存在',
+  'default': '系统未知错误,请反馈给管理员'
+}

+ 110 - 1
src/utils/page-analyzer.js

@@ -163,7 +163,7 @@ export function cleanPage(body) {
   }
 
   // 从临时容器的子元素开始处理
-  removeEmpty(temp)
+  // removeEmpty(temp) //会导致标签元素被移除
 
   // 删除 <link> 标签
   const linkTags = temp.querySelectorAll('link')
@@ -195,3 +195,112 @@ export function cleanPage(body) {
   // content.outerHTML.trim().replace(/\s+/g, " ");
   return cleanedHtml
 }
+function cleanPage2(body) {
+  const cloneBody = body.cloneNode(true)
+  // 移除所有行内样式
+  const elementsWithInlineStyle = cloneBody.querySelectorAll('[style]')
+  elementsWithInlineStyle.forEach((element) => {
+    element.removeAttribute('style')
+  })
+
+  // 移除所有注释节点
+  const comments = []
+  const walk = document.createTreeWalker(
+    cloneBody,
+    NodeFilter.SHOW_COMMENT,
+    null,
+    false
+  )
+  while (walk.nextNode()) {
+    comments.push(walk.currentNode)
+  }
+  comments.forEach((comment) => comment.remove())
+
+  // 移除所有SVG图标
+  cloneBody
+    .querySelectorAll('script, style,  svg')
+    .forEach((svg) => svg.remove())
+
+  // 移除所有图片的src属性
+  const images = cloneBody.querySelectorAll('img')
+  images.forEach((img) => {
+    img.removeAttribute('src')
+  })
+
+  // 可选择移除其他无关内容,比如脚本和广告等
+  // const scripts = body.querySelectorAll('script');
+  // scripts.forEach(script => script.remove());
+
+  // const iframes = body.querySelectorAll('iframe');
+  // iframes.forEach(iframe => iframe.remove());
+
+  const ads = cloneBody.querySelectorAll('.ad, .advertisement, .ads')
+  ads.forEach((ad) => ad.remove())
+
+  const regex = /\s+/g
+
+  // 定义标准 HTML 属性集合 排除id class style href src target
+  const standardAttributes = new Set([
+    'id',
+    'class',
+    'alt',
+    'title',
+    'type',
+    'value',
+    'name',
+    'placeholder',
+    'disabled',
+    'checked',
+    'selected',
+    'readonly',
+    'required',
+    'maxlength',
+    'min',
+    'max',
+    'step',
+    'pattern',
+    'autocomplete',
+    'autofocus',
+    'multiple',
+    'rows',
+    'cols',
+    'rel',
+    'aria-*'
+  ])
+
+  // 创建一个临时容器
+  const temp = document.createElement('div')
+  temp.innerHTML = cloneBody.outerHTML
+
+
+  // 删除 <link> 标签
+  const linkTags = temp.querySelectorAll('link')
+  linkTags.forEach((link) => {
+    link.remove()
+  })
+
+  // 遍历所有元素
+  const elements = temp.querySelectorAll('*')
+  elements.forEach((element) => {
+    // 获取所有属性
+    const attributes = Array.from(element.attributes)
+    attributes.forEach((attr) => {
+      // 如果属性不是标准属性,则移除
+      if (
+        !standardAttributes.has(attr.name) &&
+        !attr.name.startsWith('aria-')
+      ) {
+        element.removeAttribute(attr.name)
+      }
+    })
+  })
+
+  // 获取处理后的 HTML 字符串
+  const cleanedHtml = temp.innerHTML.trim().replace(regex, ' ')
+
+  // 销毁临时容器
+  temp.remove()
+  // content.outerHTML.trim().replace(/\s+/g, " ");
+  return cleanedHtml
+}
+

+ 45 - 46
src/utils/request.js

@@ -1,65 +1,58 @@
 import axios, { CancelToken, isCancel } from 'axios'
 import { ElMessage, ElNotification } from 'element-plus'
 import errorCode from './errorCode.js'
+import { useUserStore } from '@/store/modules/user'
 
 axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
+
 // 创建axios实例
 const service = axios.create({
-  // axios中请求配置有baseURL选项,表示请求URL公共部分
   baseURL: import.meta.env.VITE_APP_BASE_API,
-  // 超时
   timeout: 60000
 })
+console.log(import.meta.env.VITE_APP_BASE_API);
+
 let cancel
 // request拦截器
 service.interceptors.request.use(
-  (config) => {
-    // console.log(config)
+ async (config) => {
     if (cancel) cancel('取消了')
     if (config.cancel) {
       config.cancelToken = new CancelToken((c) => {
-        //c是一个函数,调用c就可以关闭本次请求
         cancel = c
       })
     }
-    // // 是否需要设置 token
-    // const isToken = (config.headers || {}).isToken === false
-    // // 是否需要防止数据重复提交
-    // const isRepeatSubmit = (config.headers || {}).repeatSubmit === false
-    let token = sessionStorage.getItem('token')
-    if (token) {
-      config.headers['Authorization'] = 'Bearer ' + token // 让每个请求携带自定义token 请根据实际情况自行修改
+
+    // 处理GET请求,将data参数拼接到URL
+    if (config.method === 'get' && config.data) {
+      // 将data对象转换为URL参数
+      let params = Object.keys(config.data)
+        .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(config.data[key])}`)
+        .join('&');
+      
+      // 拼接到URL
+      if (params) {
+        config.url += (config.url.includes('?') ? '&' : '?') + params;
+      }
+      // 清空data,避免重复发送
+      config.data = undefined;
     }
-    // get请求映射params参数
-    // if (config.method === 'get' && config.params) {
-    //   let url = config.url + '?' + tansParams(config.params);
-    //   url = url.slice(0, -1);
-    //   config.params = {};
-    //   config.url = url;
-    // }
-    // if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
-    //   const requestObj = {
-    //     url: config.url,
-    //     data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
-    //     time: new Date().getTime()
-    //   }
-    //   const sessionObj = cache.session.getJSON('sessionObj')
-    //   if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
-    //     cache.session.setJSON('sessionObj', requestObj)
-    //   } else {
-    //     const s_url = sessionObj.url;                // 请求地址
-    //     const s_data = sessionObj.data;              // 请求数据
-    //     const s_time = sessionObj.time;              // 请求时间
-    //     const interval = 1000;                       // 间隔时间(ms),小于此时间视为重复提交
-    //     if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
-    //       const message = '数据正在处理,请勿重复提交';
-    //       console.warn(`[${s_url}]: ` + message)
-    //       return Promise.reject(new Error(message))
-    //     } else {
-    //       cache.session.setJSON('sessionObj', requestObj)
-    //     }
-    //   }
+    console.log(config.url);
+
+    const {token} = await new Promise((resolve) => {
+      chrome.storage.local.get(['token'], (result) => {
+        resolve(result)
+      })
+    })
+    
+    // 当token不存在时,执行退出登录
+    // if (!token && !config.url.includes('/login')) {
+    //   return Promise.reject('请先登录')
     // }
+
+    if (token) {
+      config.headers['Authorization'] = 'Bearer ' + token
+    }
     return config
   },
   (error) => {
@@ -70,6 +63,8 @@ service.interceptors.request.use(
 // 响应拦截器
 service.interceptors.response.use(
   (res) => {
+    console.log(res,85888);
+
     // 未设置状态码则默认成功状态
     const code = res.data.code || 200
     // 获取错误信息
@@ -82,7 +77,8 @@ service.interceptors.response.use(
       let filename = res.headers['content-disposition'].split('filename=')[1]
       return { fileName: filename, data: res.data }
     }
-    if (code === 401) {
+    
+    if (code === '401') {
       // if (!isRelogin.show) {
       //   isRelogin.show = true;
       //   ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => {
@@ -94,17 +90,20 @@ service.interceptors.response.use(
       //     isRelogin.show = false;
       //   });
       // }
+      ElMessage.error('您的登录状态已过期,请重新登录。')
+      const userStore = useUserStore()
+      userStore.logout()
       return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
     } else if (code === 500) {
       ElMessage({ message: msg, type: 'error', grouping: true })
       return Promise.reject(msg)
+    } if (code === '1') {
+      ElMessage({ message: '接口异常', type: 'error', grouping: true })
+      return Promise.reject(msg)
     } else if (code === 601) {
       ElMessage({ message: msg, type: 'warning', grouping: true })
       return Promise.reject(new Error(msg))
-    } else if (code !== 200) {
-      ElNotification.error({ title: msg })
-      return Promise.reject('error')
-    } else {
+    }  else {
       return Promise.resolve(res.data)
     }
   },

+ 1 - 1
wxt.config.ts

@@ -19,7 +19,7 @@ export default defineConfig({
   srcDir: 'src',
   manifest: {
     name: '派维斯智能体助手',
-    version: '0.1.7',
+    version: '0.1.9',
     permissions: [
       'storage',
       'history',

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů