xiongzhu 3 anni fa
parent
commit
f54e9c7419
14 ha cambiato i file con 645 aggiunte e 35 eliminazioni
  1. 1 0
      package.json
  2. 55 1
      src/App.vue
  3. BIN
      src/assets/flash.png
  4. BIN
      src/assets/switch_camera.png
  5. 1 5
      src/index.less
  6. 6 6
      src/main.js
  7. 12 1
      src/router/index.js
  8. 59 0
      src/views/About.vue
  9. 64 2
      src/views/Detail.vue
  10. 108 0
      src/views/Detail1.vue
  11. 105 20
      src/views/Home.vue
  12. 218 0
      src/views/Scan.vue
  13. 4 0
      vue.config.js
  14. 12 0
      yarn.lock

+ 1 - 0
package.json

@@ -7,6 +7,7 @@
     "build": "vue-cli-service build"
   },
   "dependencies": {
+    "axios": "^0.25.0",
     "core-js": "^3.6.5",
     "vant": "^2.12.39",
     "vue": "^2.6.11",

+ 55 - 1
src/App.vue

@@ -1,6 +1,10 @@
 <template>
     <div id="app">
-        <router-view v-transition />
+        <transition :name="transitionName">
+            <keep-alive>
+                <router-view class="router-view" />
+            </keep-alive>
+        </transition>
         <van-tabbar
             fixed
             :safe-area-inset-bottom="true"
@@ -39,8 +43,22 @@ export default {
             transitionName: "slide-left",
         };
     },
+    created() {
+        this.showTabbar = ["Home", "About"].includes(this.$route.name);
+    },
     watch: {
         $route(to, from) {
+            if (
+                ["Home", "About"].includes(to.name) &&
+                ["Home", "About"].includes(from.name)
+            ) {
+                this.transitionName = "";
+                return;
+            }
+            const toDepth = to.path.split("/").length;
+            const fromDepth = from.path.split("/").length;
+            this.transitionName =
+                toDepth < fromDepth ? "slide-right" : "slide-left";
             this.showTabbar = ["Home", "About"].includes(to.name);
         },
     },
@@ -51,4 +69,40 @@ export default {
     display: none;
     width: 0;
 }
+.router-view {
+    background: #f2f4f5;
+    width: 100vw;
+    height: 100vh;
+    overflow-y: auto;
+    position: absolute;
+    top: 0;
+    left: 0;
+}
+.slide-left-enter {
+    transform: translateX(100vw);
+}
+.slide-left-leave-to {
+    transform: translateX(-50vw);
+}
+.slide-left-enter-active,
+.slide-left-leave-active {
+    transition: all 0.45s cubic-bezier(0.29, 1.03, 0.65, 1);
+}
+
+.slide-right-enter {
+    transform: translateX(-50vw);
+}
+.slide-right-leave-to {
+    transform: translateX(100vw);
+}
+.slide-right-enter-active,
+.slide-right-leave-active {
+    transition: all 0.45s cubic-bezier(0.29, 1.03, 0.65, 1);
+}
+.slide-right-enter-active {
+    z-index: 1;
+}
+.slide-right-leave-active {
+    z-index: 2;
+}
 </style>

BIN
src/assets/flash.png


BIN
src/assets/switch_camera.png


+ 1 - 5
src/index.less

@@ -7,13 +7,11 @@ html {
     -moz-osx-font-smoothing: grayscale;
     margin: 0;
     padding: 0;
-    background: #f2f4f5;
 }
 
 body {
     width: 100%;
     height: 100%;
-    background: #f2f4f5;
     font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
         "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
     -webkit-font-smoothing: antialiased;
@@ -24,7 +22,6 @@ body {
 #app {
     width: 100%;
     height: 100%;
-    background: #f2f4f5;
 }
 a {
     text-decoration: none;
@@ -34,7 +31,6 @@ a img {
     border: 0;
 }
 
-input,
 textarea,
 button {
     border: none;
@@ -68,6 +64,6 @@ li {
     padding-bottom: env(safe-area-inset-top);
 }
 
-* {
+* :not(input) {
     user-select: none;
 }

+ 6 - 6
src/main.js

@@ -2,14 +2,14 @@ import Vue from "vue";
 import App from "./App.vue";
 import router from "./router";
 import Vant from "vant";
-import vueg from "vueg";
+// import vueg from "vueg";
 import "vant/lib/index.less";
 import "./index.less";
-Vue.use(vueg, router, {
-    disableAtSameDepths: true,
-    shadow: false,
-    disable: ["Home", "About"],
-});
+// Vue.use(vueg, router, {
+//     disableAtSameDepths: true,
+//     shadow: false,
+//     disable: ["Home", "About"],
+// });
 Vue.use(Vant);
 Vue.config.productionTip = false;
 

+ 12 - 1
src/router/index.js

@@ -3,7 +3,8 @@ import VueRouter from "vue-router";
 import Home from "../views/Home.vue";
 import About from "../views/About.vue";
 import Detail from "../views/Detail.vue";
-
+import Detail1 from "../views/Detail1.vue";
+import Scan from "../views/Scan.vue";
 Vue.use(VueRouter);
 
 const routes = [
@@ -26,6 +27,16 @@ const routes = [
         name: "Detail",
         component: Detail,
     },
+    {
+        path: "/home/detail1",
+        name: "Detail1",
+        component: Detail1,
+    },
+    {
+        path: "/home/scan",
+        name: "Scan",
+        component: Scan,
+    },
 ];
 
 const router = new VueRouter({

+ 59 - 0
src/views/About.vue

@@ -10,9 +10,49 @@
                 绿洲宇宙于全球范围内持续性引入世界级IP,建立强大且完善的IP数据库:包含演艺明星、体育赛事、科技潮玩,潮流音乐,当红小说、知名艺术品及流量动漫在内的多种配置。
             </p>
         </div>
+        <a href="mailto:raex@raex.vip" class="cell">
+            <van-icon name="comment-o" />
+            <div class="text">联系我们</div>
+        </a>
+        <div class="cell" @click="share">
+            <van-icon name="share-o" />
+            <div class="text">分享给朋友</div>
+        </div>
         <div class="ph"></div>
     </div>
 </template>
+<script>
+export default {
+    methods: {
+        share() {
+            // this is the complete list of currently supported params you can pass to the plugin (all optional)
+            var options = {
+                message: "RAEX绿洲宇宙", // not supported on some apps (Facebook, Instagram)
+                subject: "RAEX绿洲宇宙", // fi. for email
+                files: ["https://www.raex.vip/static/img/app_icon_raex.png"], // an array of filenames either locally or remotely
+                url: "https://www.raex.vip/static/download_raex.html",
+            };
+
+            var onSuccess = (result) => {
+                this.$toast.clear();
+                console.log("Share completed? " + result.completed); // On Android apps mostly return false even while it's true
+                console.log("Shared to app: " + result.app); // On Android result.app since plugin version 5.4.0 this is no longer empty. On iOS it's empty when sharing is cancelled (result.completed=false)
+            };
+
+            var onError = (msg) => {
+                this.$toast.clear();
+                console.log("Sharing failed with message: " + msg);
+            };
+            this.$toast.loading({ duration: 0 });
+            window.plugins.socialsharing.shareWithOptions(
+                options,
+                onSuccess,
+                onError
+            );
+        },
+    },
+};
+</script>
 <style lang="less" scoped>
 .about {
     .title {
@@ -37,4 +77,23 @@
         flex-grow: 1;
     }
 }
+.cell {
+    .van-icon {
+        font-size: 20px;
+    }
+    .text {
+        margin-left: 10px;
+    }
+    color: #939599;
+    height: 54px;
+    margin-top: 15px;
+    margin-left: 15px;
+    margin-right: 15px;
+    border-radius: 16px;
+    background: white;
+    display: flex;
+    align-items: center;
+    font-size: 14px;
+    padding-left: 15px;
+}
 </style>

+ 64 - 2
src/views/Detail.vue

@@ -1,5 +1,9 @@
 <template>
-    <div>
+    <div class="detail">
+        <div class="list">
+            <div class="navbar-placeholder"></div>
+            <van-image :src="info.detail" class="detail-img" show-loading />
+        </div>
         <van-nav-bar
             :title="info.title"
             left-text="返回"
@@ -8,8 +12,11 @@
             fixed
             placeholder
             safe-area-inset-top
+            class="nav-bar"
         />
-        <van-image :src="info.detail" class="detail-img" show-loading />
+        <div @click="share" class="btn-share">
+            <van-icon name="share" />
+        </div>
     </div>
 </template>
 <script>
@@ -23,16 +30,71 @@ export default {
         console.log(this.$route.params);
         this.info = this.$route.params;
     },
+    activated() {
+        this.info = this.$route.params;
+    },
     methods: {
         onClickLeft() {
             this.$router.go(-1);
         },
+        share() {
+            // this is the complete list of currently supported params you can pass to the plugin (all optional)
+            var options = {
+                message: this.$route.params.title, // not supported on some apps (Facebook, Instagram)
+                subject: this.$route.params.title, // fi. for email
+                files: [this.$route.params.cover, this.$route.params.detail], // an array of filenames either locally or remotely
+                url: "https://www.raex.vip/static/download_raex.html",
+            };
+
+            var onSuccess = (result) => {
+                this.$toast.clear();
+                console.log("Share completed? " + result.completed); // On Android apps mostly return false even while it's true
+                console.log("Shared to app: " + result.app); // On Android result.app since plugin version 5.4.0 this is no longer empty. On iOS it's empty when sharing is cancelled (result.completed=false)
+            };
+
+            var onError = (msg) => {
+                this.$toast.clear();
+                console.log("Sharing failed with message: " + msg);
+            };
+            this.$toast.loading({ duration: 0 });
+            window.plugins.socialsharing.shareWithOptions(
+                options,
+                onSuccess,
+                onError
+            );
+        },
     },
 };
 </script>
 <style lang="less" scoped>
+.list {
+    height: 100%;
+    overflow: auto;
+}
+.detail {
+    background: #f2f4f5;
+}
 .detail-img {
     width: 100%;
     height: auto;
 }
+.navbar-placeholder {
+    height: 46px;
+    height: calc(46px + env(safe-area-inset-top));
+}
+.btn-share {
+    color: #3ab200;
+    position: absolute;
+    bottom: 70px;
+    bottom: calc(70px + env(safe-area-inset-bottom));
+    right: 20px;
+    font-size: 24px;
+    background: white;
+    border-radius: 50%;
+    width: 50px;
+    height: 50px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
 </style>

+ 108 - 0
src/views/Detail1.vue

@@ -0,0 +1,108 @@
+<template>
+    <div class="detail">
+        <div class="list">
+            <div class="navbar-placeholder"></div>
+            <van-image :src="info.pic[0].url" class="detail-img" show-loading />
+            <div class="content" v-html="info.detail"></div>
+        </div>
+        <van-nav-bar
+            :title="info.name"
+            left-text="返回"
+            left-arrow
+            @click-left="onClickLeft"
+            fixed
+            placeholder
+            safe-area-inset-top
+            class="nav-bar"
+        />
+        <div @click="share" class="btn-share">
+            <van-icon name="share" />
+        </div>
+    </div>
+</template>
+<script>
+export default {
+    data() {
+        return {
+            info: {},
+        };
+    },
+    created() {
+        console.log(this.$route.params);
+        this.info = this.$route.params;
+    },
+    activated() {
+        this.info = this.$route.params;
+    },
+    methods: {
+        onClickLeft() {
+            this.$router.go(-1);
+        },
+        share() {
+            // this is the complete list of currently supported params you can pass to the plugin (all optional)
+            var options = {
+                message: this.$route.params.name, // not supported on some apps (Facebook, Instagram)
+                subject: this.$route.params.name, // fi. for email
+                files: [this.$route.params.pic[0].url], // an array of filenames either locally or remotely
+                url: "https://www.raex.vip/static/download_raex.html",
+            };
+
+            var onSuccess = (result) => {
+                this.$toast.clear();
+                console.log("Share completed? " + result.completed); // On Android apps mostly return false even while it's true
+                console.log("Shared to app: " + result.app); // On Android result.app since plugin version 5.4.0 this is no longer empty. On iOS it's empty when sharing is cancelled (result.completed=false)
+            };
+
+            var onError = (msg) => {
+                this.$toast.clear();
+                console.log("Sharing failed with message: " + msg);
+            };
+            this.$toast.loading({ duration: 0 });
+            window.plugins.socialsharing.shareWithOptions(
+                options,
+                onSuccess,
+                onError
+            );
+        },
+    },
+};
+</script>
+<style lang="less" scoped>
+.list {
+    height: 100%;
+    overflow: auto;
+}
+.detail {
+    background: #f2f4f5;
+}
+.detail-img {
+    width: 100%;
+    height: auto;
+}
+.navbar-placeholder {
+    height: 46px;
+    height: calc(46px + env(safe-area-inset-top));
+}
+.btn-share {
+    color: #3ab200;
+    position: fixed;
+    bottom: 70px;
+    bottom: calc(70px + env(safe-area-inset-bottom));
+    right: 20px;
+    font-size: 24px;
+    background: white;
+    border-radius: 50%;
+    width: 50px;
+    height: 50px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+.content {
+    padding: 15px;
+    img {
+        width: 100%;
+        height: auto;
+    }
+}
+</style>

+ 105 - 20
src/views/Home.vue

@@ -1,29 +1,46 @@
 <template>
     <div class="home">
-        <div class="safe-top"></div>
-        <div class="item" v-for="item in list" @click="detail(item)">
-            <van-image
-                class="cover"
-                width="100%"
-                height="calc(100vw - 30px)"
-                fit="cover"
-                :src="item.cover + '?x-oss-process=image/resize,h_800,m_lfit'"
-            />
-            <div class="title">{{ item.title }}</div>
-            <div class="info">
+        <div class="list">
+            <div class="search-placeholder"></div>
+            <div class="item" v-for="item in filterList" @click="detail(item)">
                 <van-image
-                    class="avatar"
-                    width="20"
-                    height="20"
+                    class="cover"
+                    width="100%"
+                    height="calc(100vw - 30px)"
                     fit="cover"
                     :src="
-                        item.avatar + '?x-oss-process=image/resize,h_60,m_lfit'
+                        item.cover + '?x-oss-process=image/resize,h_800,m_lfit'
                     "
-                ></van-image>
-                <div class="name">{{ item.name }}</div>
+                />
+                <div class="title">{{ item.title }}</div>
+                <div class="info">
+                    <van-image
+                        class="avatar"
+                        width="20"
+                        height="20"
+                        fit="cover"
+                        :src="
+                            item.avatar +
+                            '?x-oss-process=image/resize,h_60,m_lfit'
+                        "
+                    ></van-image>
+                    <div class="name">{{ item.name }}</div>
+                </div>
             </div>
+            <van-empty description="暂无结果" v-if="filterList.length === 0" />
+            <div class="tabbar-placeholder"></div>
+        </div>
+        <div class="search">
+            <div class="safe-top"></div>
+            <van-search
+                v-model="searchText"
+                shape="round"
+                placeholder="请输入搜索关键词"
+            />
+        </div>
+        <div @click="scan" class="btn-scan">
+            <van-icon name="scan" />
         </div>
-        <div style="height: 20px"></div>
     </div>
 </template>
 
@@ -33,6 +50,7 @@ export default {
     components: {},
     data() {
         return {
+            searchText: "",
             list: [
                 {
                     cover: "https://raex-meta.oss-cn-shenzhen.aliyuncs.com/nft/2021-12-31-01-09-31OIXJSttd.jpg",
@@ -72,6 +90,17 @@ export default {
             ],
         };
     },
+    computed: {
+        filterList() {
+            if (this.searchText) {
+                return this.list.filter((i) =>
+                    i.title.includes(this.searchText)
+                );
+            } else {
+                return [...this.list];
+            }
+        },
+    },
     methods: {
         detail(item) {
             this.$router.push({
@@ -79,12 +108,38 @@ export default {
                 params: item,
             });
         },
+        scan() {
+            if (window.cordova) {
+                QRScanner.prepare((err, status) => {
+                    console.log(err, status);
+                    if (err) {
+                        this.$dialog
+                            .confirm({
+                                title: "无相机权限",
+                                message: "需要相机权限来扫码",
+                                confirmButtonText: "打开设置",
+                            })
+                            .then(() => {
+                                QRScanner.openSettings();
+                            })
+                            .catch(() => {
+                                // on cancel
+                            });
+                    } else {
+                        this.$router.push({ name: "Scan" });
+                    }
+                });
+            } else {
+                this.$router.push({ name: "Scan" });
+            }
+        }
     },
 };
 </script>
 <style lang="less" scoped>
-.home {
-    padding-top: 20px;
+.list {
+    height: 100%;
+    overflow: auto;
 }
 .item {
     display: flex;
@@ -121,4 +176,34 @@ export default {
         }
     }
 }
+.search {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    background: white;
+}
+.search-placeholder {
+    height: 74px;
+    height: calc(74px + env(safe-area-inset-top));
+}
+.tabbar-placeholder {
+    height: 50px;
+    height: calc(50px + env(safe-area-inset-top));
+}
+
+.btn-scan {
+    position: absolute;
+    bottom: 70px;
+    bottom: calc(70px + env(safe-area-inset-bottom));
+    right: 20px;
+    font-size: 24px;
+    background: white;
+    border-radius: 50%;
+    width: 50px;
+    height: 50px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
 </style>

+ 218 - 0
src/views/Scan.vue

@@ -0,0 +1,218 @@
+<template>
+    <div
+        class="scan"
+        v-on:enter="enter"
+        :style="{ background: active ? 'transparent' : '#343434' }"
+    >
+        <div class="btn-close" @click="back">
+            <van-icon name="cross" />
+        </div>
+        <div class="scan-icon">
+            <div class="c1"></div>
+            <div class="c2"></div>
+            <div class="c3"></div>
+            <div class="c4"></div>
+        </div>
+        <div class="btns">
+            <div
+                class="btn flash"
+                @click="flash"
+                v-if="status.canEnableLight === true"
+            >
+                <img src="../assets/flash.png" />
+            </div>
+            <div
+                class="btn camera"
+                @click="camera"
+                v-if="status.canChangeCamera === true"
+            >
+                <img src="../assets/switch_camera.png" />
+            </div>
+        </div>
+    </div>
+</template>
+<script>
+import axios from "axios";
+export default {
+    data() {
+        return {
+            active: false,
+            status: {},
+            light: false,
+        };
+    },
+    mounted() {},
+    activated() {
+        this.start();
+    },
+    beforeDestroy() {
+        this.end();
+    },
+    deactivated() {
+        this.end();
+    },
+    methods: {
+        start() {
+            if (window.cordova) {
+                StatusBar.styleBlackOpaque();
+            }
+            setTimeout(() => {
+                if (window.cordova) {
+                    this.active = true;
+                    QRScanner.scan((err, text) => {
+                        console.log(text);
+                        if (/productDetail\?id\=\d+/.test(text)) {
+                            let url = new URL(text);
+                            let id = url.searchParams.get("id");
+                            this.$toast.loading();
+                            axios
+                                .get(url.origin + "/collection/get/" + id)
+                                .then((res) => {
+                                    console.log(res);
+                                    this.$toast.clear();
+                                    this.$router.replace({
+                                        name: "Detail1",
+                                        params: res.data,
+                                    });
+                                });
+                        } else {
+                            this.back();
+                        }
+                    });
+                    QRScanner.show((status) => {
+                        this.status = status;
+                        console.log(status);
+                    });
+                    QRScanner.resumePreview((status) => {
+                        console.log(status);
+                    });
+                }
+            }, 450);
+        },
+        end() {
+            this.active = false;
+            if (window.cordova) {
+                StatusBar.styleDefault();
+                QRScanner.destroy(function (status) {
+                    console.log(status);
+                });
+            }
+        },
+        back() {
+            this.$router.go(-1);
+        },
+        enter() {
+            console.log("enter");
+        },
+        flash() {
+            if (this.status.lightEnabled) {
+                QRScanner.disableLight((err, status) => {
+                    err && console.error(err);
+                    console.log(status);
+                    this.status = status;
+                });
+            } else {
+                QRScanner.enableLight((err, status) => {
+                    err && console.error(err);
+                    console.log(status);
+                    this.status = status;
+                });
+            }
+        },
+        camera() {
+            if (this.status.currentCamera === 0) {
+                QRScanner.useFrontCamera((err, status) => {
+                    err && console.error(err);
+                    console.log(status);
+                    this.status = status;
+                });
+            } else {
+                QRScanner.useBackCamera((err, status) => {
+                    err && console.error(err);
+                    console.log(status);
+                    this.status = status;
+                });
+            }
+        },
+    },
+};
+</script>
+<style lang="less" scoped>
+.scan {
+    transition: translateX 0.45s background 0.15s;
+}
+.btn-close {
+    position: absolute;
+    top: 0;
+    top: env(safe-area-inset-top);
+    color: white;
+    padding: 10px 20px;
+    font-size: 26px;
+}
+.scan-icon {
+    width: 250px;
+    height: 250px;
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    margin: auto;
+    .c1,
+    .c2,
+    .c3,
+    .c4 {
+        position: absolute;
+        width: 50px;
+        height: 50px;
+    }
+    .c1 {
+        left: 0;
+        top: 0;
+        border-left: 8px solid rgba(255, 255, 255, 0.3);
+        border-top: 8px solid rgba(255, 255, 255, 0.3);
+    }
+    .c2 {
+        left: 0;
+        bottom: 0;
+        border-left: 8px solid rgba(255, 255, 255, 0.3);
+        border-bottom: 8px solid rgba(255, 255, 255, 0.3);
+    }
+    .c3 {
+        right: 0;
+        top: 0;
+        border-right: 8px solid rgba(255, 255, 255, 0.3);
+        border-top: 8px solid rgba(255, 255, 255, 0.3);
+    }
+    .c4 {
+        right: 0;
+        bottom: 0;
+        border-right: 8px solid rgba(255, 255, 255, 0.3);
+        border-bottom: 8px solid rgba(255, 255, 255, 0.3);
+    }
+}
+.btns {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    position: absolute;
+    bottom: 100px;
+    left: 0;
+    right: 0;
+    .btn {
+        width: 50px;
+        height: 50px;
+        background: rgba(0, 0, 0, 0.8);
+        border-radius: 50%;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        margin: 0 20px;
+        img {
+            width: 40%;
+            height: 40%;
+            object-fit: contain;
+        }
+    }
+}
+</style>

+ 4 - 0
vue.config.js

@@ -11,4 +11,8 @@ module.exports = {
             },
         },
     },
+    chainWebpack: (config) => {
+        config.plugins.delete("preload");
+        config.plugins.delete("prefetch");
+    },
 };

+ 12 - 0
yarn.lock

@@ -1844,6 +1844,13 @@ aws4@^1.8.0:
   resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59"
   integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==
 
+axios@^0.25.0:
+  version "0.25.0"
+  resolved "https://registry.npmmirror.com/axios/-/axios-0.25.0.tgz#349cfbb31331a9b4453190791760a8d35b093e0a"
+  integrity sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==
+  dependencies:
+    follow-redirects "^1.14.7"
+
 babel-helper-vue-jsx-merge-props@^2.0.2:
   version "2.0.3"
   resolved "https://registry.nlark.com/babel-helper-vue-jsx-merge-props/download/babel-helper-vue-jsx-merge-props-2.0.3.tgz#22aebd3b33902328e513293a8e4992b384f9f1b6"
@@ -3702,6 +3709,11 @@ follow-redirects@^1.0.0:
   resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685"
   integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==
 
+follow-redirects@^1.14.7:
+  version "1.14.8"
+  resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc"
+  integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==
+
 for-in@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"