فهرست منبع

统一web store

Michael Wang 3 سال پیش
والد
کامیت
942da1e316

+ 1 - 1
config.json

@@ -26,7 +26,7 @@
         },
         {
             "name": "线下环境",
-            "host": "http://192.168.10.120:8080"
+            "host": "http://192.168.10.224:80"
         }
     ]
 }

+ 3 - 0
package.json

@@ -16,6 +16,9 @@
   "main": "background.js",
   "dependencies": {
     "ant-design-vue": "^2.2.6",
+    "axios": "^0.21.1",
+    "axios-progress-bar": "^1.2.0",
+    "axios-retry": "^3.1.9",
     "core-js": "^3.6.5",
     "gm": "^1.23.1",
     "image-size": "^0.6.3",

+ 118 - 0
src/api/api.ts

@@ -0,0 +1,118 @@
+// import request from "requestretry";
+// import env from "./env";
+// import _logger from "./logger";
+// const logger = _logger("api.js");
+
+import { httpApp } from "@/plugins/axiosApp";
+import { store } from "@/store";
+
+// async function execute(uri, method, form) {
+//   // await new Promise((res) => setTimeout(res, 3000));
+//   console.log("sending...");
+//   console.log(uri, method, globalThis, globalThis.env);
+//   // console.log(globalThis.electron.env);
+//   // Object.assign(env, globalThis.electron.env);
+//   return new Promise((resolve, reject) => {
+//     request(
+//       {
+//         url: env.server.host + uri,
+//         method: method,
+//         json: true,
+//         timeout: 10000,
+//         form: form || {},
+//         headers: {
+//           "auth-info":
+//             "loginname=" + env.loginName + ";password=" + env.password,
+//         },
+//         maxAttempts: 50,
+//         retryDelay: 500,
+//         retryStrategy: request.RetryStrategies.HTTPOrNetworkError,
+//       },
+//       function (error, response, body) {
+//         if (response.statusCode == 200) {
+//           resolve(body);
+//         } else {
+//           const message =
+//             response.statusCode +
+//             " " +
+//             (error || "") +
+//             (response.headers["error-info"] || "");
+//           logger.error(message);
+//           reject(message);
+//         }
+//       }
+//     );
+//   });
+// }
+
+export function login() {
+  // return execute("/api/user/login", "GET");
+  return httpApp.get("/api/user/login", {
+    headers: {
+      "auth-info":
+        "loginname=" + store.env.loginName + ";password=" + store.env.password,
+    },
+    baseURL: store.env.server,
+  });
+}
+
+// export function getExams(pageNumber, pageSize) {
+//   let uri = "/api/exams";
+//   const param = [];
+//   if (pageNumber != undefined) {
+//     param.push("pageNumber=" + pageNumber);
+//   }
+//   if (pageSize != undefined) {
+//     param.push("pageSize=" + pageSize);
+//   }
+//   if (param.length > 0) {
+//     uri = uri + "?" + param.join("&");
+//   }
+//   return execute(uri, "GET");
+// }
+
+// export function getStudents(examId, pageNumber, pageSize, params) {
+//   const form = {
+//     examId: examId,
+//     pageNumber: pageNumber,
+//     pageSize: pageSize,
+//   };
+//   if (params != undefined) {
+//     for (const key in params) {
+//       if (params[key] && params[key] != "") {
+//         form[key] = params[key];
+//       }
+//     }
+//   }
+//   return execute("/api/exam/students", "POST", form);
+// }
+
+// export function countStudents(examId, params) {
+//   params = params || {};
+//   params.examId = examId;
+//   return execute("/api/exam/students/count", "POST", params);
+// }
+
+// export function getPackages(examId, upload, withUrl) {
+//   let uri = "/api/package/count/" + examId;
+//   const param = [];
+//   if (upload != undefined) {
+//     param.push("upload=" + (upload ? "true" : "false"));
+//   }
+//   if (withUrl != undefined) {
+//     param.push("withUrl=" + (withUrl ? "true" : "false"));
+//   }
+//   if (param.length > 0) {
+//     uri = uri + "?" + param.join("&");
+//   }
+//   return execute(uri, "GET");
+// }
+
+export default {
+  // execute,
+  login,
+  // getExams,
+  // getStudents,
+  // countStudents,
+  // getPackages,
+};

+ 16 - 3
src/background.ts

@@ -1,7 +1,7 @@
 "use strict";
 
 import path from "path";
-import { app, protocol, BrowserWindow } from "electron";
+import { app, protocol, BrowserWindow, ipcMain } from "electron";
 import { createProtocol } from "vue-cli-plugin-electron-builder/lib";
 import installExtension, { VUEJS3_DEVTOOLS } from "electron-devtools-installer";
 import config from "./lib/config";
@@ -15,14 +15,15 @@ protocol.registerSchemesAsPrivileged([
 async function createWindow() {
   // Create the browser window.
   const win = new BrowserWindow({
-    width: 800,
-    height: 600,
+    width: 1400,
+    height: 1000,
     webPreferences: {
       // Use pluginOptions.nodeIntegration, leave this alone
       // See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
       nodeIntegration: false, //process.env.ELECTRON_NODE_INTEGRATION as unknown as boolean,
       contextIsolation: true, //!process.env.ELECTRON_NODE_INTEGRATION,
       preload: path.join(__dirname, "preload.js"),
+      webSecurity: false,
     },
   });
 
@@ -85,3 +86,15 @@ if (isDevelopment) {
     });
   }
 }
+
+import theEnv, { merge } from "./lib/env";
+import { login } from "./lib/api";
+ipcMain.on("update-env", (event, env) => {
+  console.log(env, theEnv);
+  merge(env);
+  console.log(theEnv);
+  globalThis.env = theEnv;
+  // login();
+  // event.reply("asynchronous-reply", "pong");
+  event.returnValue = env;
+});

+ 1 - 0
src/constants/constants.ts

@@ -0,0 +1 @@
+export const YYYYMMDDHHmmss = "YYYY-MM-DD HH:mm:ss";

+ 18 - 0
src/filters/index.ts

@@ -0,0 +1,18 @@
+import { YYYYMMDDHHmmss } from "@/constants/constants";
+import moment from "moment";
+// import { store } from "@/store";
+
+export default {
+  /** 返回YYYY-MM-DD HH:mm:ss */
+  datetimeFilter(epochTime: number): string {
+    return moment(epochTime).format(YYYYMMDDHHmmss);
+  },
+  /** 根据fileServer得到完整的资源路径 */
+  // toCompleteUrl(path: string): string {
+  //   return store.setting.fileServer + path;
+  // },
+  /** 根据fileServer得到完整的资源路径 */
+  // toCompleteUrlWithFileServer(fileServer: string, path: string): string {
+  //   return fileServer + path;
+  // },
+};

+ 6 - 1
src/lib/api.ts

@@ -1,9 +1,14 @@
 import request from "requestretry";
-import env from "./env";
+// import env from "./env";
 import _logger from "./logger";
 const logger = _logger("api.js");
 
 async function execute(uri, method, form) {
+  // await new Promise((res) => setTimeout(res, 3000));
+  console.log("sending...");
+  console.log(uri, method, globalThis, globalThis.env);
+  // console.log(globalThis.electron.env);
+  // Object.assign(env, globalThis.electron.env);
   return new Promise((resolve, reject) => {
     request(
       {

+ 1 - 1
src/lib/config.ts

@@ -1,7 +1,7 @@
 import fs, { readFileSync } from "fs";
 import path from "path";
 import moment from "moment";
-import env from "./env";
+// import env from "./env";
 
 const configJson = readFileSync(path.join(__dirname, "../config.json"));
 const store = JSON.parse(configJson.toString());

+ 101 - 0
src/plugins/axiosApp.ts

@@ -0,0 +1,101 @@
+import Vue from "vue";
+// import Store from "@/store";
+import axios from "axios";
+// @ts-ignore
+import { loadProgressBar } from "axios-progress-bar";
+import { notifyInvalidTokenThrottled } from "./axiosNotice";
+import axiosRetry from "axios-retry";
+import { message } from "ant-design-vue";
+import { store } from "@/store";
+
+const config = {
+  baseURL: store.env.server || "",
+  timeout: 1 * 60 * 1000, // Timeout
+  withCredentials: true, // Check cross-site Access-Control
+};
+
+const _axiosApp = axios.create(config);
+axiosRetry(_axiosApp);
+
+_axiosApp.interceptors.request.use(
+  function (config) {
+    if (config.setGlobalMask) {
+      store.globalMask = true;
+    }
+    return config;
+  },
+  function (error) {
+    if (error.config.setGlobalMask) {
+      store.globalMask = false;
+    }
+    message.error({ content: error, duration: 10 });
+    console.log(error);
+    return Promise.reject(error);
+  }
+);
+
+// Add a response interceptor
+_axiosApp.interceptors.response.use(
+  (response) => {
+    if (response.config.setGlobalMask) {
+      store.globalMask = false;
+    }
+    return response;
+  },
+  (error) => {
+    if (error.config?.setGlobalMask) {
+      store.globalMask = false;
+    }
+    const showErrorMessage = !error.config?.noErrorMessage;
+    if (!error.response) {
+      if (showErrorMessage) {
+        // "Network Error" 网络不通,直接返回
+        message.error({
+          content: "网络连接异常,请检查网络设置。",
+          duration: 10,
+        });
+      }
+      return Promise.reject(error);
+    }
+    // 这里是返回状态码不为200时候的错误处理
+    const status = error.response.status;
+
+    // 登录失效 跳转登录页面
+    if (status == 403 || status == 401) {
+      notifyInvalidTokenThrottled();
+      return Promise.reject(error);
+    } else if (status == 405) {
+      if (showErrorMessage) {
+        message.error({ content: "没有权限!", duration: 10 });
+      }
+      return Promise.reject(error);
+    } else if (status == 502) {
+      if (showErrorMessage) {
+        message.error({ content: "服务器异常(502)!", duration: 10 });
+      }
+      return Promise.reject(error);
+    }
+
+    if (status != 200) {
+      const data = error.response.data;
+      if (data && data.message) {
+        if (showErrorMessage) {
+          message.error({ content: data.message, duration: 10 });
+        }
+      } else {
+        if (showErrorMessage) {
+          message.error({
+            content: "未定义异常: " + JSON.stringify(data, null, 2),
+            duration: 10,
+          });
+        }
+      }
+      return Promise.reject(error);
+    }
+  }
+);
+
+// _axiosApp.get = cachingGet(_axiosApp, cacheGetUrls);
+loadProgressBar(null, _axiosApp);
+
+export const httpApp = _axiosApp;

+ 32 - 0
src/plugins/axiosCache.ts

@@ -0,0 +1,32 @@
+import { AxiosInstance } from "axios";
+
+export default function (axios: AxiosInstance, regexes: [RegExp] | []) {
+  // cachingGet
+  const cache = new Map();
+
+  return function cachedGet(url: string) {
+    const key = url;
+
+    if (regexes.some((regex) => url.match(regex))) {
+      if (cache.has(key)) {
+        const request = cache.get(key);
+        // console.log("cache.get(key):" + request.then(v => console.log(v)));
+        return request;
+      } else {
+        // @ts-ignore
+        const request = axios.get(...arguments);
+        return request.then((v) => {
+          if (v.status === 200) {
+            // 如果能取到数据,才缓存
+            cache.set(key, request);
+          }
+          return request;
+        });
+      }
+    } else {
+      // @ts-ignore
+      const request = axios.get(...arguments);
+      return request;
+    }
+  };
+}

+ 2 - 0
src/plugins/axiosIndex.ts

@@ -0,0 +1,2 @@
+export { httpApp } from "./axiosApp";
+export { httpNoAuth } from "./axiosNoAuth";

+ 78 - 0
src/plugins/axiosNoAuth.ts

@@ -0,0 +1,78 @@
+import Vue from "vue";
+import axios from "axios";
+// @ts-ignore
+import { loadProgressBar } from "axios-progress-bar";
+import cachingGet from "./axiosCache";
+import axiosRetry from "axios-retry";
+import { message } from "ant-design-vue";
+
+const config = {
+  timeout: 60 * 1000, // Timeout
+};
+const cacheGetUrls: [RegExp] | [] = [];
+
+const _axiosNoAuth = axios.create(config);
+axiosRetry(_axiosNoAuth);
+
+/**
+ * 本应用的无auth api,或者第三方的api
+ * 1. 统一使用的http api,方便使用,无需fetch
+ * 2. 无默认headers
+ */
+
+_axiosNoAuth.interceptors.request.use(
+  function (config) {
+    return config;
+  },
+  function (error) {
+    message.error({ content: error, duration: 10 });
+    return Promise.reject(error);
+  }
+);
+
+// Add a response interceptor
+_axiosNoAuth.interceptors.response.use(
+  (response) => {
+    return response;
+  },
+  (error) => {
+    if (!error.response) {
+      // "Network Error" 网络不通,直接返回
+      message.error({
+        content: "网络连接异常,请检查网络设置。",
+        duration: 10,
+      });
+      return Promise.reject(error);
+    }
+
+    const data = error.response.data;
+    if (data && data.desc) {
+      message.error({ content: data.desc, duration: 10 });
+    } else {
+      message.error({
+        content: `异常(${error.response.status}): ${error.config.url}`,
+        duration: 10,
+      });
+    }
+    return Promise.reject(error);
+  }
+);
+
+_axiosNoAuth.get = cachingGet(_axiosNoAuth, cacheGetUrls);
+loadProgressBar(null, _axiosNoAuth);
+
+// Plugin.install = function (Vue) {
+//   Object.defineProperties(Vue.prototype, {
+//     $httpNoAuth: {
+//       get() {
+//         return _axiosNoAuth;
+//       },
+//     },
+//   });
+// };
+
+// Vue.use(Plugin);
+
+export default Plugin;
+
+export const httpNoAuth = _axiosNoAuth;

+ 15 - 0
src/plugins/axiosNotice.ts

@@ -0,0 +1,15 @@
+import Vue from "vue";
+import { throttle } from "lodash";
+import { message } from "ant-design-vue";
+// import { doLogout } from "@/api/markPage";
+
+export const notifyInvalidTokenThrottled = throttle(() => {
+  message.error({
+    content: "登录失效,请重新登录!",
+    duration: 10,
+  });
+  // setTimeout(() => {
+  //   doLogout();
+  // }, 3000);
+  console.log("登录失效");
+}, 1000);

+ 10 - 0
src/plugins/customComponents.ts

@@ -0,0 +1,10 @@
+// import {} from "vue";
+// // import AppType from "@/components/common/AppType.vue";
+
+// const components = {
+//   //  AppType,
+// };
+
+// Object.keys(components).forEach((key) => {
+//   App.component(key, components[key]);
+// });

+ 10 - 5
src/preload.ts

@@ -1,11 +1,16 @@
-import { contextBridge } from "electron";
+import { contextBridge, ipcRenderer } from "electron";
 
 import env from "./lib/env";
 import config from "./lib/config";
-import api from "./lib/api";
+// import api from "./lib/api";
 
-contextBridge.exposeInMainWorld("electron", {
+export const electronInWindow = {
   env: env,
   config,
-  api,
-});
+  // api,
+  updateEnv(env) {
+    ipcRenderer.sendSync("update-env", env);
+  },
+};
+
+contextBridge.exposeInMainWorld("electron", electronInWindow);

+ 6 - 0
src/router/index.ts

@@ -9,6 +9,12 @@ const routes: Array<RouteRecordRaw> = [
         /* webpackChunkName: "about" */ "../views/features/Login/Login.vue"
       ),
   },
+  {
+    path: "/home",
+    name: "Home",
+    component: () =>
+      import(/* webpackChunkName: "about" */ "../views/features/Home/Home.vue"),
+  },
   {
     path: "/about",
     name: "About",

+ 21 - 0
src/store.ts

@@ -0,0 +1,21 @@
+import { Store } from "@/types";
+
+const store = {
+  globalMask: false,
+  env: {
+    loginName: "",
+    password: "",
+    server: "",
+    user: {},
+  },
+  config: window.electron.config || {
+    server: "",
+    loginName: "",
+    password: "",
+  },
+} as Store;
+
+export { store };
+
+// @ts-ignore
+window.store = store;

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

@@ -0,0 +1,47 @@
+import filters from "@/filters";
+import { default as message } from "ant-design-vue/lib/message";
+// import { default as notification } from "ant-design-vue/lib/notification";
+// import { default as notification } from "ant-design-vue/es/notification/index.js";
+// import { notification } from "ant-design-vue";
+
+declare module "@vue/runtime-core" {
+  interface ComponentCustomProperties {
+    $filters: typeof filters;
+
+    $message: typeof message;
+    // $notification: typeof notification;
+  }
+}
+
+declare module "axios/index" {
+  interface AxiosRequestConfig {
+    noErrorMessage?: boolean | false;
+    setGlobalMask?: boolean | false;
+  }
+}
+
+import { electronInWindow } from "../preload";
+
+declare global {
+  interface Window {
+    electron: typeof electronInWindow;
+    // electron: string;
+  }
+}
+
+declare module "vue" {
+  // for volar
+  export interface GlobalComponents {
+    RouterLink: typeof import("vue-router")["RouterLink"];
+    RouterView: typeof import("vue-router")["RouterView"];
+    AModal: typeof import("ant-design-vue")["Modal"];
+    AButton: typeof import("ant-design-vue")["Button"];
+    AForm: typeof import("ant-design-vue")["Form"];
+    AFormItem: typeof import("ant-design-vue")["FormItem"];
+    AInput: typeof import("ant-design-vue")["Input"];
+    APopover: typeof import("ant-design-vue")["Popover"];
+    APopconfirm: typeof import("ant-design-vue")["Popconfirm"];
+    ATooltip: typeof import("ant-design-vue")["Tooltip"];
+    ASelect: typeof import("ant-design-vue")["Select"];
+  }
+}

+ 20 - 0
src/types/index.ts

@@ -0,0 +1,20 @@
+export interface Store {
+  globalMask: boolean;
+
+  env: {
+    server: string;
+    loginName: string;
+    password: string;
+    user: {
+      campusId: number;
+      schoolId: number;
+      userId: number;
+      userName: string;
+      userRole: string;
+    };
+  };
+
+  config: {
+    servers: Array<{ name: string; host: string }>;
+  };
+}

+ 3 - 6
src/views/features/Home/Home.vue

@@ -35,14 +35,11 @@
 </template>
 
 <script setup lang="ts">
-import env from "../../../lib/env";
-
-Object.assign(env, JSON.parse(window.localStorage.getItem("env") || "{}"));
-
+import { store } from "@/store";
 const {
   user: { userName },
-  exam: { iid, name, examTime },
-} = env;
+  // exam: { iid, name, examTime },
+} = store.env;
 
 // $("#user-name").html(env.user.userName);
 

+ 56 - 55
src/views/features/Login/Login.vue

@@ -2,24 +2,33 @@
   <div class="login-flex">
     <div class="login">
       <div class="logo"><img src="img/logo_blue.png" /></div>
-      <form>
+      <form @submit="loginAction">
         <div>
           <a-select
             style="width: 100%"
             v-model:value="server"
             :options="servers"
-            type="round"
             placeholder="请选择服务地址"
           >
           </a-select>
         </div>
         <div>
-          <input id="loginName-input" type="text" placeholder="请输入账号" />
+          <a-input
+            v-model:value="loginName"
+            id="loginName-input"
+            type="text"
+            placeholder="请输入账号"
+          />
         </div>
         <div>
-          <input id="password-input" type="password" placeholder="请输入密码" />
+          <a-input
+            v-model:value="password"
+            id="password-input"
+            type="password"
+            placeholder="请输入密码"
+          />
         </div>
-        <div><a href="##" id="login-button">登录</a></div>
+        <div><a href="##" id="login-button" @click="loginAction">登录</a></div>
       </form>
     </div>
     <div class="ft">
@@ -30,67 +39,59 @@
 
 <script setup lang="ts">
 import { onMounted, ref } from "vue";
+import { store } from "@/store";
+import { login } from "@/api/api";
+import router from "@/router";
 
 Object.assign(
-  electron.env,
+  window.electron.env,
   JSON.parse(window.localStorage.getItem("env") || "{}")
 );
 
-let server = ref("");
-const servers = electron.config.servers.map((v) => {
+let server = ref(null);
+const servers = store.config.servers.map((v) => {
   return { label: v.name, value: v.host };
 });
-console.log(servers);
+
+let loginName = ref("");
+let password = ref("");
 
 onMounted(() => {
   window.localStorage.clear();
 });
 
-// document.onkeydown = function (event) {
-//     var e = event || window.event;
-//     if (e && e.keyCode == 13) { //回车键的键值为13
-//         $('#login-button').click() //调用登录按钮的登录事件
-//     }
-// }
-
-// $('#login-button').click(() => {
-//     let index = $('#server-select').val()
-//     if (index != '') {
-//         env.server = config.servers[parseInt(index)]
-//     } else {
-//         env.server = undefined
-//     }
-//     env.loginName = $('#loginName-input').val()
-//     env.password = $('#password-input').val()
-//     if (env.server == undefined) {
-//         alert('请选择服务地址')
-//         return
-//     }
-//     if (env.loginName == '') {
-//         alert('请输入账号')
-//         return
-//     }
-//     if (env.password == '') {
-//         alert('请输入密码')
-//         return
-//     }
-//     api.login().then(user => {
-//         env.user = user
-//         window.localStorage.setItem('env', JSON.stringify(env))
-//         window.location.href = 'exam-list.html'
-//     }).catch(err => {
-//         alert('登陆失败,用户名或密码错误')
-//     })
-// })
-
-// const {
-//   user: { userName },
-//   exam: { iid, name, examTime },
-// } = env;
-
-// $("#user-name").html(env.user.userName);
+const loginAction = () => {
+  // window.electron.env.server = server.value;
 
-// $("#exam-title").find(".id").html(env.exam.id);
-// $("#exam-title").find(".name").html(env.exam.name);
-// $("#exam-title").find(".time").html(env.exam.examTime);
+  // window.electron.env.loginName = loginName.value;
+  // window.electron.env.password = password.value;
+  Object.assign(store.env, {
+    server: server.value,
+    loginName: loginName.value,
+    password: password.value,
+  });
+  // if (window.electron.env.server == undefined) {
+  //   alert("请选择服务地址");
+  //   return;
+  // }
+  // if (window.electron.env.loginName == "") {
+  //   alert("请输入账号");
+  //   return;
+  // }
+  // if (window.electron.env.password == "") {
+  //   alert("请输入密码");
+  //   return;
+  // }
+  login()
+    .then((user) => {
+      console.log(user);
+      Object.assign(store.env.user, user);
+      window.localStorage.setItem("env", JSON.stringify(store.env));
+      // window.location.href = 'exam-list.html'
+      router.push("/home");
+    })
+    .catch((err) => {
+      alert("登陆失败,用户名或密码错误");
+    });
+};
 </script>

+ 2 - 1
tsconfig.json

@@ -12,7 +12,8 @@
     "sourceMap": true,
     "baseUrl": ".",
     "types": [
-      "webpack-env"
+      "webpack-env",
+      "node_modules/@types"
     ],
     "paths": {
       "@/*": [

+ 29 - 0
yarn.lock

@@ -2368,6 +2368,25 @@ aws4@^1.8.0:
   resolved "https://registry.npm.taobao.org/aws4/download/aws4-1.11.0.tgz"
   integrity sha1-1h9G2DslGSUOJ4Ta9bCUeai0HFk=
 
+axios-progress-bar@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.npm.taobao.org/axios-progress-bar/download/axios-progress-bar-1.2.0.tgz#f9ee88dc9af977246be1ef07eedfa4c990c639c5"
+  integrity sha1-+e6I3Jr5dyRr4e8H7t+kyZDGOcU=
+
+axios-retry@^3.1.9:
+  version "3.1.9"
+  resolved "https://registry.npm.taobao.org/axios-retry/download/axios-retry-3.1.9.tgz#6c30fc9aeb4519aebaec758b90ef56fa03fe72e8"
+  integrity sha1-bDD8mutFGa667HWLkO9W+gP+cug=
+  dependencies:
+    is-retry-allowed "^1.1.0"
+
+axios@^0.21.1:
+  version "0.21.1"
+  resolved "https://registry.nlark.com/axios/download/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8"
+  integrity sha1-IlY0gZYvTWvemnbVFu8OXTwJsrg=
+  dependencies:
+    follow-redirects "^1.10.0"
+
 babel-code-frame@^6.22.0:
   version "6.26.0"
   resolved "https://registry.npm.taobao.org/babel-code-frame/download/babel-code-frame-6.26.0.tgz"
@@ -4992,6 +5011,11 @@ follow-redirects@^1.0.0:
   resolved "https://registry.nlark.com/follow-redirects/download/follow-redirects-1.14.1.tgz"
   integrity sha1-2RFN7Qoc/dM04WTmZirQK/2R/0M=
 
+follow-redirects@^1.10.0:
+  version "1.14.2"
+  resolved "https://registry.nlark.com/follow-redirects/download/follow-redirects-1.14.2.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.nlark.com%2Ffollow-redirects%2Fdownload%2Ffollow-redirects-1.14.2.tgz#cecb825047c00f5e66b142f90fed4f515dec789b"
+  integrity sha1-zsuCUEfAD15msUL5D+1PUV3seJs=
+
 for-in@^1.0.2:
   version "1.0.2"
   resolved "https://registry.npm.taobao.org/for-in/download/for-in-1.0.2.tgz"
@@ -6273,6 +6297,11 @@ is-resolvable@^1.0.0:
   resolved "https://registry.npm.taobao.org/is-resolvable/download/is-resolvable-1.1.0.tgz"
   integrity sha1-+xj4fOH+uSUWnJpAfBkxijIG7Yg=
 
+is-retry-allowed@^1.1.0:
+  version "1.2.0"
+  resolved "https://registry.npm.taobao.org/is-retry-allowed/download/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4"
+  integrity sha1-13hIi9CkZmo76KFIK58rqv7eqLQ=
+
 is-stream@^1.1.0:
   version "1.1.0"
   resolved "https://registry.nlark.com/is-stream/download/is-stream-1.1.0.tgz?cache=0&sync_timestamp=1628593099396&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fis-stream%2Fdownload%2Fis-stream-1.1.0.tgz"