浏览代码

refactor: 重构 Subconverter 组件为模块化架构

  - 将大型组件拆分为独立的业务模块和组件
  - 新增 Composables 架构:useSubscription.js、useSubscriptionForm.js、useUrlParser.js
  - 提取服务层:backendService.js、shortUrlService.js、configUploadService.js
  - 创建可复用组件:ConfigUploadDialog.vue、UrlParseDialog.vue
  - 新增配置模块:client-types.js、constants.js、remote-configs.js
  - 重构工具模块:formatters.js、search.js、storage.js、validators.js
  - 改进代码组织,提升可维护性和可测试性
  - 优化用户体验,添加计算属性控制按钮状态
CareyWong 2 周之前
父节点
当前提交
9bcbeee762

+ 3 - 5
package.json

@@ -3,7 +3,7 @@
   "version": "0.1.0",
   "private": true,
   "engines": {
-    "node": ">=22.0.0"
+    "node": "22.x"
   },
   "scripts": {
     "serve": "vue-cli-service serve",
@@ -16,8 +16,7 @@
     "btoa": "^1.2.1",
     "core-js": "^3.12.1",
     "element-ui": "^2.15.1",
-    "register-service-worker": "^1.7.1",
-    "vue": "^2.6.10",
+        "vue": "^2.6.10",
     "vue-clipboard2": "^0.3.1",
     "vue-router": "^3.5.1"
   },
@@ -26,8 +25,7 @@
     "@babel/eslint-parser": "^7.25.9",
     "@vue/cli-plugin-babel": "5",
     "@vue/cli-plugin-eslint": "5",
-    "@vue/cli-plugin-pwa": "5",
-    "@vue/cli-plugin-router": "5",
+        "@vue/cli-plugin-router": "5",
     "@vue/cli-service": "5",
     "babel-plugin-component": "^1.1.1",
     "babel-plugin-import": "^1.13.3",

二进制
public/favicon.ico


二进制
public/favicons/android-chrome-192x192.png


二进制
public/favicons/android-chrome-512x512.png


二进制
public/favicons/apple-touch-icon.png


二进制
public/favicons/favicon-16x16.png


二进制
public/favicons/favicon-32x32.png


二进制
public/favicons/favicon.ico


+ 1 - 0
public/favicons/site.webmanifest

@@ -0,0 +1 @@
+{"name":"sub-web","short_name":"sub-web","icons":[{"src":"/favicons/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/favicons/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#2196F3","background_color":"#ffffff","display":"standalone"}

+ 12 - 2
public/index.html

@@ -4,8 +4,18 @@
     <meta charset="utf-8">
     <meta http-equiv="X-UA-Compatible" content="IE=edge">
     <meta name="viewport" content="width=device-width,initial-scale=1.0">
-    <meta name="theme-color" content="#00142A">
-    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+    <!-- Standard favicon -->
+    <link rel="icon" href="<%= BASE_URL %>favicons/favicon.ico">
+    <!-- Favicon for modern browsers -->
+    <link rel="icon" type="image/png" sizes="32x32" href="<%= BASE_URL %>favicons/favicon-32x32.png">
+    <link rel="icon" type="image/png" sizes="16x16" href="<%= BASE_URL %>favicons/favicon-16x16.png">
+    <!-- Apple Touch Icon -->
+    <link rel="apple-touch-icon" sizes="180x180" href="<%= BASE_URL %>favicons/apple-touch-icon.png">
+    <!-- Android Chrome icons -->
+    <link rel="icon" type="image/png" sizes="192x192" href="<%= BASE_URL %>favicons/android-chrome-192x192.png">
+    <link rel="icon" type="image/png" sizes="512x512" href="<%= BASE_URL %>favicons/android-chrome-512x512.png">
+    <!-- Web Manifest -->
+    <link rel="manifest" href="<%= BASE_URL %>favicons/site.webmanifest">
     <title>sub-web</title>
   </head>
   <body>

+ 0 - 20
public/manifest.json

@@ -1,20 +0,0 @@
-{
-    "short_name": "sub-web",
-    "name": "sub-web",
-    "icon": [
-        {
-            "src": "./img/icons/icon-192x192.png",
-            "sizes": "192x192",
-            "type": "image/png"
-        },
-        {
-            "src": "./img/icons/android-chrome-512x512.png",
-            "sizes": "512x512",
-            "type": "image/png"
-        }
-    ],
-    "start_url": "index.html",
-    "display": "standalone",
-    "background_color": "#002140",
-    "theme_color": "#002140"
-}

+ 84 - 0
src/components/ConfigUploadDialog.vue

@@ -0,0 +1,84 @@
+<template>
+  <el-dialog
+    :visible.sync="localVisible"
+    :show-close="false"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    width="700px"
+  >
+    <div slot="title">
+      Remote config upload
+      <el-popover trigger="hover" placement="right" style="margin-left: 10px">
+        <el-link type="primary" :href="sampleConfig" target="_blank" icon="el-icon-info">参考配置</el-link>
+        <i class="el-icon-question" slot="reference"></i>
+      </el-popover>
+    </div>
+
+    <el-form label-position="left">
+      <el-form-item prop="uploadConfig">
+        <el-input
+          v-model="localUploadConfig"
+          type="textarea"
+          :autosize="{ minRows: 15, maxRows: 30 }"
+          maxlength="10000"
+          show-word-limit
+        />
+      </el-form-item>
+    </el-form>
+
+    <div slot="footer" class="dialog-footer">
+      <el-button @click="handleCancel">取 消</el-button>
+      <el-button type="primary" @click="handleConfirm" :disabled="localUploadConfig.length === 0">
+        确 定
+      </el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import { CONSTANTS } from '@/config/constants';
+
+export default {
+  name: 'ConfigUploadDialog',
+  props: {
+    visible: {
+      type: Boolean,
+      default: false
+    },
+    uploadConfig: {
+      type: String,
+      default: ''
+    },
+    loading: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      sampleConfig: CONSTANTS.REMOTE_CONFIG_SAMPLE,
+      localUploadConfig: this.uploadConfig,
+      localVisible: this.visible
+    };
+  },
+  watch: {
+    uploadConfig(newVal) {
+      this.localUploadConfig = newVal;
+    },
+    visible(newVal) {
+      this.localVisible = newVal;
+    },
+    localVisible(newVal) {
+      this.$emit('update:visible', newVal);
+    }
+  },
+  methods: {
+    handleCancel() {
+      this.$emit('cancel');
+    },
+    handleConfirm() {
+      this.$emit('confirm', this.localUploadConfig);
+    }
+  }
+};
+</script>

+ 71 - 0
src/components/UrlParseDialog.vue

@@ -0,0 +1,71 @@
+<template>
+  <el-dialog
+    :visible.sync="localVisible"
+    :show-close="false"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    width="700px"
+  >
+    <div slot="title">
+      解析 Subconverter 链接
+    </div>
+
+    <el-form label-position="left" :inline="true">
+      <el-form-item prop="loadConfig" label="订阅链接:" label-width="85px">
+        <el-input v-model="localLoadConfig" style="width: 565px" />
+      </el-form-item>
+    </el-form>
+
+    <div slot="footer" class="dialog-footer">
+      <el-button @click="handleCancel">取 消</el-button>
+      <el-button type="primary" @click="handleConfirm" :disabled="localLoadConfig.length === 0">
+        确 定
+      </el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+export default {
+  name: 'UrlParseDialog',
+  props: {
+    visible: {
+      type: Boolean,
+      default: false
+    },
+    loadConfig: {
+      type: String,
+      default: ''
+    },
+    loading: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      localLoadConfig: this.loadConfig,
+      localVisible: this.visible
+    };
+  },
+  watch: {
+    loadConfig(newVal) {
+      this.localLoadConfig = newVal;
+    },
+    visible(newVal) {
+      this.localVisible = newVal;
+    },
+    localVisible(newVal) {
+      this.$emit('update:visible', newVal);
+    }
+  },
+  methods: {
+    handleCancel() {
+      this.$emit('cancel');
+    },
+    handleConfirm() {
+      this.$emit('confirm', this.localLoadConfig);
+    }
+  }
+};
+</script>

+ 153 - 0
src/composables/useSubscription.js

@@ -0,0 +1,153 @@
+import { validateForm } from '@/utils/validators';
+
+/**
+ * 订阅链接生成逻辑
+ */
+export function useSubscription() {
+  /**
+   * 构建基础URL
+   * @param {Object} form - 表单数据
+   * @param {string} processedSubUrl - 处理后的订阅链接
+   * @param {string} currentBackend - 当前后端地址
+   * @returns {string} 基础URL
+   */
+  const buildBaseUrl = (form, processedSubUrl, currentBackend) => {
+    return currentBackend +
+      "target=" + form.clientType +
+      "&url=" + encodeURIComponent(processedSubUrl) +
+      "&insert=" + form.insert;
+  };
+
+  /**
+   * 构建布尔参数
+   * @param {Object} form - 表单数据
+   * @returns {string} 参数字符串
+   */
+  const buildBooleanParams = (form) => {
+    return "&emoji=" + form.emoji.toString() +
+      "&list=" + form.nodeList.toString() +
+      "&tfo=" + form.tfo.toString() +
+      "&scv=" + form.scv.toString() +
+      "&fdn=" + form.fdn.toString() +
+      "&expand=" + form.expand.toString() +
+      "&sort=" + form.sort.toString();
+  };
+
+  /**
+   * 构建模板特定参数
+   * @param {Object} form - 表单数据
+   * @returns {string} 参数字符串
+   */
+  const buildTemplateParams = (form) => {
+    let params = "";
+
+    if (form.tpl.surge.doh === true) {
+      params += "&surge.doh=true";
+    }
+
+    if (form.clientType === "clash") {
+      if (form.tpl.clash.doh === true) {
+        params += "&clash.doh=true";
+      }
+      params += "&new_name=" + form.new_name.toString();
+    }
+
+    return params;
+  };
+
+  /**
+   * 构建自定义参数
+   * @param {Array} customParams - 自定义参数数组
+   * @returns {string} 参数字符串
+   */
+  const buildCustomParams = (customParams) => {
+    return customParams
+      .filter(param => param.name && param.value)
+      .map(param => `&${encodeURIComponent(param.name)}=${encodeURIComponent(param.value)}`)
+      .join("");
+  };
+
+  /**
+   * 构建进阶模式参数
+   * @param {Object} form - 表单数据
+   * @param {Array} customParams - 自定义参数数组
+   * @param {boolean} needUdp - 是否需要UDP
+   * @returns {string} 参数字符串
+   */
+  const buildAdvancedParams = (form, customParams, needUdp) => {
+    let params = "";
+
+    // 远程配置
+    if (form.remoteConfig) {
+      params += "&config=" + encodeURIComponent(form.remoteConfig);
+    }
+
+    // 过滤参数
+    if (form.excludeRemarks) {
+      params += "&exclude=" + encodeURIComponent(form.excludeRemarks);
+    }
+    if (form.includeRemarks) {
+      params += "&include=" + encodeURIComponent(form.includeRemarks);
+    }
+
+    // 文件名
+    if (form.filename) {
+      params += "&filename=" + encodeURIComponent(form.filename);
+    }
+
+    // 节点类型
+    if (form.appendType) {
+      params += "&append_type=" + form.appendType.toString();
+    }
+
+    // 基础布尔参数
+    params += buildBooleanParams(form);
+
+    // UDP 参数
+    if (needUdp) {
+      params += "&udp=" + form.udp.toString();
+    }
+
+    // 模板特定参数
+    params += buildTemplateParams(form);
+
+    // 自定义参数
+    params += buildCustomParams(customParams);
+
+    return params;
+  };
+
+  /**
+   * 生成订阅链接
+   * @param {Object} form - 表单数据
+   * @param {string} advanced - 高级模式标识
+   * @param {string} processedSubUrl - 处理后的订阅链接
+   * @param {string} currentBackend - 当前后端地址
+   * @param {Array} customParams - 自定义参数数组
+   * @param {boolean} needUdp - 是否需要UDP
+   * @returns {string} 生成的订阅链接
+   */
+  const makeUrl = (form, advanced, processedSubUrl, currentBackend, customParams, needUdp) => {
+    // 验证必填项
+    if (!validateForm(form)) {
+      return "";
+    }
+
+    // 构建基础 URL
+    const baseUrl = buildBaseUrl(form, processedSubUrl, currentBackend);
+    let customSubUrl = baseUrl;
+
+    // 进阶模式添加额外参数
+    if (advanced === "2") {
+      customSubUrl += buildAdvancedParams(form, customParams, needUdp);
+    }
+
+    return customSubUrl;
+  };
+
+  return {
+    makeUrl,
+    buildBaseUrl,
+    buildAdvancedParams
+  };
+}

+ 76 - 0
src/composables/useSubscriptionForm.js

@@ -0,0 +1,76 @@
+import { setLocalStorageItem } from '@/utils/storage';
+
+/**
+ * 订阅表单状态管理 - 为Vue 2 Options API设计
+ */
+export function useSubscriptionForm() {
+  // 返回响应式数据和方法的集合
+  return {
+    // 表单数据
+    form: {
+      sourceSubUrl: "",
+      clientType: "",
+      customBackend: "",
+      remoteConfig: "",
+      excludeRemarks: "",
+      includeRemarks: "",
+      filename: "",
+      emoji: true,
+      nodeList: false,
+      extraset: false,
+      sort: false,
+      udp: false,
+      tfo: false,
+      scv: true,
+      fdn: false,
+      expand: true,
+      appendType: false,
+      insert: false, // 是否插入默认订阅的节点,对应配置项 insert_url
+      new_name: true, // 是否使用 Clash 新字段
+
+      // tpl 定制功能
+      tpl: {
+        surge: {
+          doh: false // dns 查询是否使用 DoH
+        },
+        clash: {
+          doh: false
+        }
+      }
+    },
+
+    // 自定义参数
+    customParams: [],
+
+    // 高级模式
+    advanced: "2",
+
+    // 是否需要UDP
+    needUdp: false,
+
+    // 生成的订阅链接
+    customSubUrl: ""
+  };
+}
+
+/**
+ * 添加自定义参数
+ * @param {Array} customParams - 自定义参数数组
+ */
+export function addCustomParam(customParams) {
+  customParams.push({
+    name: "",
+    value: "",
+  });
+}
+
+/**
+ * 保存订阅URL到本地存储
+ * @param {Object} form - 表单对象
+ */
+export function saveSubUrl(form) {
+  if (form && form.sourceSubUrl !== '') {
+    const ttl = process.env.VUE_APP_CACHE_TTL || 3600;
+    setLocalStorageItem('sourceSubUrl', form.sourceSubUrl, ttl);
+  }
+}

+ 115 - 0
src/composables/useUrlParser.js

@@ -0,0 +1,115 @@
+/**
+ * URL解析逻辑
+ */
+export function useUrlParser() {
+  /**
+   * 异步分析URL
+   * @param {string} loadConfig - 待分析的URL
+   * @returns {Promise<string>} 分析结果
+   */
+  const analyzeUrl = async (loadConfig) => {
+    // Check if `loadConfig` includes "target"
+    if (loadConfig.includes("target")) {
+      // If it does, return `loadConfig`
+      return loadConfig;
+    } else {
+      // Otherwise, fetch the data from `loadConfig` using GET method and follow redirects
+      try {
+        let response = await fetch(loadConfig, {
+          method: "GET",
+          redirect: "follow",
+        });
+        // Return the URL from the response
+        return response.url;
+      } catch (e) {
+        throw new Error("解析短链接失败,请检查短链接服务端是否配置跨域:" + e);
+      }
+    }
+  };
+
+  /**
+   * 确认并加载配置
+   * @param {string} loadConfig - 待解析的配置URL
+   * @param {Object} form - 表单对象
+   * @param {Array} customParams - 自定义参数数组
+   * @param {Function} onSuccess - 成功回调
+   * @param {Function} onError - 错误回调
+   * @returns {Promise<boolean>} 是否成功
+   */
+  const parseUrl = async (loadConfig, form, customParams, onSuccess, onError) => {
+    // Check if 'loadConfig' is empty
+    if (loadConfig.trim() === "") {
+      onError("订阅链接不能为空");
+      return false;
+    }
+
+    try {
+      // Analyze the URL and extract its components
+      const url = new URL(await analyzeUrl(loadConfig));
+
+      // Set the custom backend URL
+      form.customBackend = url.origin + url.pathname + "?";
+
+      // Parse the URL parameters
+      const params = new URLSearchParams(url.search);
+
+      // Record parameters have been read
+      const getParam = params.get.bind(params);
+      const excludeParams = new Set();
+      params.get = key => {
+        excludeParams.add(key);
+        return getParam(key);
+      };
+
+      // Get the 'target' parameter
+      const target = params.get("target");
+
+      // Set the client type based on the 'target' parameter
+      if (target === "surge") {
+        const ver = params.get("ver") || "4";
+        form.clientType = target + "&ver=" + ver;
+      } else {
+        form.clientType = target;
+      }
+
+      // Set other form properties based on the URL parameters
+      form.sourceSubUrl = params.get("url").replace(/\|/g, "\n");
+      form.insert = params.get("insert") === "true";
+      form.remoteConfig = params.get("config");
+      form.excludeRemarks = params.get("exclude");
+      form.includeRemarks = params.get("include");
+      form.filename = params.get("filename");
+      form.appendType = params.get("append_type") === "true";
+      form.emoji = params.get("emoji") === "true";
+      form.nodeList = params.get("list") === "true";
+      form.tfo = params.get("tfo") === "true";
+      form.scv = params.get("scv") === "true";
+      form.fdn = params.get("fdn") === "true";
+      form.sort = params.get("sort") === "true";
+      form.udp = params.get("udp") === "true";
+      form.expand = params.get("expand") === "true";
+      form.tpl.surge.doh = params.get("surge.doh") === "true";
+      form.tpl.clash.doh = params.get("clash.doh") === "true";
+      form.new_name = params.get("new_name") === "true";
+
+      // Filter custom parameters
+      customParams.splice(0, customParams.length);
+      Array.from(params
+        .entries()
+        .filter(e => !excludeParams.has(e[0]))
+        .map(e => ({ name: e[0], value: e[1] }))
+      ).forEach(param => customParams.push(param));
+
+      onSuccess();
+      return true;
+    } catch (error) {
+      onError("请输入正确的订阅地址!");
+      return false;
+    }
+  };
+
+  return {
+    analyzeUrl,
+    parseUrl
+  };
+}

+ 19 - 0
src/config/client-types.js

@@ -0,0 +1,19 @@
+// 客户端类型配置
+export const CLIENT_TYPES = {
+  Clash: "clash",
+  Surge: "surge&ver=4",
+  Quantumult: "quan",
+  QuantumultX: "quanx",
+  Mellow: "mellow",
+  Surfboard: "surfboard",
+  Loon: "loon",
+  singbox: "singbox",
+  ss: "ss",
+  ssd: "ssd",
+  sssub: "sssub",
+  ssr: "ssr",
+  ClashR: "clashr",
+  V2Ray: "v2ray",
+  Trojan: "trojan",
+  Surge3: "surge&ver=3",
+};

+ 14 - 0
src/config/constants.js

@@ -0,0 +1,14 @@
+// 项目常量定义
+export const CONSTANTS = {
+  PROJECT: process.env.VUE_APP_PROJECT,
+  REMOTE_CONFIG_SAMPLE: process.env.VUE_APP_SUBCONVERTER_REMOTE_CONFIG,
+  DOC_ADVANCED: process.env.VUE_APP_SUBCONVERTER_DOC_ADVANCED,
+  BACKEND_RELEASE: process.env.VUE_APP_BACKEND_RELEASE,
+  DEFAULT_BACKEND: process.env.VUE_APP_SUBCONVERTER_DEFAULT_BACKEND + '/sub?',
+  SHORT_URL_API: process.env.VUE_APP_MYURLS_API,
+  CONFIG_UPLOAD_API: process.env.VUE_APP_CONFIG_UPLOAD_API,
+  BOT_LINK: process.env.VUE_APP_BOT_LINK,
+  DEFAULT_CLIENT_TYPE: 'clash',
+  BUTTON_WIDTH: '140px',
+  LARGE_BUTTON_WIDTH: '290px'
+};

+ 35 - 0
src/config/remote-configs.js

@@ -0,0 +1,35 @@
+// 远程配置选项
+export const REMOTE_CONFIGS = [
+  {
+    label: "universal",
+    options: [
+      {
+        label: "No-Urltest",
+        value: "https://cdn.jsdelivr.net/gh/SleepyHeeead/subconverter-config@master/remote-config/universal/no-urltest.ini"
+      },
+      {
+        label: "Urltest",
+        value: "https://cdn.jsdelivr.net/gh/SleepyHeeead/subconverter-config@master/remote-config/universal/urltest.ini"
+      }
+    ]
+  },
+  {
+    label: "customized",
+    options: [
+      { label: "Maying", value: "https://cdn.jsdelivr.net/gh/SleepyHeeead/subconverter-config@master/remote-config/customized/maying.ini" },
+      { label: "Ytoo", value: "https://cdn.jsdelivr.net/gh/SleepyHeeead/subconverter-config@master/remote-config/customized/ytoo.ini" },
+      { label: "FlowerCloud", value: "https://cdn.jsdelivr.net/gh/SleepyHeeead/subconverter-config@master/remote-config/customized/flowercloud.ini" },
+      { label: "Nexitally", value: "https://cdn.jsdelivr.net/gh/SleepyHeeead/subconverter-config@master/remote-config/customized/nexitally.ini" },
+      { label: "SoCloud", value: "https://cdn.jsdelivr.net/gh/SleepyHeeead/subconverter-config@master/remote-config/customized/socloud.ini" },
+      { label: "ARK", value: "https://cdn.jsdelivr.net/gh/SleepyHeeead/subconverter-config@master/remote-config/customized/ark.ini" },
+      { label: "ssrCloud", value: "https://cdn.jsdelivr.net/gh/SleepyHeeead/subconverter-config@master/remote-config/customized/ssrcloud.ini" }
+    ]
+  },
+  {
+    label: "Special",
+    options: [
+      { label: "NeteaseUnblock(仅规则,No-Urltest)", value: "https://cdn.jsdelivr.net/gh/SleepyHeeead/subconverter-config@master/remote-config/special/netease.ini" },
+      { label: "Basic(仅GEOIP CN + Final)", value: "https://cdn.jsdelivr.net/gh/SleepyHeeead/subconverter-config@master/remote-config/special/basic.ini" }
+    ]
+  }
+];

+ 0 - 1
src/main.js

@@ -9,7 +9,6 @@ require(`@/plugins/axios`)
 require(`@/plugins/device`)
 
 import '@/icons' // icon
-import './registerServiceWorker'
 
 Vue.config.productionTip = false
 

+ 0 - 32
src/registerServiceWorker.js

@@ -1,32 +0,0 @@
-/* eslint-disable no-console */
-
-import { register } from 'register-service-worker'
-
-if (process.env.NODE_ENV === 'production') {
-  register(`${process.env.BASE_URL}sub-web.js`, {
-    ready () {
-      console.log(
-        'App is being served from cache by a service worker.\n' +
-        'For more details, visit https://goo.gl/AFskqB'
-      )
-    },
-    registered () {
-      console.log('Service worker has been registered.')
-    },
-    cached () {
-      console.log('Content has been cached for offline use.')
-    },
-    updatefound () {
-      console.log('New content is downloading.')
-    },
-    updated () {
-      console.log('New content is available; please refresh.')
-    },
-    offline () {
-      console.log('No internet connection found. App is running in offline mode.')
-    },
-    error (error) {
-      console.error('Error during service worker registration:', error)
-    }
-  })
-}

+ 27 - 0
src/services/backendService.js

@@ -0,0 +1,27 @@
+import { CONSTANTS } from '@/config/constants';
+import { formatVersion } from '@/utils/formatters';
+
+/**
+ * 后端版本检查服务
+ */
+export class BackendService {
+  /**
+   * 获取后端版本信息
+   * @param {Object} $axios - Axios实例
+   * @returns {Promise<string>} 版本信息
+   */
+  static async getBackendVersion($axios) {
+    // 提取版本 API 路径
+    const versionApiUrl = CONSTANTS.DEFAULT_BACKEND.substring(0, CONSTANTS.DEFAULT_BACKEND.length - 5) + "/version";
+
+    try {
+      const response = await $axios.get(versionApiUrl);
+      // 清理版本信息格式
+      let version = formatVersion(response.data);
+      return version;
+    } catch (error) {
+      // 静默处理,不显示错误信息,避免干扰用户体验
+      return "";
+    }
+  }
+}

+ 54 - 0
src/services/configUploadService.js

@@ -0,0 +1,54 @@
+import { CONSTANTS } from '@/config/constants';
+
+/**
+ * 配置上传服务
+ */
+export class ConfigUploadService {
+  /**
+   * 上传配置
+   * @param {Object} $axios - Axios实例
+   * @param {string} content - 配置内容
+   * @returns {Promise<Object>} 上传结果
+   */
+  static async uploadConfig($axios, content) {
+    const body = {
+      content: content,
+    };
+
+    const response = await $axios.post(CONSTANTS.CONFIG_UPLOAD_API, body);
+    return response.data;
+  }
+
+  /**
+   * 处理上传成功响应
+   * @param {Object} res - 响应对象 (已经是response.data)
+   * @param {Function} $copyText - 复制文本函数
+   * @param {Function} $message - 消息提示函数
+   * @returns {Object} 处理结果
+   */
+  static handleUploadSuccess(res, $copyText, $message) {
+    // res 已经是 response.data,所以直接访问 res.code, res.msg, res.data.url
+    if (res.code === 0 && res.data && res.data.url) {
+      const configUrl = res.data.url;
+      $copyText(configUrl);
+      $message.success(
+        "远程配置上传成功,配置链接已复制到剪贴板,有效期三个月望知悉"
+      );
+      return {
+        success: true,
+        url: configUrl
+      };
+    } else {
+      const errorMsg = res.msg || "远程配置上传失败";
+      throw new Error(errorMsg);
+    }
+  }
+
+  /**
+   * 处理上传错误
+   * @param {Function} $message - 消息提示函数
+   */
+  static handleUploadError($message) {
+    $message.error("远程配置上传失败");
+  }
+}

+ 56 - 0
src/services/shortUrlService.js

@@ -0,0 +1,56 @@
+import { CONSTANTS } from '@/config/constants';
+
+/**
+ * 短链接生成服务
+ */
+export class ShortUrlService {
+  /**
+   * 生成短链接
+   * @param {Object} $axios - Axios实例
+   * @param {string} longUrl - 长链接
+   * @returns {Promise<string>} 短链接
+   */
+  static async generateShortUrl($axios, longUrl) {
+    // 构建请求数据
+    const formData = new FormData();
+    formData.append("longUrl", btoa(longUrl));
+
+    const response = await $axios.post(CONSTANTS.SHORT_URL_API, formData, {
+      headers: {
+        "Content-Type": "application/form-data; charset=utf-8"
+      }
+    });
+
+    if (response.data.Code === 1 && response.data.ShortUrl !== "") {
+      return response.data.ShortUrl;
+    } else {
+      throw new Error(response.data.Message || "短链接获取失败");
+    }
+  }
+
+  /**
+   * 处理短链接成功响应
+   * @param {Object} res - 响应对象
+   * @param {Function} $copyText - 复制文本函数
+   * @param {Function} $message - 消息提示函数
+   * @returns {string} 短链接
+   */
+  static handleShortUrlSuccess(res, $copyText, $message) {
+    if (res.data.Code === 1 && res.data.ShortUrl !== "") {
+      const shortUrl = res.data.ShortUrl;
+      $copyText(shortUrl);
+      $message.success("短链接已复制到剪贴板");
+      return shortUrl;
+    } else {
+      throw new Error(res.data.Message);
+    }
+  }
+
+  /**
+   * 处理短链接错误
+   * @param {Function} $message - 消息提示函数
+   */
+  static handleShortUrlError($message) {
+    $message.error("短链接获取失败");
+  }
+}

+ 37 - 0
src/utils/formatters.js

@@ -0,0 +1,37 @@
+/**
+ * 格式化错误消息
+ * @param {Error|string} error - 错误对象或字符串
+ * @returns {string} 格式化后的错误消息
+ */
+export const formatErrorMessage = (error) => {
+  if (typeof error === 'string') {
+    return error;
+  }
+  if (error.response && error.response.data && error.response.data.message) {
+    return error.response.data.message;
+  }
+  if (error.message) {
+    return error.message;
+  }
+  return "操作失败,请重试";
+};
+
+/**
+ * 清理版本信息格式
+ * @param {string} version - 原始版本信息
+ * @returns {string} 清理后的版本信息
+ */
+export const formatVersion = (version) => {
+  let cleaned = version.replace(/backend\n$/gm, "");
+  cleaned = cleaned.replace("subconverter", "");
+  return cleaned;
+};
+
+/**
+ * 处理订阅链接(去除换行符)
+ * @param {string} url - 原始订阅链接
+ * @returns {string} 处理后的订阅链接
+ */
+export const processSubUrl = (url) => {
+  return url.replace(/(\n|\r|\n\r)/g, "|");
+};

+ 26 - 0
src/utils/search.js

@@ -0,0 +1,26 @@
+/**
+ * 创建过滤器函数
+ * @param {string} queryString - 查询字符串
+ * @returns {Function} 过滤器函数
+ */
+export const createFilter = (queryString) => {
+  return (candidate) => {
+    return (
+      candidate.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0
+    );
+  };
+};
+
+/**
+ * 后端搜索建议
+ * @param {string} queryString - 查询字符串
+ * @param {Array} backends - 后端列表
+ * @returns {Array} 搜索结果
+ */
+export const backendSearch = (queryString, backends) => {
+  let results = queryString
+    ? backends.filter(createFilter(queryString))
+    : backends;
+
+  return results;
+};

+ 39 - 0
src/utils/storage.js

@@ -0,0 +1,39 @@
+/**
+ * 获取本地存储项
+ * @param {string} itemKey - 存储键
+ * @returns {string} 存储值
+ */
+export const getLocalStorageItem = (itemKey) => {
+  const now = +new Date();
+  let ls = localStorage.getItem(itemKey);
+
+  let itemValue = '';
+  if (ls !== null) {
+    let data = JSON.parse(ls);
+    if (data.expire > now) {
+      itemValue = data.value;
+    } else {
+      localStorage.removeItem(itemKey);
+    }
+  }
+
+  return itemValue;
+};
+
+/**
+ * 设置本地存储项
+ * @param {string} itemKey - 存储键
+ * @param {string} itemValue - 存储值
+ * @param {number} ttl - 生存时间(秒)
+ */
+export const setLocalStorageItem = (itemKey, itemValue, ttl) => {
+  const now = +new Date();
+
+  let data = {
+    setTime: now,
+    ttl: parseInt(ttl),
+    expire: now + ttl * 1000,
+    value: itemValue
+  };
+  localStorage.setItem(itemKey, JSON.stringify(data));
+};

+ 33 - 0
src/utils/validators.js

@@ -0,0 +1,33 @@
+/**
+ * 验证订阅链接格式
+ * @param {string} url - 订阅链接
+ * @returns {Object} 验证结果
+ */
+export const validateSubUrl = (url) => {
+  if (!url || url.trim() === "") {
+    return { valid: false, message: "订阅链接不能为空" };
+  }
+
+  // 检查是否包含有效的协议或节点信息
+  const hasValidFormat = /^(ss|ssr|vmess|trojan|hysteria|tuic|sip008|vless):\/\//.test(url) ||
+                         /^https?:\/\//.test(url) ||
+                         /^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z0-9]+$/.test(url);
+
+  if (!hasValidFormat) {
+    return { valid: false, message: "订阅链接格式可能不正确" };
+  }
+
+  return { valid: true };
+};
+
+/**
+ * 验证表单必填项
+ * @param {Object} form - 表单数据
+ * @returns {boolean} 验证结果
+ */
+export const validateForm = (form) => {
+  if (form.sourceSubUrl === "" || form.clientType === "") {
+    return false;
+  }
+  return true;
+};

+ 232 - 500
src/views/Subconverter.vue

@@ -130,23 +130,53 @@
                 </el-input>
               </el-form-item>
 
+              <!-- 操作按钮组 -->
               <el-form-item label-width="0px" style="margin-top: 40px; text-align: center">
-                <el-button style="width: 140px" type="danger" @click="makeUrl"
-                  :disabled="form.sourceSubUrl.length === 0">生成订阅链接</el-button>
-                <el-button style="width: 140px" type="danger" @click="makeShortUrl" :loading="loading"
-                  :disabled="customSubUrl.length === 0">生成短链接</el-button>
-                <!-- <el-button style="width: 140px" type="primary" @click="surgeInstall" icon="el-icon-connection">一键导入Surge</el-button> -->
+                <el-button
+                  :style="buttonStyle"
+                  type="danger"
+                  @click="makeUrlClick"
+                  :disabled="!canGenerateUrl">
+                  生成订阅链接
+                </el-button>
+                <el-button
+                  :style="buttonStyle"
+                  type="danger"
+                  @click="makeShortUrlClick"
+                  :loading="loading"
+                  :disabled="!canGenerateShortUrl">
+                  生成短链接
+                </el-button>
               </el-form-item>
 
               <el-form-item label-width="0px" style="text-align: center">
-                <el-button style="width: 140px" type="primary" @click="dialogUploadConfigVisible = true"
-                  icon="el-icon-upload" :loading="loading">上传配置</el-button>
-                <el-button style="width: 140px" type="primary" @click="clashInstall" icon="el-icon-connection"
-                  :disabled="customSubUrl.length === 0">一键导入 Clash</el-button>
+                <el-button
+                  :style="buttonStyle"
+                  type="primary"
+                  @click="dialogUploadConfigVisible = true"
+                  icon="el-icon-upload"
+                  :loading="loading">
+                  上传配置
+                </el-button>
+                <el-button
+                  :style="buttonStyle"
+                  type="primary"
+                  @click="clashInstall"
+                  icon="el-icon-connection"
+                  :disabled="!canImportClash">
+                  一键导入 Clash
+                </el-button>
               </el-form-item>
+
               <el-form-item label-width="0px" style="text-align: center">
-                <el-button style="width: 290px" type="primary" @click="dialogLoadConfigVisible = true"
-                  icon="el-icon-copy-document" :loading="loading">从 URL 解析</el-button>
+                <el-button
+                  :style="{ width: '290px' }"
+                  type="primary"
+                  @click="dialogLoadConfigVisible = true"
+                  icon="el-icon-copy-document"
+                  :loading="loading">
+                  从 URL 解析
+                </el-button>
               </el-form-item>
             </el-form>
           </el-container>
@@ -154,219 +184,123 @@
       </el-col>
     </el-row>
 
-    <el-dialog :visible.sync="dialogUploadConfigVisible" :show-close="false" :close-on-click-modal="false"
-      :close-on-press-escape="false" width="700px">
-      <div slot="title">
-        Remote config upload
-        <el-popover trigger="hover" placement="right" style="margin-left: 10px">
-          <el-link type="primary" :href="sampleConfig" target="_blank" icon="el-icon-info">参考配置</el-link>
-          <i class="el-icon-question" slot="reference"></i>
-        </el-popover>
-      </div>
-      <el-form label-position="left">
-        <el-form-item prop="uploadConfig">
-          <el-input v-model="uploadConfig" type="textarea" :autosize="{ minRows: 15, maxRows: 30 }" maxlength="10000"
-            show-word-limit></el-input>
-        </el-form-item>
-      </el-form>
-      <div slot="footer" class="dialog-footer">
-        <el-button @click="uploadConfig = ''; dialogUploadConfigVisible = false">取 消</el-button>
-        <el-button type="primary" @click="confirmUploadConfig" :disabled="uploadConfig.length === 0">确 定</el-button>
-      </div>
-    </el-dialog>
-
-    <el-dialog :visible.sync="dialogLoadConfigVisible" :show-close="false" :close-on-click-modal="false"
-      :close-on-press-escape="false" width="700px">
-      <div slot="title">
-        解析 Subconverter 链接
-      </div>
-      <el-form label-position="left" :inline="true" >
-        <el-form-item prop="uploadConfig" label="订阅链接:" label-width="85px">
-          <el-input v-model="loadConfig" style="width: 565px"></el-input>
-        </el-form-item>
-      </el-form>
-      <div slot="footer" class="dialog-footer">
-        <el-button @click="loadConfig = ''; dialogLoadConfigVisible = false">取 消</el-button>
-        <el-button type="primary" @click="confirmLoadConfig" :disabled="loadConfig.length === 0">确 定</el-button>
-      </div>
-    </el-dialog>
-
+    <!-- 配置上传对话框 -->
+    <ConfigUploadDialog
+      :visible="dialogUploadConfigVisible"
+      :upload-config="uploadConfig"
+      :loading="loading"
+      @cancel="handleUploadCancel"
+      @confirm="handleConfigUpload"
+    />
+
+    <!-- URL解析对话框 -->
+    <UrlParseDialog
+      :visible="dialogLoadConfigVisible"
+      :load-config="loadConfig"
+      :loading="loading"
+      @cancel="handleLoadCancel"
+      @confirm="handleUrlParse"
+    />
   </div>
 </template>
 
 <script>
-const project = process.env.VUE_APP_PROJECT
-const remoteConfigSample = process.env.VUE_APP_SUBCONVERTER_REMOTE_CONFIG
-const subDocAdvanced = process.env.VUE_APP_SUBCONVERTER_DOC_ADVANCED
-const gayhubRelease = process.env.VUE_APP_BACKEND_RELEASE
-const defaultBackend = process.env.VUE_APP_SUBCONVERTER_DEFAULT_BACKEND + '/sub?'
-const shortUrlBackend = process.env.VUE_APP_MYURLS_API
-const configUploadBackend = process.env.VUE_APP_CONFIG_UPLOAD_API
-const tgBotLink = process.env.VUE_APP_BOT_LINK
+// 导入配置
+import { CONSTANTS } from '@/config/constants';
+import { CLIENT_TYPES } from '@/config/client-types';
+import { REMOTE_CONFIGS } from '@/config/remote-configs';
+
+// 导入Composables
+import { useSubscriptionForm, addCustomParam, saveSubUrl as saveSubscriptionUrl } from '@/composables/useSubscriptionForm';
+import { useSubscription } from '@/composables/useSubscription';
+import { useUrlParser } from '@/composables/useUrlParser';
+
+// 导入工具函数
+import { getLocalStorageItem } from '@/utils/storage';
+
+// 导入服务
+import { BackendService } from '@/services/backendService';
+import { ShortUrlService } from '@/services/shortUrlService';
+import { ConfigUploadService } from '@/services/configUploadService';
+
+// 导入组件
+import ConfigUploadDialog from '@/components/ConfigUploadDialog.vue';
+import UrlParseDialog from '@/components/UrlParseDialog.vue';
 
 export default {
+  name: 'Subconverter',
+  components: {
+    ConfigUploadDialog,
+    UrlParseDialog
+  },
   data() {
-    return {
-      backendVersion: "",
-      advanced: "2",
-
-      // 是否为 PC 端
-      isPC: true,
+    const subscriptionForm = useSubscriptionForm();
 
+    return {
+      // 配置选项
       options: {
-        clientTypes: {
-          Clash: "clash",
-          Surge: "surge&ver=4",
-          Quantumult: "quan",
-          QuantumultX: "quanx",
-          Mellow: "mellow",
-          Surfboard: "surfboard",
-          Loon: "loon",
-          singbox: "singbox",
-          ss: "ss",
-          ssd: "ssd",
-          sssub: "sssub",
-          ssr: "ssr",
-          ClashR: "clashr",          
-          V2Ray: "v2ray",
-          Trojan: "trojan",
-          Surge3: "surge&ver=3",
-        },
+        clientTypes: CLIENT_TYPES,
         backendOptions: [{ value: "http://127.0.0.1:25500/sub?" }],
-        remoteConfig: [
-          {
-            label: "universal",
-            options: [
-              {
-                label: "No-Urltest",
-                value:
-                  "https://cdn.jsdelivr.net/gh/SleepyHeeead/subconverter-config@master/remote-config/universal/no-urltest.ini"
-              },
-              {
-                label: "Urltest",
-                value:
-                  "https://cdn.jsdelivr.net/gh/SleepyHeeead/subconverter-config@master/remote-config/universal/urltest.ini"
-              }
-            ]
-          },
-          {
-            label: "customized",
-            options: [
-              {
-                label: "Maying",
-                value:
-                  "https://cdn.jsdelivr.net/gh/SleepyHeeead/subconverter-config@master/remote-config/customized/maying.ini"
-              },
-              {
-                label: "Ytoo",
-                value:
-                  "https://cdn.jsdelivr.net/gh/SleepyHeeead/subconverter-config@master/remote-config/customized/ytoo.ini"
-              },
-              {
-                label: "FlowerCloud",
-                value:
-                  "https://cdn.jsdelivr.net/gh/SleepyHeeead/subconverter-config@master/remote-config/customized/flowercloud.ini"
-              },
-              {
-                label: "Nexitally",
-                value:
-                  "https://cdn.jsdelivr.net/gh/SleepyHeeead/subconverter-config@master/remote-config/customized/nexitally.ini"
-              },
-              {
-                label: "SoCloud",
-                value:
-                  "https://cdn.jsdelivr.net/gh/SleepyHeeead/subconverter-config@master/remote-config/customized/socloud.ini"
-              },
-              {
-                label: "ARK",
-                value:
-                  "https://cdn.jsdelivr.net/gh/SleepyHeeead/subconverter-config@master/remote-config/customized/ark.ini"
-              },
-              {
-                label: "ssrCloud",
-                value:
-                  "https://cdn.jsdelivr.net/gh/SleepyHeeead/subconverter-config@master/remote-config/customized/ssrcloud.ini"
-              }
-            ]
-          },
-          {
-            label: "Special",
-            options: [
-              {
-                label: "NeteaseUnblock(仅规则,No-Urltest)",
-                value:
-                  "https://cdn.jsdelivr.net/gh/SleepyHeeead/subconverter-config@master/remote-config/special/netease.ini"
-              },
-              {
-                label: "Basic(仅GEOIP CN + Final)",
-                value:
-                  "https://cdn.jsdelivr.net/gh/SleepyHeeead/subconverter-config@master/remote-config/special/basic.ini"
-              }
-            ]
-          }
-        ]
-      },
-      form: {
-        sourceSubUrl: "",
-        clientType: "",
-        customBackend: "",
-        remoteConfig: "",
-        excludeRemarks: "",
-        includeRemarks: "",
-        filename: "",
-        emoji: true,
-        nodeList: false,
-        extraset: false,
-        sort: false,
-        udp: false,
-        tfo: false,
-        scv: true,
-        fdn: false,
-        expand: true,
-        appendType: false,
-        insert: false, // 是否插入默认订阅的节点,对应配置项 insert_url
-        new_name: true, // 是否使用 Clash 新字段
-
-        // tpl 定制功能
-        tpl: {
-          surge: {
-            doh: false // dns 查询是否使用 DoH
-          },
-          clash: {
-            doh: false
-          }
-        }
+        remoteConfig: REMOTE_CONFIGS
       },
 
-      customParams: [],
-
+      // 状态
+      backendVersion: "",
       loading: false,
-      customSubUrl: "",
       curtomShortSubUrl: "",
-
       dialogUploadConfigVisible: false,
       loadConfig: "",
       dialogLoadConfigVisible: false,
       uploadConfig: "",
-      uploadPassword: "",
-      myBot: tgBotLink,
-      sampleConfig: remoteConfigSample,
-      subDocAdvanced: subDocAdvanced,
+      subDocAdvanced: CONSTANTS.DOC_ADVANCED,
 
-      needUdp: false, // 是否需要添加 udp 参数
+      // 是否为 PC 端
+      isPC: true,
+
+      // 合并表单状态
+      ...subscriptionForm
     };
   },
+  computed: {
+    // 按钮统一样式
+    buttonStyle() {
+      return { width: '140px' };
+    },
+
+    canGenerateShortUrl() {
+      return this.customSubUrl.length > 0 && !this.loading;
+    },
+
+    canGenerateUrl() {
+      return this.form.sourceSubUrl.length > 0 && this.form.clientType;
+    },
+
+    canImportClash() {
+      return this.customSubUrl.length > 0;
+    },
+
+    processedSubUrl() {
+      return this.form.sourceSubUrl.replace(/(\n|\r|\n\r)/g, "|");
+    },
+
+    currentBackend() {
+      return this.form.customBackend || CONSTANTS.DEFAULT_BACKEND;
+    }
+  },
   created() {
     document.title = "Subscription Converter";
     this.isPC = this.$getOS().isPc;
 
     // 获取 url cache
     if (process.env.VUE_APP_USE_STORAGE === 'true') {
-      this.form.sourceSubUrl = this.getLocalStorageItem('sourceSubUrl')
+      const cachedUrl = getLocalStorageItem('sourceSubUrl');
+      if (cachedUrl) {
+        this.form.sourceSubUrl = cachedUrl;
+      }
     }
   },
   mounted() {
-    this.form.clientType = "clash";
+    this.form.clientType = CONSTANTS.DEFAULT_CLIENT_TYPE;
     this.notify();
     this.getBackendVersion();
   },
@@ -374,15 +308,19 @@ export default {
     onCopy() {
       this.$message.success("Copied!");
     },
+
     goToProject() {
-      window.open(project);
+      window.open(CONSTANTS.PROJECT);
     },
+
     gotoGayhub() {
-      window.open(gayhubRelease);
+      window.open(CONSTANTS.BACKEND_RELEASE);
     },
+
     gotoRemoteConfig() {
-      window.open(remoteConfigSample);
+      window.open(CONSTANTS.REMOTE_CONFIG_SAMPLE);
     },
+
     clashInstall() {
       if (this.customSubUrl === "") {
         this.$message.error("请先填写必填项,生成订阅链接");
@@ -399,107 +337,19 @@ export default {
         )
       );
     },
-    surgeInstall() {
-      if (this.customSubUrl === "") {
-        this.$message.error("请先填写必填项,生成订阅链接");
-        return false;
-      }
 
-      const url = "surge://install-config?url=";
-      window.open(url + this.customSubUrl);
-    },
-    addCustomParam(){
-      this.customParams.push({
-        name: "",
-        value: "",
-      })
-    },
-    makeUrl() {
-      if (this.form.sourceSubUrl === "" || this.form.clientType === "") {
+    makeUrlClick() {
+      const url = this.makeUrl(this.form, this.advanced, this.processedSubUrl, this.currentBackend, this.customParams, this.needUdp);
+      if (url) {
+        this.customSubUrl = url;
+        this.$copyText(this.customSubUrl);
+        this.$message.success("定制订阅已复制到剪贴板");
+      } else {
         this.$message.error("订阅链接与客户端为必填项");
-        return false;
       }
-
-      let backend =
-        this.form.customBackend === ""
-          ? defaultBackend
-          : this.form.customBackend;
-
-      let sourceSub = this.form.sourceSubUrl;
-      sourceSub = sourceSub.replace(/(\n|\r|\n\r)/g, "|");
-
-      this.customSubUrl =
-        backend +
-        "target=" +
-        this.form.clientType +
-        "&url=" +
-        encodeURIComponent(sourceSub) +
-        "&insert=" +
-        this.form.insert;
-
-      if (this.advanced === "2") {
-        if (this.form.remoteConfig) {
-          this.customSubUrl +=
-            "&config=" + encodeURIComponent(this.form.remoteConfig);
-        }
-        if (this.form.excludeRemarks) {
-          this.customSubUrl +=
-            "&exclude=" + encodeURIComponent(this.form.excludeRemarks);
-        }
-        if (this.form.includeRemarks) {
-          this.customSubUrl +=
-            "&include=" + encodeURIComponent(this.form.includeRemarks);
-        }
-        if (this.form.filename) {
-          this.customSubUrl +=
-            "&filename=" + encodeURIComponent(this.form.filename);
-        }
-        if (this.form.appendType) {
-          this.customSubUrl +=
-            "&append_type=" + this.form.appendType.toString();
-        }
-
-        this.customSubUrl +=
-          "&emoji=" +
-          this.form.emoji.toString() +
-          "&list=" +
-          this.form.nodeList.toString() +
-          "&tfo=" +
-          this.form.tfo.toString() +
-          "&scv=" +
-          this.form.scv.toString() +
-          "&fdn=" +
-          this.form.fdn.toString() +
-          "&expand=" +
-          this.form.expand.toString() +
-          "&sort=" +
-          this.form.sort.toString();
-
-        if (this.needUdp) {
-          this.customSubUrl += "&udp=" + this.form.udp.toString()
-        }
-
-        if (this.form.tpl.surge.doh === true) {
-          this.customSubUrl += "&surge.doh=true";
-        }
-
-        if (this.form.clientType === "clash") {
-          if (this.form.tpl.clash.doh === true) {
-            this.customSubUrl += "&clash.doh=true";
-          }
-
-          this.customSubUrl += "&new_name=" + this.form.new_name.toString();
-        }
-
-        this.customParams.filter(param => param.name && param.value).forEach(param => {
-          this.customSubUrl += `&${encodeURIComponent(param.name)}=${encodeURIComponent(param.value)}`
-        })
-      }
-
-      this.$copyText(this.customSubUrl);
-      this.$message.success("定制订阅已复制到剪贴板");
     },
-    makeShortUrl() {
+
+    makeShortUrlClick() {
       if (this.customSubUrl === "") {
         this.$message.warning("请先生成订阅链接,再获取对应短链接");
         return false;
@@ -507,44 +357,20 @@ export default {
 
       this.loading = true;
 
-      let data = new FormData();
-      data.append("longUrl", btoa(this.customSubUrl));
-
-      this.$axios
-        .post(shortUrlBackend, data, {
-          header: {
-            "Content-Type": "application/form-data; charset=utf-8"
-          }
+      ShortUrlService.generateShortUrl(this.$axios, this.customSubUrl)
+        .then(shortUrl => {
+          this.curtomShortSubUrl = shortUrl;
+          this.$copyText(shortUrl);
+          this.$message.success("短链接已复制到剪贴板");
         })
-        .then(res => {
-          if (res.data.Code === 1 && res.data.ShortUrl !== "") {
-            this.curtomShortSubUrl = res.data.ShortUrl;
-            this.$copyText(res.data.ShortUrl);
-            this.$message.success("短链接已复制到剪贴板");
-          } else {
-            this.$message.error("短链接获取失败:" + res.data.Message);
-          }
-        })
-        .catch(() => {
-          this.$message.error("短链接获取失败");
+        .catch(error => {
+          this.$message.error("短链接获取失败:" + error.message);
         })
         .finally(() => {
           this.loading = false;
         });
     },
-    notify() {
-      const h = this.$createElement;
 
-      this.$notify({
-        title: "隐私提示",
-        type: "warning",
-        message: h(
-          "i",
-          { style: "color: teal" },
-          "各种订阅链接(短链接服务除外)生成纯前端实现,无隐私问题。默认提供后端转换服务,隐私担忧者请自行搭建后端服务。"
-        )
-      });
-    },
     confirmUploadConfig() {
       if (this.uploadConfig === "") {
         this.$message.warning("远程配置不能为空");
@@ -553,205 +379,111 @@ export default {
 
       this.loading = true;
 
-      let body = {
-        content: this.uploadConfig,
-      }
-      this.$axios.post(configUploadBackend, body).then(res => {
-        if (res.data.code === 0 && res.data.data.url !== "") {
-          this.$message.success(
-            "远程配置上传成功,配置链接已复制到剪贴板,有效期三个月望知悉"
-          );
-
-          // 自动填充至『表单-远程配置』
-          this.form.remoteConfig = res.data.data.url;
-          this.$copyText(this.form.remoteConfig);
-
-          this.dialogUploadConfigVisible = false;
-        } else {
-          this.$message.error("远程配置上传失败: " + res.data.msg);
-        }
-      })
-        .catch(() => {
-          this.$message.error("远程配置上传失败");
+      ConfigUploadService.uploadConfig(this.$axios, this.uploadConfig)
+        .then(res => {
+          const result = ConfigUploadService.handleUploadSuccess(res, this.$copyText, this.$message);
+          if (result.success) {
+            // 自动填充至『表单-远程配置』
+            this.form.remoteConfig = result.url;
+            this.$copyText(this.form.remoteConfig);
+            this.dialogUploadConfigVisible = false;
+            this.uploadConfig = "";
+          }
+        })
+        .catch(error => {
+          this.$message.error("远程配置上传失败: " + error.message);
         })
         .finally(() => {
           this.loading = false;
         });
     },
-    /**
- * Asynchronously analyzes the URL.
- *
- * @return {Promise<string>} The result of the analysis.
- */
-    async analyzeUrl() {
-      // Check if `loadConfig` includes "target"
-      if (this.loadConfig.includes("target")) {
-        // If it does, return `loadConfig`
-        return this.loadConfig;
-      } else {
-        // Otherwise, set `loading` to true
-        this.loading = true;
-        try {
-          // Fetch the data from `loadConfig` using GET method and follow redirects
-          let response = await fetch(this.loadConfig, {
-            method: "GET",
-            redirect: "follow",
-          });
-          // Return the URL from the response
-          return response.url;
-        } catch (e) {
-          // If an error occurs, display an error message with the error details
-          this.$message.error(
-            "解析短链接失败,请检查短链接服务端是否配置跨域:" + e
-          );
-        } finally {
-          // Set `loading` to false
-          this.loading = false;
-        }
-      }
-    },
-    /**
-     * Confirm and load the configuration.
-     *
-     * @return {boolean} Returns false if the 'loadConfig' is empty, otherwise returns true.
-     */
-    confirmLoadConfig() {
-      // Check if 'loadConfig' is empty
-      if (this.loadConfig.trim() === "") {
-        // Display error message if 'loadConfig' is empty
-        this.$message.error("订阅链接不能为空");
-        return false;
-      }
-
-      // Async function to handle the configuration loading
-      (async () => {
-        try {
-          // Analyze the URL and extract its components
-          const url = new URL(await this.analyzeUrl());
 
-          // Set the custom backend URL
-          this.form.customBackend = url.origin + url.pathname + "?";
+    handleUploadCancel() {
+      this.uploadConfig = "";
+      this.dialogUploadConfigVisible = false;
+    },
 
-          // Parse the URL parameters
-          const params = new URLSearchParams(url.search);
+    handleConfigUpload(configContent) {
+      this.uploadConfig = configContent;
+      this.confirmUploadConfig();
+    },
 
-          // Record parameters have been read
-          const getParam = params.get.bind(params)
-          const excludeParams = new Set()
-          params.get = key => {
-            excludeParams.add(key)
-            return getParam(key)
-          }
+    handleLoadCancel() {
+      this.loadConfig = "";
+      this.dialogLoadConfigVisible = false;
+    },
 
-          // Get the 'target' parameter
-          const target = params.get("target");
+    handleUrlParse(url) {
+      this.loadConfig = url;
+      this.confirmLoadConfig();
+    },
 
-          // Set the client type based on the 'target' parameter
-          if (target === "surge") {
-            const ver = params.get("ver") || "4";
-            this.form.clientType = target + "&ver=" + ver;
-          } else {
-            this.form.clientType = target;
-          }
+    confirmLoadConfig() {
+      this.loading = true;
 
-          // Set other form properties based on the URL parameters
-          this.form.sourceSubUrl = params.get("url").replace(/\|/g, "\n");
-          this.form.insert = params.get("insert") === "true";
-          this.form.remoteConfig = params.get("config");
-          this.form.excludeRemarks = params.get("exclude");
-          this.form.includeRemarks = params.get("include");
-          this.form.filename = params.get("filename");
-          this.form.appendType = params.get("append_type") === "true";
-          this.form.emoji = params.get("emoji") === "true";
-          this.form.nodeList = params.get("list") === "true";
-          this.form.tfo = params.get("tfo") === "true";
-          this.form.scv = params.get("scv") === "true";
-          this.form.fdn = params.get("fdn") === "true";
-          this.form.sort = params.get("sort") === "true";
-          this.form.udp = params.get("udp") === "true";
-          this.form.expand = params.get("expand") === "true";
-          this.form.tpl.surge.doh = params.get("surge.doh") === "true";
-          this.form.tpl.clash.doh = params.get("clash.doh") === "true";
-          this.form.new_name = params.get("new_name") === "true";
-
-          // Filter custom parameters
-          this.customParams = Array.from(params
-            .entries()
-            .filter(e => !excludeParams.has(e[0]))
-            .map(e => ({ name: e[0], value: e[1] }))
-          )
-
-          // Hide the configuration dialog
+      this.parseUrl(
+        this.loadConfig,
+        this.form,
+        this.customParams,
+        () => {
           this.dialogLoadConfigVisible = false;
-
-          // Display success message
+          this.loadConfig = "";
           this.$message.success("长/短链接已成功解析为订阅信息");
-        } catch (error) {
-          // Display error message if URL is not valid
-          this.$message.error("请输入正确的订阅地址!");
+        },
+        (error) => {
+          this.$message.error(error);
         }
-      })();
+      ).then(() => {
+        this.loading = false;
+      }).catch(() => {
+        this.loading = false;
+      });
     },
-    backendSearch(queryString, cb) {
-      let backends = this.options.backendOptions;
-
-      let results = queryString
-        ? backends.filter(this.createFilter(queryString))
-        : backends;
 
-      // 调用 callback 返回建议列表的数据
+    backendSearch(queryString, cb) {
+      const results = this.backendSearchSuggestions(queryString, this.options.backendOptions);
       cb(results);
     },
-    createFilter(queryString) {
-      return candidate => {
-        return (
-          candidate.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0
-        );
-      };
+
+    backendSearchSuggestions(queryString, backends) {
+      if (queryString) {
+        return backends.filter(backend => {
+          return backend.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0;
+        });
+      }
+      return backends;
+    },
+
+    async getBackendVersion() {
+      this.backendVersion = await BackendService.getBackendVersion(this.$axios);
     },
-    getBackendVersion() {
-      this.$axios
-        .get(
-          defaultBackend.substring(0, defaultBackend.length - 5) + "/version"
+
+    notify() {
+      const h = this.$createElement;
+
+      this.$notify({
+        title: "隐私提示",
+        type: "warning",
+        message: h(
+          "i",
+          { style: "color: teal" },
+          "各种订阅链接(短链接服务除外)生成纯前端实现,无隐私问题。默认提供后端转换服务,隐私担忧者请自行搭建后端服务。"
         )
-        .then(res => {
-          this.backendVersion = res.data.replace(/backend\n$/gm, "");
-          this.backendVersion = this.backendVersion.replace("subconverter", "");
-        });
+      });
     },
+
+    // 表单相关方法
     saveSubUrl() {
-      if (this.form.sourceSubUrl !== '') {
-        this.setLocalStorageItem('sourceSubUrl', this.form.sourceSubUrl)
-      }
+      saveSubscriptionUrl(this.form);
     },
-    getLocalStorageItem(itemKey) {
-      const now = +new Date()
-      let ls = localStorage.getItem(itemKey)
-
-      let itemValue = ''
-      if (ls !== null) {
-        let data = JSON.parse(ls)
-        if (data.expire > now) {
-          itemValue = data.value
-        } else {
-          localStorage.removeItem(itemKey)
-        }
-      }
 
-      return itemValue
+    addCustomParam() {
+      addCustomParam(this.customParams);
     },
-    setLocalStorageItem(itemKey, itemValue) {
-      const ttl = process.env.VUE_APP_CACHE_TTL
-      const now = +new Date()
-
-      let data = {
-        setTime: now,
-        ttl: parseInt(ttl),
-        expire: now + ttl * 1000,
-        value: itemValue
-      }
-      localStorage.setItem(itemKey, JSON.stringify(data))
-    }
-  },
+
+    // 使用 composables
+    ...useSubscription(),
+    ...useUrlParser()
+  }
 };
 </script>

+ 0 - 13
vue.config.js

@@ -30,18 +30,5 @@ module.exports = {
         symbolId: 'icon-[name]'
       })
       .end()
-  },
-
-  pwa: {
-    workboxOptions: {
-      // https://developers.google.com/web/tools/workbox/modules/workbox-webpack-plugin
-      skipWaiting: true,
-      clientsClaim: true,
-      importScripts: [
-        'https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js'
-      ],
-      navigateFallback: '/',
-      navigateFallbackDenylist: [/\/api\//]
-    }
   }
 };

+ 16 - 536
yarn.lock

@@ -11,16 +11,7 @@
     event-pubsub "4.3.0"
     js-message "1.0.7"
 
-"@apideck/better-ajv-errors@^0.3.1":
-  version "0.3.6"
-  resolved "https://registry.npmmirror.com/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz#957d4c28e886a64a8141f7522783be65733ff097"
-  integrity sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==
-  dependencies:
-    json-schema "^0.4.0"
-    jsonpointer "^5.0.0"
-    leven "^3.1.0"
-
-"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.27.1":
+"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.27.1":
   version "7.27.1"
   resolved "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be"
   integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==
@@ -34,7 +25,7 @@
   resolved "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.28.5.tgz#a8a4962e1567121ac0b3b487f52107443b455c7f"
   integrity sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==
 
-"@babel/core@^7.11.1", "@babel/core@^7.12.16", "@babel/core@^7.26.0":
+"@babel/core@^7.12.16", "@babel/core@^7.26.0":
   version "7.28.5"
   resolved "https://registry.npmmirror.com/@babel/core/-/core-7.28.5.tgz#4c81b35e51e1b734f510c99b07dfbc7bbbb48f7e"
   integrity sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==
@@ -147,7 +138,7 @@
     "@babel/types" "7.0.0-beta.35"
     lodash "^4.2.0"
 
-"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.27.1":
+"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.27.1":
   version "7.27.1"
   resolved "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz#7ef769a323e2655e126673bb6d2d6913bbead204"
   integrity sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==
@@ -754,7 +745,7 @@
     "@babel/helper-create-regexp-features-plugin" "^7.27.1"
     "@babel/helper-plugin-utils" "^7.27.1"
 
-"@babel/preset-env@^7.11.0", "@babel/preset-env@^7.12.16":
+"@babel/preset-env@^7.12.16":
   version "7.28.5"
   resolved "https://registry.npmmirror.com/@babel/preset-env/-/preset-env-7.28.5.tgz#82dd159d1563f219a1ce94324b3071eb89e280b0"
   integrity sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg==
@@ -839,7 +830,7 @@
     "@babel/types" "^7.4.4"
     esutils "^2.0.2"
 
-"@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13":
+"@babel/runtime@^7.12.13":
   version "7.28.4"
   resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326"
   integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==
@@ -1127,43 +1118,6 @@
   resolved "https://registry.npmmirror.com/@polka/url/-/url-1.0.0-next.29.tgz#5a40109a1ab5f84d6fd8fc928b19f367cbe7e7b1"
   integrity sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==
 
-"@rollup/plugin-babel@^5.2.0":
-  version "5.3.1"
-  resolved "https://registry.npmmirror.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283"
-  integrity sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==
-  dependencies:
-    "@babel/helper-module-imports" "^7.10.4"
-    "@rollup/pluginutils" "^3.1.0"
-
-"@rollup/plugin-node-resolve@^11.2.1":
-  version "11.2.1"
-  resolved "https://registry.npmmirror.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz#82aa59397a29cd4e13248b106e6a4a1880362a60"
-  integrity sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==
-  dependencies:
-    "@rollup/pluginutils" "^3.1.0"
-    "@types/resolve" "1.17.1"
-    builtin-modules "^3.1.0"
-    deepmerge "^4.2.2"
-    is-module "^1.0.0"
-    resolve "^1.19.0"
-
-"@rollup/plugin-replace@^2.4.1":
-  version "2.4.2"
-  resolved "https://registry.npmmirror.com/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz#a2d539314fbc77c244858faa523012825068510a"
-  integrity sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==
-  dependencies:
-    "@rollup/pluginutils" "^3.1.0"
-    magic-string "^0.25.7"
-
-"@rollup/pluginutils@^3.1.0":
-  version "3.1.0"
-  resolved "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b"
-  integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==
-  dependencies:
-    "@types/estree" "0.0.39"
-    estree-walker "^1.0.1"
-    picomatch "^2.2.2"
-
 "@sideway/address@^4.1.5":
   version "4.1.5"
   resolved "https://registry.npmmirror.com/@sideway/address/-/address-4.1.5.tgz#4bc149a0076623ced99ca8208ba780d65a99b9d5"
@@ -1196,16 +1150,6 @@
   resolved "https://registry.npmmirror.com/@soda/get-current-script/-/get-current-script-1.0.2.tgz#a53515db25d8038374381b73af20bb4f2e508d87"
   integrity sha512-T7VNNlYVM1SgQ+VsMYhnDkcGmWhQdL0bDyGm5TlQ3GBXnJscEClUUOKduWTmm2zCnvNLC1hc3JpuXjs/nFOc5w==
 
-"@surma/rollup-plugin-off-main-thread@^2.2.3":
-  version "2.2.3"
-  resolved "https://registry.npmmirror.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053"
-  integrity sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==
-  dependencies:
-    ejs "^3.1.6"
-    json5 "^2.2.0"
-    magic-string "^0.25.0"
-    string.prototype.matchall "^4.0.6"
-
 "@trysound/[email protected]":
   version "0.2.0"
   resolved "https://registry.npmmirror.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
@@ -1270,11 +1214,6 @@
   resolved "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
   integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
 
-"@types/[email protected]":
-  version "0.0.39"
-  resolved "https://registry.npmmirror.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
-  integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
-
 "@types/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0":
   version "5.1.0"
   resolved "https://registry.npmmirror.com/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz#74f47555b3d804b54cb7030e6f9aa0c7485cfc5b"
@@ -1380,13 +1319,6 @@
   resolved "https://registry.npmmirror.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb"
   integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==
 
-"@types/[email protected]":
-  version "1.17.1"
-  resolved "https://registry.npmmirror.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6"
-  integrity sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==
-  dependencies:
-    "@types/node" "*"
-
 "@types/[email protected]":
   version "0.12.0"
   resolved "https://registry.npmmirror.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
@@ -1438,11 +1370,6 @@
   dependencies:
     "@types/node" "*"
 
-"@types/trusted-types@^2.0.2":
-  version "2.0.7"
-  resolved "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
-  integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
-
 "@types/ws@^8.5.5":
   version "8.18.1"
   resolved "https://registry.npmmirror.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9"
@@ -1616,16 +1543,6 @@
     webpack "^5.54.0"
     yorkie "^2.0.0"
 
-"@vue/cli-plugin-pwa@5":
-  version "5.0.9"
-  resolved "https://registry.npmmirror.com/@vue/cli-plugin-pwa/-/cli-plugin-pwa-5.0.9.tgz#7e1693878a85c14aa5e7913f8279ae661304139b"
-  integrity sha512-x8yavT2kvD8iyARc5eHMNrYfwWB4+cDtiwz2N2F/SHDMTfxdA2wcmqUHBB3oc1kn+hMbgvf8cDeQc211Uvb3xg==
-  dependencies:
-    "@vue/cli-shared-utils" "^5.0.9"
-    html-webpack-plugin "^5.1.0"
-    webpack "^5.54.0"
-    workbox-webpack-plugin "^6.1.0"
-
 "@vue/cli-plugin-router@5", "@vue/cli-plugin-router@^5.0.9":
   version "5.0.9"
   resolved "https://registry.npmmirror.com/@vue/cli-plugin-router/-/cli-plugin-router-5.0.9.tgz#8ca4210c549d78abff8f2431357fcf6c81f84a02"
@@ -2002,7 +1919,7 @@ ajv@^6.12.4, ajv@^6.12.5:
     json-schema-traverse "^0.4.1"
     uri-js "^4.2.2"
 
-ajv@^8.0.0, ajv@^8.6.0, ajv@^8.9.0:
+ajv@^8.0.0, ajv@^8.9.0:
   version "8.17.1"
   resolved "https://registry.npmmirror.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6"
   integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==
@@ -2352,13 +2269,6 @@ brace-expansion@^1.1.7:
     balanced-match "^1.0.0"
     concat-map "0.0.1"
 
-brace-expansion@^2.0.1:
-  version "2.0.2"
-  resolved "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7"
-  integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==
-  dependencies:
-    balanced-match "^1.0.0"
-
 braces@^2.2.2:
   version "2.3.2"
   resolved "https://registry.npmmirror.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"
@@ -2411,11 +2321,6 @@ buffer@^5.5.0:
     base64-js "^1.3.1"
     ieee754 "^1.1.13"
 
-builtin-modules@^3.1.0:
-  version "3.3.0"
-  resolved "https://registry.npmmirror.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6"
-  integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==
-
 [email protected], bytes@~3.1.2:
   version "3.1.2"
   resolved "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
@@ -2719,11 +2624,6 @@ commander@^8.3.0:
   resolved "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
   integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
 
-common-tags@^1.8.0:
-  version "1.8.2"
-  resolved "https://registry.npmmirror.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6"
-  integrity sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==
-
 commondir@^1.0.1:
   version "1.0.1"
   resolved "https://registry.npmmirror.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
@@ -2877,11 +2777,6 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3:
     shebang-command "^2.0.0"
     which "^2.0.1"
 
-crypto-random-string@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.npmmirror.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
-  integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==
-
 css-declaration-sorter@^6.3.1:
   version "6.4.1"
   resolved "https://registry.npmmirror.com/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz#28beac7c20bad7f1775be3a7129d7eae409a3a71"
@@ -3074,11 +2969,6 @@ deepmerge@^1.2.0, deepmerge@^1.5.2:
   resolved "https://registry.npmmirror.com/deepmerge/-/deepmerge-1.5.2.tgz#10499d868844cdad4fee0842df8c7f6f0c95a753"
   integrity sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==
 
-deepmerge@^4.2.2:
-  version "4.3.1"
-  resolved "https://registry.npmmirror.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
-  integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
-
 default-gateway@^6.0.3:
   version "6.0.3"
   resolved "https://registry.npmmirror.com/default-gateway/-/default-gateway-6.0.3.tgz#819494c888053bdb743edbf343d6cdf7f2943a71"
@@ -3301,13 +3191,6 @@ [email protected]:
   resolved "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
   integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
 
-ejs@^3.1.6:
-  version "3.1.10"
-  resolved "https://registry.npmmirror.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b"
-  integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==
-  dependencies:
-    jake "^10.8.5"
-
 electron-to-chromium@^1.5.263:
   version "1.5.266"
   resolved "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.266.tgz#41ed029b3cf641c4ee071de42954b36dca8f5f4e"
@@ -3389,7 +3272,7 @@ error-stack-parser@^2.0.6:
   dependencies:
     stackframe "^1.3.4"
 
-es-abstract@^1.23.5, es-abstract@^1.23.6, es-abstract@^1.23.9:
+es-abstract@^1.23.5, es-abstract@^1.23.9:
   version "1.24.0"
   resolved "https://registry.npmmirror.com/es-abstract/-/es-abstract-1.24.0.tgz#c44732d2beb0acc1ed60df840869e3106e7af328"
   integrity sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==
@@ -3637,11 +3520,6 @@ estraverse@^5.1.0, estraverse@^5.2.0:
   resolved "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
   integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
 
-estree-walker@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.npmmirror.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700"
-  integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==
-
 estree-walker@^2.0.2:
   version "2.0.2"
   resolved "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
@@ -3808,7 +3686,7 @@ fast-glob@^3.2.7, fast-glob@^3.2.9:
     merge2 "^1.3.0"
     micromatch "^4.0.8"
 
-fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0:
+fast-json-stable-stringify@^2.0.0:
   version "2.1.0"
   resolved "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
   integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
@@ -3851,13 +3729,6 @@ file-entry-cache@^6.0.1:
   dependencies:
     flat-cache "^3.0.4"
 
-filelist@^1.0.4:
-  version "1.0.4"
-  resolved "https://registry.npmmirror.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5"
-  integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==
-  dependencies:
-    minimatch "^5.0.1"
-
 fill-range@^4.0.0:
   version "4.0.0"
   resolved "https://registry.npmmirror.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
@@ -3971,7 +3842,7 @@ [email protected], fresh@~0.5.2:
   resolved "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
   integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
 
-fs-extra@^9.0.1, fs-extra@^9.1.0:
+fs-extra@^9.1.0:
   version "9.1.0"
   resolved "https://registry.npmmirror.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d"
   integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==
@@ -4049,11 +3920,6 @@ get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@
     hasown "^2.0.2"
     math-intrinsics "^1.1.0"
 
-get-own-enumerable-property-symbols@^3.0.0:
-  version "3.0.2"
-  resolved "https://registry.npmmirror.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664"
-  integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==
-
 get-proto@^1.0.1:
   version "1.0.1"
   resolved "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
@@ -4112,7 +3978,7 @@ glob-to-regexp@^0.4.1:
   resolved "https://registry.npmmirror.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
   integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
 
-glob@^7.1.3, glob@^7.1.6:
+glob@^7.1.3:
   version "7.2.3"
   resolved "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
   integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
@@ -4451,11 +4317,6 @@ icss-utils@^5.0.0, icss-utils@^5.1.0:
   resolved "https://registry.npmmirror.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
   integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==
 
-idb@^7.0.1:
-  version "7.1.1"
-  resolved "https://registry.npmmirror.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b"
-  integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==
-
 ieee754@^1.1.13:
   version "1.2.1"
   resolved "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
@@ -4718,11 +4579,6 @@ is-map@^2.0.3:
   resolved "https://registry.npmmirror.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e"
   integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==
 
-is-module@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.npmmirror.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
-  integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==
-
 is-negative-zero@^2.0.3:
   version "2.0.3"
   resolved "https://registry.npmmirror.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747"
@@ -4748,11 +4604,6 @@ is-number@^7.0.0:
   resolved "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
   integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
 
-is-obj@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.npmmirror.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
-  integrity sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==
-
 is-path-inside@^3.0.3:
   version "3.0.3"
   resolved "https://registry.npmmirror.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
@@ -4785,11 +4636,6 @@ is-regex@^1.2.1:
     has-tostringtag "^1.0.2"
     hasown "^2.0.2"
 
-is-regexp@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.npmmirror.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069"
-  integrity sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==
-
 is-set@^2.0.3:
   version "2.0.3"
   resolved "https://registry.npmmirror.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d"
@@ -4900,29 +4746,11 @@ isobject@^3.0.0, isobject@^3.0.1:
   resolved "https://registry.npmmirror.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
   integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==
 
-jake@^10.8.5:
-  version "10.9.4"
-  resolved "https://registry.npmmirror.com/jake/-/jake-10.9.4.tgz#d626da108c63d5cfb00ab5c25fadc7e0084af8e6"
-  integrity sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==
-  dependencies:
-    async "^3.2.6"
-    filelist "^1.0.4"
-    picocolors "^1.1.1"
-
 javascript-stringify@^2.0.1:
   version "2.1.0"
   resolved "https://registry.npmmirror.com/javascript-stringify/-/javascript-stringify-2.1.0.tgz#27c76539be14d8bd128219a2d731b09337904e79"
   integrity sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==
 
-jest-worker@^26.2.1:
-  version "26.6.2"
-  resolved "https://registry.npmmirror.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed"
-  integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==
-  dependencies:
-    "@types/node" "*"
-    merge-stream "^2.0.0"
-    supports-color "^7.0.0"
-
 jest-worker@^27.0.2, jest-worker@^27.4.5:
   version "27.5.1"
   resolved "https://registry.npmmirror.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0"
@@ -5004,11 +4832,6 @@ json-schema-traverse@^1.0.0:
   resolved "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
   integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
 
-json-schema@^0.4.0:
-  version "0.4.0"
-  resolved "https://registry.npmmirror.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5"
-  integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==
-
 json-stable-stringify-without-jsonify@^1.0.1:
   version "1.0.1"
   resolved "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
@@ -5021,7 +4844,7 @@ json5@^1.0.1:
   dependencies:
     minimist "^1.2.0"
 
-json5@^2.1.2, json5@^2.2.0, json5@^2.2.3:
+json5@^2.1.2, json5@^2.2.3:
   version "2.2.3"
   resolved "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
   integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
@@ -5035,11 +4858,6 @@ jsonfile@^6.0.1:
   optionalDependencies:
     graceful-fs "^4.1.6"
 
-jsonpointer@^5.0.0:
-  version "5.0.1"
-  resolved "https://registry.npmmirror.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559"
-  integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==
-
 keyv@^4.5.3:
   version "4.5.4"
   resolved "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
@@ -5091,11 +4909,6 @@ launch-editor@^2.12.0, launch-editor@^2.2.1, launch-editor@^2.6.0:
     picocolors "^1.1.1"
     shell-quote "^1.8.3"
 
-leven@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.npmmirror.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
-  integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==
-
 levn@^0.4.1:
   version "0.4.1"
   resolved "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
@@ -5181,11 +4994,6 @@ lodash.merge@^4.6.2:
   resolved "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
   integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
 
-lodash.sortby@^4.7.0:
-  version "4.7.0"
-  resolved "https://registry.npmmirror.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
-  integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==
-
 lodash.uniq@^4.5.0:
   version "4.5.0"
   resolved "https://registry.npmmirror.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
@@ -5242,13 +5050,6 @@ lru-cache@^6.0.0:
   dependencies:
     yallist "^4.0.0"
 
-magic-string@^0.25.0, magic-string@^0.25.7:
-  version "0.25.9"
-  resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c"
-  integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==
-  dependencies:
-    sourcemap-codec "^1.4.8"
-
 magic-string@^0.30.21:
   version "0.30.21"
   resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91"
@@ -5410,13 +5211,6 @@ minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
   dependencies:
     brace-expansion "^1.1.7"
 
-minimatch@^5.0.1:
-  version "5.1.6"
-  resolved "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96"
-  integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==
-  dependencies:
-    brace-expansion "^2.0.1"
-
 minimist@^1.2.0, minimist@^1.2.5:
   version "1.2.8"
   resolved "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
@@ -5906,7 +5700,7 @@ picocolors@^1.0.0, picocolors@^1.1.1:
   resolved "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
   integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
 
-picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.3.1:
+picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
   version "2.3.1"
   resolved "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
   integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
@@ -6271,11 +6065,6 @@ prelude-ls@^1.2.1:
   resolved "https://registry.npmmirror.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
   integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
 
-pretty-bytes@^5.3.0, pretty-bytes@^5.4.1:
-  version "5.6.0"
-  resolved "https://registry.npmmirror.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
-  integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==
-
 pretty-error@^4.0.0:
   version "4.0.0"
   resolved "https://registry.npmmirror.com/pretty-error/-/pretty-error-4.0.0.tgz#90a703f46dd7234adb46d0f84823e9d1cb8f10d6"
@@ -6458,7 +6247,7 @@ regex-not@^1.0.0, regex-not@^1.0.2:
     extend-shallow "^3.0.2"
     safe-regex "^1.1.0"
 
-regexp.prototype.flags@^1.5.3, regexp.prototype.flags@^1.5.4:
+regexp.prototype.flags@^1.5.4:
   version "1.5.4"
   resolved "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz#1ad6c62d44a259007e55b3970e00f746efbcaa19"
   integrity sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==
@@ -6482,11 +6271,6 @@ regexpu-core@^6.3.1:
     unicode-match-property-ecmascript "^2.0.0"
     unicode-match-property-value-ecmascript "^2.2.1"
 
-register-service-worker@^1.7.1:
-  version "1.7.2"
-  resolved "https://registry.npmmirror.com/register-service-worker/-/register-service-worker-1.7.2.tgz#6516983e1ef790a98c4225af1216bc80941a4bd2"
-  integrity sha512-CiD3ZSanZqcMPRhtfct5K9f7i3OLCcBBWsJjLh1gW9RO/nS94sVzY59iS+fgYBOBqaBpf4EzfqUF3j9IG+xo8A==
-
 regjsgen@^0.8.0:
   version "0.8.0"
   resolved "https://registry.npmmirror.com/regjsgen/-/regjsgen-0.8.0.tgz#df23ff26e0c5b300a6470cad160a9d090c3a37ab"
@@ -6555,7 +6339,7 @@ resolve-url@^0.2.1:
   resolved "https://registry.npmmirror.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
   integrity sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==
 
-resolve@^1.10.0, resolve@^1.19.0, resolve@^1.22.10:
+resolve@^1.10.0, resolve@^1.22.10:
   version "1.22.11"
   resolved "https://registry.npmmirror.com/resolve/-/resolve-1.22.11.tgz#aad857ce1ffb8bfa9b0b1ac29f1156383f68c262"
   integrity sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==
@@ -6602,23 +6386,6 @@ rimraf@^3.0.2:
   dependencies:
     glob "^7.1.3"
 
-rollup-plugin-terser@^7.0.0:
-  version "7.0.2"
-  resolved "https://registry.npmmirror.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d"
-  integrity sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==
-  dependencies:
-    "@babel/code-frame" "^7.10.4"
-    jest-worker "^26.2.1"
-    serialize-javascript "^4.0.0"
-    terser "^5.0.0"
-
-rollup@^2.43.1:
-  version "2.79.2"
-  resolved "https://registry.npmmirror.com/rollup/-/rollup-2.79.2.tgz#f150e4a5db4b121a21a747d762f701e5e9f49090"
-  integrity sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==
-  optionalDependencies:
-    fsevents "~2.3.2"
-
 run-parallel@^1.1.9:
   version "1.2.0"
   resolved "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
@@ -6797,13 +6564,6 @@ send@~0.19.0:
     range-parser "~1.2.1"
     statuses "2.0.1"
 
-serialize-javascript@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.npmmirror.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa"
-  integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==
-  dependencies:
-    randombytes "^2.1.0"
-
 serialize-javascript@^6.0.0, serialize-javascript@^6.0.2:
   version "6.0.2"
   resolved "https://registry.npmmirror.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2"
@@ -7019,11 +6779,6 @@ sockjs@^0.3.24:
     uuid "^8.3.2"
     websocket-driver "^0.7.4"
 
-source-list-map@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.npmmirror.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
-  integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
-
 "source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.2.1:
   version "1.2.1"
   resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
@@ -7063,18 +6818,6 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1:
   resolved "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
   integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
 
-source-map@^0.8.0-beta.0:
-  version "0.8.0-beta.0"
-  resolved "https://registry.npmmirror.com/source-map/-/source-map-0.8.0-beta.0.tgz#d4c1bb42c3f7ee925f005927ba10709e0d1d1f11"
-  integrity sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==
-  dependencies:
-    whatwg-url "^7.0.0"
-
-sourcemap-codec@^1.4.8:
-  version "1.4.8"
-  resolved "https://registry.npmmirror.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
-  integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
-
 spdx-correct@^3.0.0:
   version "3.2.0"
   resolved "https://registry.npmmirror.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c"
@@ -7201,25 +6944,6 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
     is-fullwidth-code-point "^3.0.0"
     strip-ansi "^6.0.1"
 
-string.prototype.matchall@^4.0.6:
-  version "4.0.12"
-  resolved "https://registry.npmmirror.com/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz#6c88740e49ad4956b1332a911e949583a275d4c0"
-  integrity sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==
-  dependencies:
-    call-bind "^1.0.8"
-    call-bound "^1.0.3"
-    define-properties "^1.2.1"
-    es-abstract "^1.23.6"
-    es-errors "^1.3.0"
-    es-object-atoms "^1.0.0"
-    get-intrinsic "^1.2.6"
-    gopd "^1.2.0"
-    has-symbols "^1.1.0"
-    internal-slot "^1.1.0"
-    regexp.prototype.flags "^1.5.3"
-    set-function-name "^2.0.2"
-    side-channel "^1.1.0"
-
 string.prototype.trim@^1.2.10:
   version "1.2.10"
   resolved "https://registry.npmmirror.com/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz#40b2dd5ee94c959b4dcfb1d65ce72e90da480c81"
@@ -7266,15 +6990,6 @@ string_decoder@~1.1.1:
   dependencies:
     safe-buffer "~5.1.0"
 
-stringify-object@^3.3.0:
-  version "3.3.0"
-  resolved "https://registry.npmmirror.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629"
-  integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==
-  dependencies:
-    get-own-enumerable-property-symbols "^3.0.0"
-    is-obj "^1.0.1"
-    is-regexp "^1.0.0"
-
 strip-ansi@^3.0.0:
   version "3.0.1"
   resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
@@ -7296,11 +7011,6 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1:
   dependencies:
     ansi-regex "^5.0.1"
 
-strip-comments@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.npmmirror.com/strip-comments/-/strip-comments-2.0.1.tgz#4ad11c3fbcac177a67a40ac224ca339ca1c1ba9b"
-  integrity sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==
-
 strip-eof@^1.0.0:
   version "1.0.0"
   resolved "https://registry.npmmirror.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
@@ -7348,7 +7058,7 @@ supports-color@^5.3.0:
   dependencies:
     has-flag "^3.0.0"
 
-supports-color@^7.0.0, supports-color@^7.1.0:
+supports-color@^7.1.0:
   version "7.2.0"
   resolved "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
   integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
@@ -7432,21 +7142,6 @@ tapable@^2.0.0, tapable@^2.2.0, tapable@^2.2.1, tapable@^2.3.0:
   resolved "https://registry.npmmirror.com/tapable/-/tapable-2.3.0.tgz#7e3ea6d5ca31ba8e078b560f0d83ce9a14aa8be6"
   integrity sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==
 
-temp-dir@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.npmmirror.com/temp-dir/-/temp-dir-2.0.0.tgz#bde92b05bdfeb1516e804c9c00ad45177f31321e"
-  integrity sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==
-
-tempy@^0.6.0:
-  version "0.6.0"
-  resolved "https://registry.npmmirror.com/tempy/-/tempy-0.6.0.tgz#65e2c35abc06f1124a97f387b08303442bde59f3"
-  integrity sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==
-  dependencies:
-    is-stream "^2.0.0"
-    temp-dir "^2.0.0"
-    type-fest "^0.16.0"
-    unique-string "^2.0.0"
-
 terser-webpack-plugin@^5.1.1, terser-webpack-plugin@^5.3.11:
   version "5.3.15"
   resolved "https://registry.npmmirror.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.15.tgz#0a26860b765eaffa8e840170aabc5b3a3f6f6bb9"
@@ -7458,7 +7153,7 @@ terser-webpack-plugin@^5.1.1, terser-webpack-plugin@^5.3.11:
     serialize-javascript "^6.0.2"
     terser "^5.31.1"
 
-terser@^5.0.0, terser@^5.10.0, terser@^5.31.1:
+terser@^5.10.0, terser@^5.31.1:
   version "5.44.1"
   resolved "https://registry.npmmirror.com/terser/-/terser-5.44.1.tgz#e391e92175c299b8c284ad6ded609e37303b0a9c"
   integrity sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==
@@ -7560,13 +7255,6 @@ totalist@^3.0.0:
   resolved "https://registry.npmmirror.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8"
   integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==
 
-tr46@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.npmmirror.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
-  integrity sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==
-  dependencies:
-    punycode "^2.1.0"
-
 tr46@~0.0.3:
   version "0.0.3"
   resolved "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
@@ -7593,11 +7281,6 @@ type-check@^0.4.0, type-check@~0.4.0:
   dependencies:
     prelude-ls "^1.2.1"
 
-type-fest@^0.16.0:
-  version "0.16.0"
-  resolved "https://registry.npmmirror.com/type-fest/-/type-fest-0.16.0.tgz#3240b891a78b0deae910dbeb86553e552a148860"
-  integrity sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==
-
 type-fest@^0.20.2:
   version "0.20.2"
   resolved "https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
@@ -7733,13 +7416,6 @@ union-value@^1.0.0:
     is-extendable "^0.1.1"
     set-value "^2.0.1"
 
-unique-string@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.npmmirror.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d"
-  integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==
-  dependencies:
-    crypto-random-string "^2.0.0"
-
 universalify@^2.0.0:
   version "2.0.1"
   resolved "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
@@ -7758,11 +7434,6 @@ unset-value@^1.0.0:
     has-value "^0.3.1"
     isobject "^3.0.0"
 
-upath@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.npmmirror.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894"
-  integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==
-
 update-browserslist-db@^1.2.0:
   version "1.2.2"
   resolved "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz#cfb4358afa08b3d5731a2ecd95eebf4ddef8033e"
@@ -7931,11 +7602,6 @@ webidl-conversions@^3.0.0:
   resolved "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
   integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
 
-webidl-conversions@^4.0.2:
-  version "4.0.2"
-  resolved "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
-  integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
-
 webpack-bundle-analyzer@^4.4.0:
   version "4.10.2"
   resolved "https://registry.npmmirror.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz#633af2862c213730be3dbdf40456db171b60d5bd"
@@ -8018,14 +7684,6 @@ webpack-merge@^5.7.3:
     flat "^5.0.2"
     wildcard "^2.0.0"
 
-webpack-sources@^1.4.3:
-  version "1.4.3"
-  resolved "https://registry.npmmirror.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933"
-  integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==
-  dependencies:
-    source-list-map "^2.0.0"
-    source-map "~0.6.1"
-
 webpack-sources@^3.3.3:
   version "3.3.3"
   resolved "https://registry.npmmirror.com/webpack-sources/-/webpack-sources-3.3.3.tgz#d4bf7f9909675d7a070ff14d0ef2a4f3c982c723"
@@ -8094,15 +7752,6 @@ whatwg-url@^5.0.0:
     tr46 "~0.0.3"
     webidl-conversions "^3.0.0"
 
-whatwg-url@^7.0.0:
-  version "7.1.0"
-  resolved "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"
-  integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==
-  dependencies:
-    lodash.sortby "^4.7.0"
-    tr46 "^1.0.1"
-    webidl-conversions "^4.0.2"
-
 which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1:
   version "1.1.1"
   resolved "https://registry.npmmirror.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz#d76ec27df7fa165f18d5808374a5fe23c29b176e"
@@ -8180,175 +7829,6 @@ word-wrap@^1.2.5:
   resolved "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
   integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
 
[email protected]:
-  version "6.6.1"
-  resolved "https://registry.npmmirror.com/workbox-background-sync/-/workbox-background-sync-6.6.1.tgz#08d603a33717ce663e718c30cc336f74909aff2f"
-  integrity sha512-trJd3ovpWCvzu4sW0E8rV3FUyIcC0W8G+AZ+VcqzzA890AsWZlUGOTSxIMmIHVusUw/FDq1HFWfy/kC/WTRqSg==
-  dependencies:
-    idb "^7.0.1"
-    workbox-core "6.6.1"
-
[email protected]:
-  version "6.6.1"
-  resolved "https://registry.npmmirror.com/workbox-broadcast-update/-/workbox-broadcast-update-6.6.1.tgz#0fad9454cf8e4ace0c293e5617c64c75d8a8c61e"
-  integrity sha512-fBhffRdaANdeQ1V8s692R9l/gzvjjRtydBOvR6WCSB0BNE2BacA29Z4r9/RHd9KaXCPl6JTdI9q0bR25YKP8TQ==
-  dependencies:
-    workbox-core "6.6.1"
-
[email protected]:
-  version "6.6.1"
-  resolved "https://registry.npmmirror.com/workbox-build/-/workbox-build-6.6.1.tgz#6010e9ce550910156761448f2dbea8cfcf759cb0"
-  integrity sha512-INPgDx6aRycAugUixbKgiEQBWD0MPZqU5r0jyr24CehvNuLPSXp/wGOpdRJmts656lNiXwqV7dC2nzyrzWEDnw==
-  dependencies:
-    "@apideck/better-ajv-errors" "^0.3.1"
-    "@babel/core" "^7.11.1"
-    "@babel/preset-env" "^7.11.0"
-    "@babel/runtime" "^7.11.2"
-    "@rollup/plugin-babel" "^5.2.0"
-    "@rollup/plugin-node-resolve" "^11.2.1"
-    "@rollup/plugin-replace" "^2.4.1"
-    "@surma/rollup-plugin-off-main-thread" "^2.2.3"
-    ajv "^8.6.0"
-    common-tags "^1.8.0"
-    fast-json-stable-stringify "^2.1.0"
-    fs-extra "^9.0.1"
-    glob "^7.1.6"
-    lodash "^4.17.20"
-    pretty-bytes "^5.3.0"
-    rollup "^2.43.1"
-    rollup-plugin-terser "^7.0.0"
-    source-map "^0.8.0-beta.0"
-    stringify-object "^3.3.0"
-    strip-comments "^2.0.1"
-    tempy "^0.6.0"
-    upath "^1.2.0"
-    workbox-background-sync "6.6.1"
-    workbox-broadcast-update "6.6.1"
-    workbox-cacheable-response "6.6.1"
-    workbox-core "6.6.1"
-    workbox-expiration "6.6.1"
-    workbox-google-analytics "6.6.1"
-    workbox-navigation-preload "6.6.1"
-    workbox-precaching "6.6.1"
-    workbox-range-requests "6.6.1"
-    workbox-recipes "6.6.1"
-    workbox-routing "6.6.1"
-    workbox-strategies "6.6.1"
-    workbox-streams "6.6.1"
-    workbox-sw "6.6.1"
-    workbox-window "6.6.1"
-
[email protected]:
-  version "6.6.1"
-  resolved "https://registry.npmmirror.com/workbox-cacheable-response/-/workbox-cacheable-response-6.6.1.tgz#284c2b86be3f4fd191970ace8c8e99797bcf58e9"
-  integrity sha512-85LY4veT2CnTCDxaVG7ft3NKaFbH6i4urZXgLiU4AiwvKqS2ChL6/eILiGRYXfZ6gAwDnh5RkuDbr/GMS4KSag==
-  dependencies:
-    workbox-core "6.6.1"
-
[email protected]:
-  version "6.6.1"
-  resolved "https://registry.npmmirror.com/workbox-core/-/workbox-core-6.6.1.tgz#7184776d4134c5ed2f086878c882728fc9084265"
-  integrity sha512-ZrGBXjjaJLqzVothoE12qTbVnOAjFrHDXpZe7coCb6q65qI/59rDLwuFMO4PcZ7jcbxY+0+NhUVztzR/CbjEFw==
-
[email protected]:
-  version "6.6.1"
-  resolved "https://registry.npmmirror.com/workbox-expiration/-/workbox-expiration-6.6.1.tgz#a841fa36676104426dbfb9da1ef6a630b4f93739"
-  integrity sha512-qFiNeeINndiOxaCrd2DeL1Xh1RFug3JonzjxUHc5WkvkD2u5abY3gZL1xSUNt3vZKsFFGGORItSjVTVnWAZO4A==
-  dependencies:
-    idb "^7.0.1"
-    workbox-core "6.6.1"
-
[email protected]:
-  version "6.6.1"
-  resolved "https://registry.npmmirror.com/workbox-google-analytics/-/workbox-google-analytics-6.6.1.tgz#a07a6655ab33d89d1b0b0a935ffa5dea88618c5d"
-  integrity sha512-1TjSvbFSLmkpqLcBsF7FuGqqeDsf+uAXO/pjiINQKg3b1GN0nBngnxLcXDYo1n/XxK4N7RaRrpRlkwjY/3ocuA==
-  dependencies:
-    workbox-background-sync "6.6.1"
-    workbox-core "6.6.1"
-    workbox-routing "6.6.1"
-    workbox-strategies "6.6.1"
-
[email protected]:
-  version "6.6.1"
-  resolved "https://registry.npmmirror.com/workbox-navigation-preload/-/workbox-navigation-preload-6.6.1.tgz#61a34fe125558dd88cf09237f11bd966504ea059"
-  integrity sha512-DQCZowCecO+wRoIxJI2V6bXWK6/53ff+hEXLGlQL4Rp9ZaPDLrgV/32nxwWIP7QpWDkVEtllTAK5h6cnhxNxDA==
-  dependencies:
-    workbox-core "6.6.1"
-
[email protected]:
-  version "6.6.1"
-  resolved "https://registry.npmmirror.com/workbox-precaching/-/workbox-precaching-6.6.1.tgz#dedeeba10a2d163d990bf99f1c2066ac0d1a19e2"
-  integrity sha512-K4znSJ7IKxCnCYEdhNkMr7X1kNh8cz+mFgx9v5jFdz1MfI84pq8C2zG+oAoeE5kFrUf7YkT5x4uLWBNg0DVZ5A==
-  dependencies:
-    workbox-core "6.6.1"
-    workbox-routing "6.6.1"
-    workbox-strategies "6.6.1"
-
[email protected]:
-  version "6.6.1"
-  resolved "https://registry.npmmirror.com/workbox-range-requests/-/workbox-range-requests-6.6.1.tgz#ddaf7e73af11d362fbb2f136a9063a4c7f507a39"
-  integrity sha512-4BDzk28govqzg2ZpX0IFkthdRmCKgAKreontYRC5YsAPB2jDtPNxqx3WtTXgHw1NZalXpcH/E4LqUa9+2xbv1g==
-  dependencies:
-    workbox-core "6.6.1"
-
[email protected]:
-  version "6.6.1"
-  resolved "https://registry.npmmirror.com/workbox-recipes/-/workbox-recipes-6.6.1.tgz#ea70d2b2b0b0bce8de0a9d94f274d4a688e69fae"
-  integrity sha512-/oy8vCSzromXokDA+X+VgpeZJvtuf8SkQ8KL0xmRivMgJZrjwM3c2tpKTJn6PZA6TsbxGs3Sc7KwMoZVamcV2g==
-  dependencies:
-    workbox-cacheable-response "6.6.1"
-    workbox-core "6.6.1"
-    workbox-expiration "6.6.1"
-    workbox-precaching "6.6.1"
-    workbox-routing "6.6.1"
-    workbox-strategies "6.6.1"
-
[email protected]:
-  version "6.6.1"
-  resolved "https://registry.npmmirror.com/workbox-routing/-/workbox-routing-6.6.1.tgz#cba9a1c7e0d1ea11e24b6f8c518840efdc94f581"
-  integrity sha512-j4ohlQvfpVdoR8vDYxTY9rA9VvxTHogkIDwGdJ+rb2VRZQ5vt1CWwUUZBeD/WGFAni12jD1HlMXvJ8JS7aBWTg==
-  dependencies:
-    workbox-core "6.6.1"
-
[email protected]:
-  version "6.6.1"
-  resolved "https://registry.npmmirror.com/workbox-strategies/-/workbox-strategies-6.6.1.tgz#38d0f0fbdddba97bd92e0c6418d0b1a2ccd5b8bf"
-  integrity sha512-WQLXkRnsk4L81fVPkkgon1rZNxnpdO5LsO+ws7tYBC6QQQFJVI6v98klrJEjFtZwzw/mB/HT5yVp7CcX0O+mrw==
-  dependencies:
-    workbox-core "6.6.1"
-
[email protected]:
-  version "6.6.1"
-  resolved "https://registry.npmmirror.com/workbox-streams/-/workbox-streams-6.6.1.tgz#b2f7ba7b315c27a6e3a96a476593f99c5d227d26"
-  integrity sha512-maKG65FUq9e4BLotSKWSTzeF0sgctQdYyTMq529piEN24Dlu9b6WhrAfRpHdCncRS89Zi2QVpW5V33NX8PgH3Q==
-  dependencies:
-    workbox-core "6.6.1"
-    workbox-routing "6.6.1"
-
[email protected]:
-  version "6.6.1"
-  resolved "https://registry.npmmirror.com/workbox-sw/-/workbox-sw-6.6.1.tgz#d4c4ca3125088e8b9fd7a748ed537fa0247bd72c"
-  integrity sha512-R7whwjvU2abHH/lR6kQTTXLHDFU2izht9kJOvBRYK65FbwutT4VvnUAJIgHvfWZ/fokrOPhfoWYoPCMpSgUKHQ==
-
-workbox-webpack-plugin@^6.1.0:
-  version "6.6.1"
-  resolved "https://registry.npmmirror.com/workbox-webpack-plugin/-/workbox-webpack-plugin-6.6.1.tgz#4f81cc1ad4e5d2cd7477a86ba83c84ee2d187531"
-  integrity sha512-zpZ+ExFj9NmiI66cFEApyjk7hGsfJ1YMOaLXGXBoZf0v7Iu6hL0ZBe+83mnDq3YYWAfA3fnyFejritjOHkFcrA==
-  dependencies:
-    fast-json-stable-stringify "^2.1.0"
-    pretty-bytes "^5.4.1"
-    upath "^1.2.0"
-    webpack-sources "^1.4.3"
-    workbox-build "6.6.1"
-
[email protected]:
-  version "6.6.1"
-  resolved "https://registry.npmmirror.com/workbox-window/-/workbox-window-6.6.1.tgz#f22a394cbac36240d0dadcbdebc35f711bb7b89e"
-  integrity sha512-wil4nwOY58nTdCvif/KEZjQ2NP8uk3gGeRNy2jPBbzypU4BT4D9L8xiwbmDBpZlSgJd2xsT9FvSNU0gsxV51JQ==
-  dependencies:
-    "@types/trusted-types" "^2.0.2"
-    workbox-core "6.6.1"
-
 wrap-ansi@^3.0.1:
   version "3.0.1"
   resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-3.0.1.tgz#288a04d87eda5c286e060dfe8f135ce8d007f8ba"