浏览代码

feat(user): 实现手机号登录功能并优化用户相关逻辑

- 新增手机号登录相关API和功能组件
- 优化用户信息获取和存储逻辑
- 添加验证码获取和验证功能
- 修复部分用户相关bug
chd 4 月之前
父节点
当前提交
ce92cb4a2f

+ 3 - 1
.env

@@ -1,3 +1,5 @@
 VITE_OPENAI_API_KEY_TONG=sk-e9855234f47346049809ce23ed3ebe3f
 VITE_MAX_FILE_NUMBER=10
-VITE_APP_BASE_API='http://192.168.1.166:7777'
+VITE_APP_BASE_API='http://192.168.1.118:7777'
+# 终端ID
+VITE_CLIENT_ID = '153062e567553ec0bf1a02b8f1f563b1'

+ 2 - 0
package.json

@@ -18,8 +18,10 @@
   "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",
+    "jsencrypt": "^3.3.2",
     "lodash": "^4.17.21",
     "moment": "^2.30.1",
     "openai": "^4.85.4",

+ 50 - 0
src/api/index.js

@@ -1,4 +1,25 @@
 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',
@@ -52,3 +73,32 @@ export function getCode(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
+    })
+}

+ 15 - 1
src/entrypoints/background.js

@@ -2,6 +2,7 @@ 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
@@ -21,7 +22,7 @@ export default defineBackground(() => {
               'Content-Type': 'application/json',
               'Authorization': 'Bearer ' + token
             },
-            body: JSON.stringify(arr.map(_ => ({ ..._, userId: token })))
+            body: JSON.stringify(arr.map(_ => ({ ..._, userId })))
           }).then(res => 1)
           arr = []
         }
@@ -142,6 +143,8 @@ export default defineBackground(() => {
     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) {
@@ -149,8 +152,19 @@ export default defineBackground(() => {
             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) => {

+ 7 - 4
src/entrypoints/sidepanel/App.vue

@@ -1,9 +1,9 @@
 <template>
-  <div class="app-container"   v-if="userStore.isLogin">
+  <div class="app-container" v-if="userStore.userInfo">
     <UserDropdown />
     <Chat />
   </div>
-    <Login v-else/> 
+  <Login v-else />
 </template>
 
 <script lang="ts" setup>
@@ -12,7 +12,7 @@ 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'
 
@@ -21,9 +21,12 @@ const userStore = useUserStore()
 
 onMounted(async () => {
   await userStore.initStore()
+  console.log(userStore.userInfo,884562)
   chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
     if (message.type === 'USER_LOGOUT') {
-      userStore.isLogin &&  userStore.logout()
+      console.log(7777);
+      
+      userStore.isLogin &&  userStore.clearUserData()
     }
   })
   // 初始化数据库连接(不包含任何Store)

+ 1 - 1
src/entrypoints/sidepanel/Chat.vue

@@ -433,9 +433,9 @@ const handleUpload = async (file) => {
         handleInput(xlsxData.value, form)
       }
     } else {
+      const msg = await addMessage(`文件上传中`)
       try {
         sendLoading.value = true
-        const msg = await addMessage(`文件上传中`)
         putChat(msg)
         const data = await getFileValue(file)
         msg.content = `已上传文件:${file.name}`

+ 64 - 42
src/entrypoints/sidepanel/component/Login.vue

@@ -6,7 +6,7 @@
         </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 class="tab" :class="{ active: activeTab === 'wechat' }" @click="activeTab = 'wechat'">微信登录</div> -->
         </div>
 
         <div class="login-content">
@@ -24,9 +24,10 @@
                         <el-icon>
                             <Lock />
                         </el-icon>
-                        <el-input v-model="code" placeholder="请输入验证码" maxlength="6" />
-                        <el-button class="code-btn" :disabled="codeBtnDisabled" @click="getVerificationCode">
-                            {{ codeBtnText }}
+                        <el-input v-model="captcha" placeholder="请输入验证码" maxlength="6" />
+                        <el-button class="code-btn" :loading="captchaLoading" :disabled="captchaDisable"
+                            @click="getVerificationCode">
+                            {{ captchaBtnName }}
                         </el-button>
                     </div>
                 </div>
@@ -41,7 +42,7 @@
                     <div class="qrcode" :class="{ expired: isQrcodeExpired }"
                         @click="isQrcodeExpired && refreshQrcode()">
                         <!-- 使用vue-qrcode组件 -->
-                        <qrcode-vue :value="qrcodeValue" :width="200"/>
+                        <qrcode-vue :value="qrcodeValue" :width="200" />
                         <!-- 过期遮罩层 -->
                         <div v-if="isQrcodeExpired" class="expired-mask">
                             <el-icon :size="40">
@@ -75,6 +76,8 @@
                 </div>
             </div> -->
         </div>
+        <Verify ref="VerifyRef" :captcha-type="captchaType" :mode="captchaMode"
+            :img-size="{ width: '330px', height: '155px' }" @success="getCaptcha" />
     </div>
 </template>
 
@@ -84,20 +87,59 @@ 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 } from '@/api/index.js'
+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 code = ref('')
+const captcha = ref('')
 const agreement = ref(false)
 const loading = ref(false)
-const codeBtnText = ref('获取验证码')
-const codeBtnDisabled = ref(false)
+const captchaLoading = ref(false)
+
 // 二维码值
 const qrcodeValue = ref('https://example.com/login?token=' + Date.now())
 let countdown = 60
@@ -111,37 +153,14 @@ const refreshQrcode = async () => {
 
 // 获取验证码
 const getVerificationCode = async () => {
-    console.log(121);
-    
     if (!validatePhone()) return
-
-    // 模拟发送验证码请求
-    try {
-    //     const res = await getCode({
-    //         phone: phone.value
-    //  })
-     ElMessage.success(`验证码已发送至 ${phone.value}`)
-     codeBtnDisabled.value = true
-     let timer = setInterval(() => {
-         countdown--
-         codeBtnText.value = `${countdown}秒后重新获取`
-         if (countdown <= 0) {
-             clearInterval(timer)
-             codeBtnDisabled.value = false
-             codeBtnText.value = '获取验证码'
-             countdown = 60
-         }
-     }, 1000)
- } catch (error) {
-    console.log(error);
-    
- }
+    onCaptcha()
 }
 
 // 手机号登录
 const handlePhoneLogin = async () => {
     if (!validatePhone()) return
-    if (!code.value) {
+    if (!captcha.value) {
         ElMessage.warning('请输入验证码')
         return
     }
@@ -154,8 +173,9 @@ const handlePhoneLogin = async () => {
         // 调用user仓库的登录方法
         const loginResult = await userStore.login({
             phone: phone.value,
-            code: code.value,
-            type: 'phone'
+            captcha: captcha.value,
+            authType: "PHONE",
+            clientId: "ef51c9a3e9046c4f2ea45142c8a8344a"
         })
      
     } catch (error) {
@@ -212,10 +232,13 @@ const validatePhone = () => {
 }
 watch(activeTab, (newTab) => {
   if (newTab === 'wechat') {
-      qrcodeValue.value = 'https://example.com/login?token=' + Date.now()
-        setTimeout(() => {
-            checkQRcodeResult()
-        }, 1000);
+    getWeChatLoginCode({source:'wechat_mp'}).then(res => {
+       qrcodeValue.value = res.authorizeUrl
+    })
+    //   qrcodeValue.value = 'https://example.com/login?token=' + Date.now()
+    //     setTimeout(() => {
+    //         checkQRcodeResult()
+    //     }, 1000);
   }
 })
 
@@ -261,7 +284,6 @@ onMounted(() => {
 .login-tabs {
     display: flex;
     border-bottom: 1px solid #eaedf2;
-    margin-bottom: 32px;
     background-color: #fff;
     border-radius: 8px 8px 0 0;
     box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);

+ 56 - 15
src/entrypoints/sidepanel/component/UserDropdown.vue

@@ -2,25 +2,36 @@
   <div class="user-dropdown">
     <el-dropdown trigger="hover" @command="handleCommand">
       <div class="avatar-container">
-        <el-icon :size="20"><Menu /></el-icon>
+        <el-icon :size="18"><Menu /></el-icon>
+        <!-- <span class="menu-text">菜单</span> -->
       </div>
       <template #dropdown>
         <el-dropdown-menu>
-          <!-- <el-dropdown-item command="profile">个人信息</el-dropdown-item>
-          <el-dropdown-item command="settings">设置</el-dropdown-item> -->
-          <el-dropdown-item divided command="logout">退出登录</el-dropdown-item>
+          <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 } from '@element-plus/icons-vue'
+import { Menu, User, SwitchButton } from '@element-plus/icons-vue'
+import UserProfile from './UserProfile.vue'
 
 const userStore = useUserStore()
+const userProfileRef = ref()
 
 // 处理下拉菜单命令
 const handleCommand = (command: string) => {
@@ -39,11 +50,7 @@ const handleCommand = (command: string) => {
       // 取消退出
     })
   } else if (command === 'profile') {
-    // 处理个人信息
-    console.log('查看个人信息')
-  } else if (command === 'settings') {
-    // 处理设置
-    console.log('打开设置')
+    userProfileRef.value?.open()
   }
 }
 </script>
@@ -59,15 +66,49 @@ const handleCommand = (command: string) => {
 .avatar-container {
   display: flex;
   align-items: center;
-  justify-content: center;
+  padding: 6px 12px;
   cursor: pointer;
-  border-radius: 4px;
-  transition: background-color 0.3s;
-  color: #606266;
+  border-radius: 6px;
+  transition: all 0.3s;
+  background-color: #f5f7fa;
+  border: 1px solid transparent;
 }
 
 .avatar-container:hover {
-  background-color: rgba(0, 0, 0, 0.05);
+  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>

文件差异内容过多而无法显示
+ 377 - 0
src/entrypoints/sidepanel/component/Verify/index.vue


+ 4 - 5
src/entrypoints/sidepanel/component/tools.vue

@@ -19,19 +19,18 @@ watchEffect(() => {
   <div class="px-3 py-2 flex justify-between items-center">
     <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">

+ 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

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

@@ -9,10 +9,11 @@ import * as ElementPlusIconsVue from '@element-plus/icons-vue'
 import store from '@/store/index'
 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)
 }

+ 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()
+}

+ 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,
+  }
+}

+ 31 - 73
src/store/modules/user.ts

@@ -1,6 +1,7 @@
 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
@@ -60,10 +61,15 @@ export const useUserStore = defineStore('user', {
           })
         })
         
-        if (data.token) this.token = data.token
-        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)
+        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') || ''
@@ -79,9 +85,15 @@ export const useUserStore = defineStore('user', {
     // 设置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)
       }
@@ -90,7 +102,6 @@ export const useUserStore = defineStore('user', {
     // 清除Token
     async clearToken() {
       this.token = ''
-      
       if (typeof chrome !== 'undefined' && chrome.storage) {
         await chrome.storage.local.remove('token')
       } else {
@@ -104,60 +115,21 @@ export const useUserStore = defineStore('user', {
         this.loginLoading = true
         
         // 这里替换为实际的API调用
-        // const res = await api.login(params)
-        
-        // 模拟登录成功
-        const res = {
-          code: 200,
-          data: {
-            token: 1234567890,
-            userInfo: {
-              id: '1',
-              username: params.username || '用户' + Math.floor(Math.random() * 1000),
-              avatar: 'https://placeholder.com/150',
-              phone: params.phone || '13800138000',
-              email: 'user@example.com',
-              roles: ['user'],
-              permissions: ['read', 'write']
-            }
-          },
-          message: '登录成功'
-        }
-        
-        if (res.code === 200) {
+        const res = await login({
+          ...params,
+          clientId: import.meta.env.VITE_CLIENT_ID
+        })
+        if (res.code === '0') {
           await this.setToken(res.data.token)
-          this.userInfo = res.data.userInfo
-          this.roles = res.data.userInfo.roles
-          this.permissions = res.data.userInfo.permissions
-            chrome.runtime.sendMessage({
-                type: 'SET_TOKEN',
-                data: {
-                    token: res.data.token
-                }
-            }, function (response) {
-            });
-          // 存储用户信息到 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))
-          }
-          
-          ElMessage.success(res.message)
+          await this.getUserInfo()
+          ElMessage.success('登陆成功')
           return true
         } else {
           ElMessage.error(res.message || '登录失败')
           return false
         }
       } catch (error: any) {
-        ElMessage.error(error.message || '登录出错')
+        // ElMessage.error(error.message || '登录出错')
         return false
       } finally {
         this.loginLoading = false
@@ -165,11 +137,10 @@ export const useUserStore = defineStore('user', {
     },
     
     // 退出登录
-    async logout() {
+    async logout(flag = true) {
       try {
         // 这里替换为实际的API调用
-        // await api.logout()
-        
+        flag && await loginOut()
         // 清除用户数据
         await this.clearUserData()
         return true
@@ -185,27 +156,14 @@ export const useUserStore = defineStore('user', {
         if (!this.token) return false
         
         // 这里替换为实际的API调用
-        // const res = await api.getUserInfo()
-        
+        const res = await getUserInfoApi()
+        console.log(res,8888);
+
         // 模拟获取用户信息
-        const res = {
-          code: 200,
-          data: {
-            id: '1',
-            username: '当前用户',
-            avatar: 'https://placeholder.com/150',
-            phone: '13800138000',
-            email: 'user@example.com',
-            roles: ['user'],
-            permissions: ['read', 'write']
-          },
-          message: '获取用户信息成功'
-        }
-        if (res.code === 200) {
+        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({

+ 4 - 1
src/utils/request.js

@@ -63,6 +63,8 @@ service.interceptors.request.use(
 // 响应拦截器
 service.interceptors.response.use(
   (res) => {
+    console.log(res,85888);
+
     // 未设置状态码则默认成功状态
     const code = res.data.code || 200
     // 获取错误信息
@@ -75,6 +77,7 @@ service.interceptors.response.use(
       let filename = res.headers['content-disposition'].split('filename=')[1]
       return { fileName: filename, data: res.data }
     }
+    
     if (code === '401') {
       // if (!isRelogin.show) {
       //   isRelogin.show = true;
@@ -101,7 +104,7 @@ service.interceptors.response.use(
       ElMessage({ message: msg, type: 'warning', grouping: true })
       return Promise.reject(new Error(msg))
     }  else {
-      return Promise.resolve(res.data.data)
+      return Promise.resolve(res.data)
     }
   },
   (error) => {

+ 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',

部分文件因为文件数量过多而无法显示