yanghao 1 долоо хоног өмнө
parent
commit
e150fe63fe

+ 3 - 1
.env

@@ -1,4 +1,6 @@
 VITE_APP_TITLE='科瑞石油技术门户网站'
 
 VITE_DINGTALK_APP_ID='dingmr9ez0ecgbmscfeb'
-VITE_DINGTALK_REDIRECT_URI='http://1.94.244.160:5172/login?loginType=dingding&type=20'
+VITE_DINGTALK_REDIRECT_URI='http://1.94.244.160:5172/login?loginType=dingding&type=20'
+
+VITE_BASE_URL='http://1.94.244.160:6080'

+ 1 - 1
.env.dev

@@ -1 +1 @@
-VITE_BASE_URL='https://iot.deepoil.cc'
+VITE_BASE_URL='https://iot.deepoil.cc/admin-api'

+ 5 - 1
package.json

@@ -16,13 +16,17 @@
   "dependencies": {
     "@iconify/iconify": "^3.1.1",
     "@iconify/vue": "^5.0.0",
+    "@types/qs": "^6.14.0",
     "axios": "^1.13.1",
     "element-plus": "^2.11.7",
+    "jsencrypt": "^3.5.4",
     "motion-v": "^1.7.4",
     "pinia": "^3.0.3",
     "pinia-plugin-persistedstate": "^4.7.1",
+    "qs": "^6.14.0",
     "vue": "^3.5.22",
-    "vue-router": "^4.6.3"
+    "vue-router": "^4.6.3",
+    "web-storage-cache": "^1.1.1"
   },
   "devDependencies": {
     "@tailwindcss/vite": "^4.1.16",

+ 94 - 0
pnpm-lock.yaml

@@ -14,12 +14,18 @@ importers:
       '@iconify/vue':
         specifier: ^5.0.0
         version: 5.0.0(vue@3.5.22(typescript@5.9.3))
+      '@types/qs':
+        specifier: ^6.14.0
+        version: 6.14.0
       axios:
         specifier: ^1.13.1
         version: 1.13.1
       element-plus:
         specifier: ^2.11.7
         version: 2.11.7(vue@3.5.22(typescript@5.9.3))
+      jsencrypt:
+        specifier: ^3.5.4
+        version: 3.5.4
       motion-v:
         specifier: ^1.7.4
         version: 1.7.4(@vueuse/core@10.11.1(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))
@@ -29,12 +35,18 @@ importers:
       pinia-plugin-persistedstate:
         specifier: ^4.7.1
         version: 4.7.1(pinia@3.0.3(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3)))
+      qs:
+        specifier: ^6.14.0
+        version: 6.14.0
       vue:
         specifier: ^3.5.22
         version: 3.5.22(typescript@5.9.3)
       vue-router:
         specifier: ^4.6.3
         version: 4.6.3(vue@3.5.22(typescript@5.9.3))
+      web-storage-cache:
+        specifier: ^1.1.1
+        version: 1.1.1
     devDependencies:
       '@tailwindcss/vite':
         specifier: ^4.1.16
@@ -650,6 +662,9 @@ packages:
   '@types/node@22.19.0':
     resolution: {integrity: sha512-xpr/lmLPQEj+TUnHmR+Ab91/glhJvsqcjB+yY0Ix9GO70H6Lb4FHH5GeqdOE5btAx7eIMwuHkp4H2MSkLcqWbA==}
 
+  '@types/qs@6.14.0':
+    resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==}
+
   '@types/web-bluetooth@0.0.16':
     resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
 
@@ -829,6 +844,10 @@ packages:
     resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
     engines: {node: '>= 0.4'}
 
+  call-bound@1.0.4:
+    resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
+    engines: {node: '>= 0.4'}
+
   caniuse-lite@1.0.30001753:
     resolution: {integrity: sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw==}
 
@@ -1075,6 +1094,9 @@ packages:
   js-tokens@9.0.1:
     resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
 
+  jsencrypt@3.5.4:
+    resolution: {integrity: sha512-kNjfYEMNASxrDGsmcSQh/rUTmcoRfSUkxnAz+MMywM8jtGu+fFEZ3nJjHM58zscVnwR0fYmG9sGkTDjqUdpiwA==}
+
   jsesc@3.1.0:
     resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
     engines: {node: '>=6'}
@@ -1261,6 +1283,10 @@ packages:
     engines: {node: ^20.5.0 || >=22.0.0, npm: '>= 10'}
     hasBin: true
 
+  object-inspect@1.13.4:
+    resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
+    engines: {node: '>= 0.4'}
+
   ohash@2.0.11:
     resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
 
@@ -1335,6 +1361,10 @@ packages:
   proxy-from-env@1.1.0:
     resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
 
+  qs@6.14.0:
+    resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
+    engines: {node: '>=0.6'}
+
   quansync@0.2.11:
     resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
 
@@ -1377,6 +1407,22 @@ packages:
     resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==}
     engines: {node: '>= 0.4'}
 
+  side-channel-list@1.0.0:
+    resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
+    engines: {node: '>= 0.4'}
+
+  side-channel-map@1.0.1:
+    resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
+    engines: {node: '>= 0.4'}
+
+  side-channel-weakmap@1.0.2:
+    resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
+    engines: {node: '>= 0.4'}
+
+  side-channel@1.1.0:
+    resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
+    engines: {node: '>= 0.4'}
+
   sirv@3.0.2:
     resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==}
     engines: {node: '>=18'}
@@ -1572,6 +1618,9 @@ packages:
       typescript:
         optional: true
 
+  web-storage-cache@1.1.1:
+    resolution: {integrity: sha512-D0MieGooOs8RpsrK+vnejXnvh4OOv/+lTFB35JRkJJQt+uOjPE08XpaE0QBLMTRu47B1KGT/Nq3Gbag3Orinzw==}
+
   webpack-virtual-modules@0.6.2:
     resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
 
@@ -2064,6 +2113,8 @@ snapshots:
     dependencies:
       undici-types: 6.21.0
 
+  '@types/qs@6.14.0': {}
+
   '@types/web-bluetooth@0.0.16': {}
 
   '@types/web-bluetooth@0.0.20': {}
@@ -2321,6 +2372,11 @@ snapshots:
       es-errors: 1.3.0
       function-bind: 1.1.2
 
+  call-bound@1.0.4:
+    dependencies:
+      call-bind-apply-helpers: 1.0.2
+      get-intrinsic: 1.3.0
+
   caniuse-lite@1.0.30001753: {}
 
   chokidar@4.0.3:
@@ -2550,6 +2606,8 @@ snapshots:
 
   js-tokens@9.0.1: {}
 
+  jsencrypt@3.5.4: {}
+
   jsesc@3.1.0: {}
 
   json-parse-even-better-errors@4.0.0: {}
@@ -2699,6 +2757,8 @@ snapshots:
       shell-quote: 1.8.3
       which: 5.0.0
 
+  object-inspect@1.13.4: {}
+
   ohash@2.0.11: {}
 
   open@10.2.0:
@@ -2759,6 +2819,10 @@ snapshots:
 
   proxy-from-env@1.1.0: {}
 
+  qs@6.14.0:
+    dependencies:
+      side-channel: 1.1.0
+
   quansync@0.2.11: {}
 
   read-package-json-fast@4.0.0:
@@ -2812,6 +2876,34 @@ snapshots:
 
   shell-quote@1.8.3: {}
 
+  side-channel-list@1.0.0:
+    dependencies:
+      es-errors: 1.3.0
+      object-inspect: 1.13.4
+
+  side-channel-map@1.0.1:
+    dependencies:
+      call-bound: 1.0.4
+      es-errors: 1.3.0
+      get-intrinsic: 1.3.0
+      object-inspect: 1.13.4
+
+  side-channel-weakmap@1.0.2:
+    dependencies:
+      call-bound: 1.0.4
+      es-errors: 1.3.0
+      get-intrinsic: 1.3.0
+      object-inspect: 1.13.4
+      side-channel-map: 1.0.1
+
+  side-channel@1.1.0:
+    dependencies:
+      es-errors: 1.3.0
+      object-inspect: 1.13.4
+      side-channel-list: 1.0.0
+      side-channel-map: 1.0.1
+      side-channel-weakmap: 1.0.2
+
   sirv@3.0.2:
     dependencies:
       '@polka/url': 1.0.0-next.29
@@ -3006,6 +3098,8 @@ snapshots:
     optionalDependencies:
       typescript: 5.9.3
 
+  web-storage-cache@1.1.1: {}
+
   webpack-virtual-modules@0.6.2: {}
 
   which@2.0.2:

+ 11 - 19
src/api/user.ts

@@ -1,21 +1,13 @@
-import http from "@utils/request";
+import request from "@/config/axios";
 
 // 钉钉扫码登录
-export const qrcodeLogin = (data: any) => {
-  return http.post("/admin-api/system/auth/appSocialLogin", data);
-};
-
-// 刷新令牌
-export const refreshToken = (data: any) => {
-  return http.post("/admin-api/system/auth/refresh-token", data);
-};
-
-// 账号密码登录
-export const login = (data: any) => {
-  return http.post("/admin-api/system/auth/login", data);
-};
-
-// 退出登录
-export const logout = () => {
-  return http.post("/admin-api/system/auth/logout");
-};
+export function socialLogin(type: string, code: string, state: string) {
+  return request.post({
+    url: "/",
+    data: {
+      type,
+      code,
+      state,
+    },
+  });
+}

+ 28 - 0
src/config/axios/config.ts

@@ -0,0 +1,28 @@
+const config: {
+  base_url: string;
+  result_code: number | string;
+  default_headers: AxiosHeaders;
+  request_timeout: number;
+} = {
+  /**
+   * api请求基础路径
+   */
+  base_url: import.meta.env.VITE_BASE_URL as string,
+  /**
+   * 接口成功返回状态码
+   */
+  result_code: 200,
+
+  /**
+   * 接口请求超时时间
+   */
+  request_timeout: 120000,
+
+  /**
+   * 默认接口请求类型
+   * 可选值:application/x-www-form-urlencoded multipart/form-data
+   */
+  default_headers: "application/json",
+};
+
+export { config };

+ 6 - 0
src/config/axios/errorCode.ts

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

+ 47 - 0
src/config/axios/index.ts

@@ -0,0 +1,47 @@
+import { service } from './service'
+
+import { config } from './config'
+
+const { default_headers } = config
+
+const request = (option: any) => {
+  const { headersType, headers, ...otherOption } = option
+  return service({
+    ...otherOption,
+    headers: {
+      'Content-Type': headersType || default_headers,
+      ...headers
+    }
+  })
+}
+export default {
+  get: async <T = any>(option: any) => {
+    const res = await request({ method: 'GET', ...option })
+    return res.data as unknown as T
+  },
+  post: async <T = any>(option: any) => {
+    const res = await request({ method: 'POST', ...option })
+    return res.data as unknown as T
+  },
+  postOriginal: async (option: any) => {
+    const res = await request({ method: 'POST', ...option })
+    return res
+  },
+  delete: async <T = any>(option: any) => {
+    const res = await request({ method: 'DELETE', ...option })
+    return res.data as unknown as T
+  },
+  put: async <T = any>(option: any) => {
+    const res = await request({ method: 'PUT', ...option })
+    return res.data as unknown as T
+  },
+  download: async <T = any>(option: any) => {
+    const res = await request({ method: 'GET', responseType: 'blob', ...option })
+    return res as unknown as Promise<T>
+  },
+  upload: async <T = any>(option: any) => {
+    option.headersType = 'multipart/form-data'
+    const res = await request({ method: 'POST', ...option })
+    return res as unknown as Promise<T>
+  }
+}

+ 224 - 0
src/config/axios/service.ts

@@ -0,0 +1,224 @@
+import axios, {
+  AxiosError,
+  type AxiosInstance,
+  type AxiosResponse,
+  type InternalAxiosRequestConfig,
+} from "axios";
+
+import { ElMessage, ElMessageBox, ElNotification } from "element-plus";
+import qs from "qs";
+import { config } from "@/config/axios/config";
+import {
+  getAccessToken,
+  getRefreshToken,
+  removeToken,
+  setToken,
+} from "@utils/auth";
+import errorCode from "./errorCode";
+
+import { resetRouter } from "@/router";
+import { deleteUserCache } from "@hooks/useCache";
+const { result_code, base_url, request_timeout } = config;
+// 需要忽略的提示。忽略后,自动 Promise.reject('error')
+const ignoreMsgs = [
+  "无效的刷新令牌", // 刷新令牌被删除时,不用提示
+  "刷新令牌已过期", // 使用刷新令牌,刷新获取新的访问令牌时,结果因为过期失败,此时需要忽略。否则,会导致继续 401,无法跳转到登出界面
+];
+// 是否显示重新登录
+export const isRelogin = { show: false };
+// Axios 无感知刷新令牌,参考 https://www.dashingdog.cn/article/11 与 https://segmentfault.com/a/1190000020210980 实现
+// 请求队列
+let requestList: any[] = [];
+// 是否正在刷新中
+let isRefreshToken = false;
+// 请求白名单,无须token的接口
+const whiteList: string[] = ["/login", "/refresh-token"];
+
+// 创建axios实例
+const service: AxiosInstance = axios.create({
+  baseURL: base_url, // api 的 base_url
+  timeout: request_timeout, // 请求超时时间
+  withCredentials: false, // 禁用 Cookie 等信息
+  // 自定义参数序列化函数
+  paramsSerializer: (params) => {
+    return qs.stringify(params, { allowDots: true });
+  },
+});
+
+// request拦截器
+service.interceptors.request.use(
+  (config: InternalAxiosRequestConfig) => {
+    // 是否需要设置 token
+    let isToken = (config!.headers || {}).isToken === false;
+    whiteList.some((v) => {
+      if (config.url && config.url.indexOf(v) > -1) {
+        return (isToken = false);
+      }
+    });
+    if (getAccessToken() && !isToken) {
+      config.headers.Authorization = "Bearer " + getAccessToken(); // 让每个请求携带自定义token
+    }
+
+    const method = config.method?.toUpperCase();
+    // 防止 GET 请求缓存
+    if (method === "GET") {
+      config.headers["Cache-Control"] = "no-cache";
+      config.headers["Pragma"] = "no-cache";
+    }
+    // 自定义参数序列化函数
+    else if (method === "POST") {
+      const contentType =
+        config.headers["Content-Type"] || config.headers["content-type"];
+      if (contentType === "application/x-www-form-urlencoded") {
+        if (config.data && typeof config.data !== "string") {
+          config.data = qs.stringify(config.data);
+        }
+      }
+    }
+    return config;
+  },
+  (error: AxiosError) => {
+    // Do something with request error
+    console.log(error); // for debug
+    return Promise.reject(error);
+  }
+);
+
+// response 拦截器
+service.interceptors.response.use(
+  async (response: AxiosResponse<any>) => {
+    let { data } = response;
+    const config = response.config;
+    if (!data) {
+      // 返回“[HTTP]请求没有返回值”;
+      throw new Error();
+    }
+
+    // 未设置状态码则默认成功状态
+    // 二进制数据则直接返回,例如说 Excel 导出
+    if (
+      response.request.responseType === "blob" ||
+      response.request.responseType === "arraybuffer"
+    ) {
+      // 注意:如果导出的响应为 json,说明可能失败了,不直接返回进行下载
+      if (response.data.type !== "application/json") {
+        return response.data;
+      }
+      data = await new Response(response.data).json();
+    }
+    const code = data.code || result_code;
+    // 获取错误信息
+    const msg = data.msg || (errorCode as any)[code] || errorCode.default;
+    if (ignoreMsgs.indexOf(msg) !== -1) {
+      // 如果是忽略的错误码,直接返回 msg 异常
+      return Promise.reject(msg);
+    } else if (code === 401) {
+      // 如果未认证,并且未进行刷新令牌,说明可能是访问令牌过期了
+      if (!isRefreshToken) {
+        isRefreshToken = true;
+        // 1. 如果获取不到刷新令牌,则只能执行登出操作
+        if (!getRefreshToken()) {
+          return handleAuthorized();
+        }
+        // 2. 进行刷新访问令牌
+        try {
+          const refreshTokenRes = await refreshToken();
+          // 2.1 刷新成功,则回放队列的请求 + 当前请求
+          setToken((await refreshTokenRes).data.data);
+          config.headers!.Authorization = "Bearer " + getAccessToken();
+          requestList.forEach((cb: any) => {
+            cb();
+          });
+          requestList = [];
+          return service(config);
+        } catch (e) {
+          // 为什么需要 catch 异常呢?刷新失败时,请求因为 Promise.reject 触发异常。
+          // 2.2 刷新失败,只回放队列的请求
+          requestList.forEach((cb: any) => {
+            cb();
+          });
+          // 提示是否要登出。即不回放当前请求!不然会形成递归
+          return handleAuthorized();
+        } finally {
+          requestList = [];
+          isRefreshToken = false;
+        }
+      } else {
+        // 添加到队列,等待刷新获取到新的令牌
+        return new Promise((resolve) => {
+          requestList.push(() => {
+            config.headers!.Authorization = "Bearer " + getAccessToken(); // 让每个请求携带自定义token 请根据实际情况自行修改
+            resolve(service(config));
+          });
+        });
+      }
+    } else if (code === 500) {
+      ElMessage.error(msg);
+      return Promise.reject(new Error(msg));
+    } else if (code === 901) {
+      ElMessage.error({
+        offset: 300,
+        dangerouslyUseHTMLString: true,
+        message: "演示模式,无法进行写操作!",
+      });
+      return Promise.reject(new Error(msg));
+    } else if (code !== 200) {
+      if (msg === "无效的刷新令牌") {
+        // hard coding:忽略这个提示,直接登出
+        console.log(msg);
+        return handleAuthorized();
+      } else {
+        ElNotification.error({ title: msg });
+      }
+      return Promise.reject("error");
+    } else {
+      return data;
+    }
+  },
+  (error: AxiosError) => {
+    console.log("err" + error); // for debug
+    let { message } = error;
+
+    if (message === "Network Error") {
+      message = "操作失败,系统异常!";
+    } else if (message.includes("timeout")) {
+      message = "接口请求超时,请刷新页面重试!";
+    } else if (message.includes("Request failed with status code")) {
+      message = "请求出错,请稍候重试" + message.substr(message.length - 3);
+    }
+    ElMessage.error(message);
+    return Promise.reject(error);
+  }
+);
+
+const refreshToken = async () => {
+  return await axios.post(
+    base_url + "/system/auth/refresh-token?refreshToken=" + getRefreshToken()
+  );
+};
+const handleAuthorized = () => {
+  if (!isRelogin.show) {
+    // 如果已经到登录页面则不进行弹窗提示
+    if (window.location.href.includes("login")) {
+      return;
+    }
+    isRelogin.show = true;
+    ElMessageBox.confirm("登陆超时,请重新登陆", "确定", {
+      showCancelButton: false,
+      closeOnClickModal: false,
+      showClose: false,
+      closeOnPressEscape: false,
+      confirmButtonText: "重新登陆",
+      type: "warning",
+    }).then(() => {
+      resetRouter(); // 重置静态路由表
+      deleteUserCache(); // 删除用户缓存
+      removeToken();
+      isRelogin.show = false;
+      // 干掉token后再走一次路由让它过router.beforeEach的校验
+      window.location.href = window.location.href;
+    });
+  }
+  return Promise.reject("登陆超时,请重新登陆");
+};
+export { service };

+ 38 - 0
src/hooks/useCache.ts

@@ -0,0 +1,38 @@
+/**
+ * 配置浏览器本地存储的方式,可直接存储对象数组。
+ */
+
+import WebStorageCache from "web-storage-cache";
+
+type CacheType = "localStorage" | "sessionStorage";
+
+export const CACHE_KEY = {
+  // 用户相关
+  ROLE_ROUTERS: "roleRouters",
+  USER: "user",
+  // 系统设置
+  IS_DARK: "isDark",
+  LANG: "lang",
+  THEME: "theme",
+  LAYOUT: "layout",
+  DICT_CACHE: "dictCache",
+  // 登录表单
+  LoginForm: "loginForm",
+};
+
+export const useCache = (type: CacheType = "localStorage") => {
+  const wsCache: WebStorageCache = new WebStorageCache({
+    storage: type,
+  });
+
+  return {
+    wsCache,
+  };
+};
+
+export const deleteUserCache = () => {
+  const { wsCache } = useCache();
+  wsCache.delete(CACHE_KEY.USER);
+  wsCache.delete(CACHE_KEY.ROLE_ROUTERS);
+  // 注意,不要清理 LoginForm 登录表单
+};

+ 42 - 1
src/router/index.ts

@@ -10,6 +10,10 @@ import Command from "@/views/command.vue";
 import ChatBI from "@/views/chatbi.vue";
 import Login from "@/views/login.vue";
 
+import { getAccessToken } from "@utils/auth";
+import { socialLogin } from "@/api/user";
+import * as authUtil from "@/utils/auth";
+
 const routes: RouteRecordRaw[] = [
   {
     path: "/",
@@ -47,6 +51,43 @@ const router = createRouter({
   },
 });
 
-const publicPages = ["/", "/login"];
+export const resetRouter = (): void => {
+  const resetWhiteNameList = ["Redirect", "Login", "NoFind", "Root"];
+  router.getRoutes().forEach((route) => {
+    const { name } = route;
+    if (name && !resetWhiteNameList.includes(name as string)) {
+      router.hasRoute(name) && router.removeRoute(name);
+    }
+  });
+};
+
+const whiteList = ["/login", "/social-login", "/auth-redirect"];
+router.beforeEach(async (to, from, next) => {
+  if (getAccessToken()) {
+    if (to.path === "/login") {
+      next({ path: "/" });
+    } else {
+      next();
+    }
+  } else {
+    if (whiteList.indexOf(to.path) !== -1) {
+      const code = to.query.code;
+      if (code) {
+        const res = await socialLogin(
+          "20",
+          typeof code === "string" ? code : "",
+          "22"
+        );
+        console.log(">>>>>>>>>>>>>>>>>>>>>>>>>", res);
+        authUtil.setToken(res);
+        next({ path: "/" });
+      } else {
+        next(); // 正常导航
+      }
+    } else {
+      next(`/login`);
+    }
+  }
+});
 
 export default router;

+ 10 - 22
src/stores/loginStore.ts

@@ -1,30 +1,18 @@
 import { defineStore } from "pinia";
-import { qrcodeLogin } from "@api/user";
 
 export const useAuthStore = defineStore("login", {
   state: () => ({
-    userId: "",
-    accessToken: "",
-    refreshToken: "",
-    expiresTime: "",
+    // userId: "",
+    // accessToken: "",
+    // refreshToken: "",
+    // expiresTime: "",
   }),
 
-  actions: {
-    // 登录成功后的操作
-    async login() {
-      const res = await qrcodeLogin({
-        type: 10,
-        code: "1024",
-        state: "9b2ffbc1-7425-4155-9894-9d5c08541d62",
-      });
+  actions: {},
 
-      this.userInfo = res.data;
-    },
-  },
-
-  persist: {
-    storage: localStorage,
-    key: "userInfo",
-    pick: ["userInfo"],
-  },
+  // persist: {
+  //   storage: localStorage,
+  //   key: "userInfo",
+  //   pick: ["accessToken", "refreshToken"],
+  // },
 });

+ 37 - 0
src/types/global.d.ts

@@ -4,4 +4,41 @@ declare global {
   interface Window {
     DDLogin: (options: any) => void;
   }
+
+  type AxiosHeaders =
+    | "application/json"
+    | "application/x-www-form-urlencoded"
+    | "multipart/form-data";
+
+  type AxiosMethod =
+    | "get"
+    | "post"
+    | "delete"
+    | "put"
+    | "GET"
+    | "POST"
+    | "DELETE"
+    | "PUT";
+
+  type AxiosResponseType =
+    | "arraybuffer"
+    | "blob"
+    | "document"
+    | "json"
+    | "text"
+    | "stream";
+
+  interface AxiosConfig {
+    params?: any;
+    data?: any;
+    url?: string;
+    method?: AxiosMethod;
+    headersType?: string;
+    responseType?: AxiosResponseType;
+  }
+
+  interface IResponse<T = any> {
+    code: string;
+    data: T extends any ? T : T & any;
+  }
 }

+ 69 - 0
src/utils/auth.ts

@@ -0,0 +1,69 @@
+import { useCache, CACHE_KEY } from "@hooks/useCache";
+import { decrypt, encrypt } from "@utils/jsencrypt";
+
+const { wsCache } = useCache();
+
+const AccessTokenKey = "ACCESS_TOKEN";
+const RefreshTokenKey = "REFRESH_TOKEN";
+type TokenType = {
+  id: number; // 编号
+  accessToken: string; // 访问令牌
+  refreshToken: string; // 刷新令牌
+  userId: number; // 用户编号
+  userType?: number; //用户类型
+  clientId?: string; //客户端编号
+  expiresTime: number; //过期时间
+};
+// 获取token
+export const getAccessToken = () => {
+  // 此处与TokenKey相同,此写法解决初始化时Cookies中不存在TokenKey报错
+  const accessToken = wsCache.get(AccessTokenKey);
+  return accessToken ? accessToken : wsCache.get("ACCESS_TOKEN");
+};
+
+// 刷新token
+export const getRefreshToken = () => {
+  return wsCache.get(RefreshTokenKey);
+};
+
+// 设置token
+export const setToken = (token: TokenType) => {
+  wsCache.set(RefreshTokenKey, token.refreshToken);
+  wsCache.set(AccessTokenKey, token.accessToken);
+};
+
+// 删除token
+export const removeToken = () => {
+  wsCache.delete(AccessTokenKey);
+  wsCache.delete(RefreshTokenKey);
+};
+
+/** 格式化token(jwt格式) */
+export const formatToken = (token: string): string => {
+  return "Bearer " + token;
+};
+// ========== 账号相关 ==========
+
+export type LoginFormType = {
+  tenantName?: string;
+  username: string;
+  password: string;
+  rememberMe?: boolean;
+};
+
+export const getLoginForm = () => {
+  const loginForm: LoginFormType = wsCache.get(CACHE_KEY.LoginForm);
+  if (loginForm) {
+    loginForm.password = decrypt(loginForm.password) as string;
+  }
+  return loginForm;
+};
+
+export const setLoginForm = (loginForm: LoginFormType) => {
+  loginForm.password = encrypt(loginForm.password) as string;
+  wsCache.set(CACHE_KEY.LoginForm, loginForm, { exp: 30 * 24 * 60 * 60 });
+};
+
+export const removeLoginForm = () => {
+  wsCache.delete(CACHE_KEY.LoginForm);
+};

+ 29 - 0
src/utils/jsencrypt.ts

@@ -0,0 +1,29 @@
+import { JSEncrypt } from "jsencrypt";
+
+const publicKey =
+  "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\n" +
+  "nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==";
+
+const privateKey =
+  "MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY\n" +
+  "7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKN\n" +
+  "PuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gA\n" +
+  "kM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWow\n" +
+  "cSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99Ecv\n" +
+  "DQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthh\n" +
+  "YhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3\n" +
+  "UP8iWi1Qw0Y=";
+
+// 加密
+export const encrypt = (txt: string) => {
+  const encryptor = new JSEncrypt();
+  encryptor.setPublicKey(publicKey); // 设置公钥
+  return encryptor.encrypt(txt); // 对数据进行加密
+};
+
+// 解密
+export const decrypt = (txt: string) => {
+  const encryptor = new JSEncrypt();
+  encryptor.setPrivateKey(privateKey); // 设置私钥
+  return encryptor.decrypt(txt); // 对数据进行解密
+};

+ 0 - 74
src/utils/request.ts

@@ -1,74 +0,0 @@
-import axios, {
-  type AxiosResponse,
-  type InternalAxiosRequestConfig,
-} from "axios";
-import { ElMessage, ElMessageBox } from "element-plus";
-import { useAuthStore } from "@stores/loginStore";
-
-const request = axios.create({
-  baseURL: import.meta.env.VITE_BASE_URL as string,
-  headers: {
-    "Content-Type": "application/json",
-  },
-  timeout: 5000,
-});
-
-// 请求拦截:注入 token
-request.interceptors.request.use(
-  (config: InternalAxiosRequestConfig) => {
-    const authStore = useAuthStore();
-
-    // 添加 token
-    if (authStore.accessToken) {
-      config.headers.Authorization = `Bearer ${authStore.accessToken}`;
-    }
-
-    // 添加请求时间戳,防止缓存
-    if (config.method?.toLowerCase() === "get" && config.params) {
-      config.params = {
-        ...config.params,
-        _t: Date.now(),
-      };
-    }
-
-    return config;
-  },
-  (error) => {
-    console.error("请求配置错误:", error);
-    return Promise.reject(error);
-  }
-);
-
-request.interceptors.response.use(
-  (response: AxiosResponse) => {
-    return response.data;
-  },
-  (error) => {
-    return Promise.reject(error);
-  }
-);
-
-type ResponseData = {
-  code: number;
-  msg: string;
-  data: any;
-};
-
-const http = {
-  async get(url: string, data?: any): Promise<ResponseData> {
-    return await request.get(url, { params: data });
-  },
-
-  async post(url: string, data?: any): Promise<ResponseData> {
-    return await request.post(url, data);
-  },
-
-  async put(url: string, data?: any): Promise<ResponseData> {
-    return await request.put(url, data);
-  },
-
-  async delete(url: string, data?: any): Promise<ResponseData> {
-    return await request.delete(url, { params: data });
-  },
-};
-export default http;

+ 37 - 55
src/views/login.vue

@@ -80,10 +80,12 @@
 </template>
 
 <script lang="ts" setup>
-import { nextTick, onMounted, reactive, ref } from "vue";
+import { nextTick, reactive, ref } from "vue";
 import { ElMessage } from "element-plus";
 import logo from "@/assets/images/logo.png";
 import bgImage from "@/assets/images/bg.png";
+import { socialLogin } from "@/api/user";
+import * as authUtil from "@/utils/auth";
 
 type LoginForm = {
   username: string;
@@ -132,6 +134,30 @@ const _getRandomString = (len: number) => {
   }
   return pwd;
 };
+
+const handleMsg = (state?: string) => {
+  return async function (event: MessageEvent) {
+    if (event.origin === "https://login.dingtalk.com") {
+      let loginTmpCode = event.data;
+      console.log("收到钉钉扫码登录消息:", loginTmpCode);
+      window.location.href =
+        "https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=" +
+        import.meta.env.VITE_DINGTALK_APP_ID +
+        "&response_type=code&scope=snsapi_login&state=" +
+        state +
+        "&redirect_uri=" +
+        import.meta.env.VITE_DINGTALK_REDIRECT_URI +
+        "&loginTmpCode=" +
+        loginTmpCode;
+
+      // const res = await socialLogin("10", loginTmpCode as string, state!);
+
+      // console.log("登录结果:", res);
+
+      // authUtil.setToken(res);
+    }
+  };
+};
 const initDingLogin = () => {
   let state = _getRandomString(10);
   const gotoUrl = encodeURIComponent(
@@ -150,7 +176,7 @@ const initDingLogin = () => {
     window.DDLogin({
       id: "login_container",
       goto: gotoUrl,
-      style: "border:none;background-color:#FFFFFF;",
+      style: "border:none;background-color:transparent;",
       width: "100%",
       height: "290",
     });
@@ -168,68 +194,24 @@ const initDingLogin = () => {
     }
   });
 
-  const handleMessage = function (event: MessageEvent) {
-    if (event.origin === "https://login.dingtalk.com") {
-      let loginTmpCode = event.data;
-      window.location.href =
-        "https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=" +
-        import.meta.env.VITE_DINGTALK_APP_ID +
-        "&response_type=code&scope=snsapi_login&state=" +
-        state +
-        "&redirect_uri=" +
-        import.meta.env.VITE_DINGTALK_REDIRECT_URI +
-        "&loginTmpCode=" +
-        loginTmpCode;
-    }
-  };
-
-  window.addEventListener("message", handleMessage, false);
+  window.addEventListener("message", handleMsg(state), false);
 };
 
-// const handleDirectLogin = async (loginTmpCode: string): Promise<void> => {
-//   try {
-//     // 调用后端接口,让后端处理钉钉认证
-//     const response = await fetch("/api/auth/dingtalk/login", {
-//       method: "POST",
-//       headers: {
-//         "Content-Type": "application/json",
-//       },
-//       body: JSON.stringify({
-//         loginTmpCode,
-//         state: currentState,
-//       }),
-//     });
-
-//     if (!response.ok) {
-//       throw new Error("登录请求失败");
-//     }
-
-//     const result = await response.json();
-
-//     if (result.success) {
-//       // 登录成功,处理返回的 token 和用户信息
-//       onLoginSuccess(result.data);
-//     } else {
-//       onLoginFailed(result.message || "登录失败");
-//     }
-//   } catch (error) {
-//     console.error("钉钉登录失败:", error);
-
-//   }
-// };
-
 const qrLogin = () => {
   showQrOnly.value = true;
-  // initDingLogin();
+  initDingLogin();
 };
 
 const backToPasswordLogin = () => {
   showQrOnly.value = false;
+  // 卸载钉钉扫码登录组件
+  let box = document.getElementById("login_container");
+  if (box) {
+    box.innerHTML = "";
+  }
+  // 移除监听事件
+  window.removeEventListener("message", handleMsg());
 };
-
-onMounted(() => {
-  initDingLogin();
-});
 </script>
 
 <style scoped>

+ 2 - 1
tsconfig.app.json

@@ -12,7 +12,8 @@
       "@api/*": ["./src/api/*"],
       "@utils/*": ["./src/utils/*"],
       "@types/*": ["./src/types/*"],
-      "@stores/*": ["./src/stores/*"]
+      "@stores/*": ["./src/stores/*"],
+      "@hooks/*": ["./src/hooks/*"]
     }
   }
 }

+ 1 - 0
vite.config.ts

@@ -39,6 +39,7 @@ export default defineConfig({
       "@utils": fileURLToPath(new URL("./src/utils", import.meta.url)),
       "@types": fileURLToPath(new URL("./src/types", import.meta.url)),
       "@stores": fileURLToPath(new URL("./src/stores", import.meta.url)),
+      "@hooks": fileURLToPath(new URL("./src/hooks", import.meta.url)),
     },
   },
 });