xiongzhu 4 yıl önce
ebeveyn
işleme
21b738dba3

+ 4 - 1
package.json

@@ -9,13 +9,16 @@
   },
   "dependencies": {
     "axios": "^0.27.2",
+    "date-fns": "^2.28.0",
     "element-plus": "^2.2.5",
     "feather-icons": "^4.29.0",
     "pinia": "^2.0.14",
     "qs": "^6.10.5",
     "vue": "^3.2.36",
     "vue-feather": "^2.0.0",
-    "vue-router": "^4.0.15"
+    "vue-lazyload": "^3.0.0-rc.2",
+    "vue-router": "^4.0.15",
+    "vue-virtualised": "^0.1.8"
   },
   "devDependencies": {
     "@rushstack/eslint-patch": "^1.1.0",

+ 2 - 0
src/App.vue

@@ -13,8 +13,10 @@ html {
     font-synthesis: none;
     text-rendering: optimizeLegibility;
     -webkit-font-smoothing: antialiased;
+    background-color: var(--el-bg-color);
 }
 body {
     margin: 0;
+    background-color: var(--el-bg-color);
 }
 </style>

+ 68 - 0
src/components/AssetItem.vue

@@ -0,0 +1,68 @@
+<template>
+    <div class="asset">
+        <img :src="info.pic[0].thumb || info.pic[0].url" class="cover" />
+        <div class="name">{{ name }}OASISPUNK.EVOOASISPUNK.EVOOASISPUNK.EVO</div>
+        <div class="number">{{ number ? '#' : '' }}{{ number }}</div>
+        <div class="price" v-if="info.consignment">¥{{ info.price }}</div>
+    </div>
+</template>
+<script setup>
+import { ref, defineProps, watchEffect } from 'vue'
+const props = defineProps({
+    info: {
+        type: Object,
+        required: true
+    }
+})
+const info = props.info || {}
+const name = ref(info.name)
+const number = ref(info.number)
+watchEffect(() => {
+    const m = /^(.*?)\s*(#\d+)$/.exec(name.value)
+    if (m) {
+        name.value = m[1]
+        number.value = m[2].replace('#', '')
+    }
+})
+</script>
+<style lang="less" scoped>
+.asset {
+    width: 130px;
+    height: 200px;
+    border-radius: 8px;
+    overflow: hidden;
+    margin: 0 20px 20px 0;
+    background-color: var(--el-fill-color);
+    display: flex;
+    flex-direction: column;
+    font-size: 12px;
+    .cover {
+        width: 130px;
+        height: 130px;
+        object-fit: cover;
+    }
+    .name {
+        color: var(--el-text-color);
+        padding: 0 8px;
+        margin-top: 4px;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+        word-wrap: normal;
+    }
+    .number {
+        padding: 0 8px;
+        margin-top: 4px;
+    }
+    .price {
+        flex-grow: 1;
+        display: flex;
+        align-items: flex-end;
+        color: var(--el-color-danger);
+        font-weight: 500;
+        justify-content: flex-end;
+        padding: 0 8px;
+        margin: 8px 0;
+    }
+}
+</style>

+ 87 - 0
src/components/ListItem.vue

@@ -0,0 +1,87 @@
+<template>
+    <div class="list-item">
+        <img :src="info.pic[0].thumb || info.pic[0].url" class="cover" />
+        <div class="info">
+            <div class="row">
+                <div class="name">{{ info.name }}</div>
+                <div class="price">¥{{ info.price }}</div>
+            </div>
+            <div class="row">
+                <div class="number">{{ info.number ? '#' : '' }}{{ info.number }}</div>
+                <div class="time">{{ timeAgo(info.createdAt) }}</div>
+            </div>
+        </div>
+    </div>
+</template>
+<script setup>
+import { defineProps } from 'vue'
+import { formatDistance, subDays, parse } from 'date-fns'
+import { zhCN } from 'date-fns/locale'
+const props = defineProps({
+    info: {
+        type: Object,
+        required: true
+    }
+})
+const info = props.info || {}
+const timeAgo = date => {
+    return formatDistance(subDays(new Date(), 1), parse(date, 'yyyy-MM-dd HH:mm:ss', new Date()), { locale: zhCN })
+}
+</script>
+<style lang="less" scoped>
+.list-item {
+    display: flex;
+    align-items: center;
+    height: 52px;
+    border-radius: 8px;
+    overflow: hidden;
+    background: var(--el-fill-color);
+    margin-bottom: 10px;
+    .cover {
+        width: 44px;
+        height: 44px;
+        margin-left: 4px;
+        object-fit: cover;
+        border-radius: 4px;
+    }
+    .info {
+        flex-basis: 0;
+        flex-grow: 1;
+        font-size: 13px;
+        display: flex;
+        flex-direction: column;
+        margin-left: 10px;
+        margin-right: 10px;
+        justify-content: center;
+        .row {
+            display: flex;
+            align-items: center;
+            &:last-child {
+                margin-top: 2px;
+            }
+            .name {
+                flex-basis: 0;
+                flex-grow: 1;
+                width: 1px;
+                overflow: hidden;
+                text-overflow: ellipsis;
+                white-space: nowrap;
+                word-wrap: normal;
+            }
+            .price {
+                color: var(--el-color-danger);
+                font-size: 14px;
+                font-weight: 500;
+            }
+            .number {
+                flex-basis: 0;
+                flex-grow: 1;
+            }
+            .time {
+                font-size: 12px;
+                color: var(--el-text-color-secondary);
+            }
+        }
+    }
+}
+</style>

+ 6 - 0
src/main.js

@@ -6,12 +6,18 @@ import ElementPlus from 'element-plus'
 import 'element-plus/dist/index.css'
 import 'element-plus/theme-chalk/dark/css-vars.css'
 import http from '@/plugins/http'
+import * as ElementPlusIconsVue from '@element-plus/icons-vue'
+import VueLazyload from 'vue-lazyload'
 
 const app = createApp(App)
 
 app.use(createPinia())
 app.use(router)
 app.use(ElementPlus)
+for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
+    app.component(key, component)
+}
 app.use(http)
+app.use(VueLazyload)
 
 app.mount('#app')

+ 45 - 22
src/plugins/http.js

@@ -31,30 +31,53 @@ axiosInstance.interceptors.response.use(
     }
 )
 
-export default {
-    install: app => {
-        app.config.globalProperties.$http = {
-            setToken(token) {
-                axiosInstance.defaults.headers.common['Authorization'] = 'Bearer ' + token
-                sessionStorage.setItem('token', token)
-            },
-            async login(phone, password) {
-                let { data: token } = await axiosInstance.post('/auth/phonePwdLogin', qs.stringify({ phone, password }))
-                this.setToken(token)
-            },
-            get(url, params) {
-                return new Promise((resolve, reject) => {
-                    axiosInstance
-                        .get(url, { params, withCredentials: true })
-                        .then(res => {
-                            resolve(res.data)
-                        })
-                        .catch(e => {
-                            reject(e)
-                        })
+const http = {
+    setToken(token) {
+        axiosInstance.defaults.headers.common['Authorization'] = 'Bearer ' + token
+        sessionStorage.setItem('token', token)
+    },
+    async login(phone, password) {
+        let { data: token } = await axiosInstance.post('/auth/phonePwdLogin', qs.stringify({ phone, password }))
+        this.setToken(token)
+    },
+    get(url, params) {
+        return new Promise((resolve, reject) => {
+            axiosInstance
+                .get(url, { params, withCredentials: true })
+                .then(res => {
+                    resolve(res.data)
                 })
+                .catch(e => {
+                    reject(e)
+                })
+        })
+    },
+    post(url, body, options) {
+        options = options || {}
+        body = body || {}
+        if (!(body instanceof FormData)) {
+            if (options.body !== 'json') {
+                body = qs.stringify(body)
             }
         }
-        app.provide('http', app.config.globalProperties.$http)
+        return new Promise((resolve, reject) => {
+            axiosInstance
+                .post(url, body, { withCredentials: true })
+                .then(res => {
+                    resolve(res.data)
+                })
+                .catch(e => {
+                    reject(e)
+                })
+        })
     }
 }
+
+export default {
+    install: app => {
+        app.config.globalProperties.$http = http
+        app.provide('http', app.config.globalProperties.$http)
+    },
+    http
+}
+export { http }

+ 24 - 0
src/router/index.js

@@ -1,5 +1,7 @@
 import { createRouter, createWebHistory } from 'vue-router'
 import HomeView from '../views/HomeView.vue'
+import { store } from '@/stores/store'
+import { http } from '@/plugins/http'
 
 const router = createRouter({
     history: createWebHistory(import.meta.env.BASE_URL),
@@ -17,4 +19,26 @@ const router = createRouter({
     ]
 })
 
+router.beforeEach(async (to, from, next) => {
+    if (!store.userInfo && sessionStorage.getItem('token')) {
+        try {
+            store.userInfo = await http.get('/user/my')
+        } catch (e) {
+            // eslint-disable-next-line no-empty
+        }
+    }
+    if (to.name !== 'login') {
+        if (!store.userInfo) {
+            next({ name: 'login' })
+            return
+        }
+    } else {
+        if (store.userInfo) {
+            next({ path: '/' })
+            return
+        }
+    }
+    next()
+})
+
 export default router

+ 14 - 0
src/stores/store.js

@@ -0,0 +1,14 @@
+import { defineStore } from 'pinia'
+
+export const store = defineStore({
+    id: 'store',
+    state: () => ({
+        userInfo: null
+    }),
+    getters: {},
+    actions: {
+        setUserInfo(userInfo) {
+            this.userInfo = userInfo
+        }
+    }
+})

+ 167 - 17
src/views/HomeView.vue

@@ -1,46 +1,154 @@
 <template>
     <el-container>
         <el-header class="app-header">
+            <el-select placeholder="选择" v-model="tagId" @change="onTagChange">
+                <el-option v-for="item in tags" :key="item.id" :label="item.name" :value="item.id"></el-option>
+            </el-select>
+            <div style="flex-grow: 1"></div>
             <dark-mode />
+            <vue-feather type="log-out" :size="16" class="logout-icon" title="退出"></vue-feather>
         </el-header>
         <el-main>
             <el-row :gutter="20" style="margin: 10px" class="card-headers">
-                <el-col :span="6">上架</el-col>
-                <el-col :span="6">交易</el-col>
-                <el-col :span="12">我的</el-col>
+                <el-col :span="6">
+                    <span class="head-title">上架</span>
+                    <el-icon :size="15" class="refresh-icon" v-if="listLoading"><RefreshRight /></el-icon>
+                    <div style="flex-grow: 1"></div>
+                    <el-select size="small" v-model="listSort">
+                        <el-option
+                            v-for="item in sortOptions"
+                            :key="item.value"
+                            :label="item.label"
+                            :value="item.value"
+                        ></el-option>
+                    </el-select>
+                </el-col>
+                <el-col :span="6">
+                    <span class="head-title">交易</span>
+                    <el-icon :size="15" class="refresh-icon" v-if="orderLoading"><RefreshRight /></el-icon>
+                    <div style="flex-grow: 1"></div>
+                    <el-select size="small" v-model="orderSort">
+                        <el-option
+                            v-for="item in sortOptions"
+                            :key="item.value"
+                            :label="item.label"
+                            :value="item.value"
+                        ></el-option>
+                    </el-select>
+                </el-col>
+                <el-col :span="12">
+                    <span class="head-title">持仓</span>
+                    <el-icon :size="15" class="refresh-icon" v-if="assetsLoading"><RefreshRight /></el-icon>
+                </el-col>
             </el-row>
             <el-row :gutter="20" style="margin: 10px">
-                <el-col :span="6"> <el-card shadow="hover"> Hover </el-card></el-col>
-                <el-col :span="6"> <el-card shadow="hover"> Hover </el-card></el-col>
-                <el-col :span="12"> <el-card shadow="hover"> Hover </el-card></el-col>
+                <el-col :span="6" key="list" class="list-container">
+                    <list-item :info="item" v-for="item in list" :key="item.id"></list-item>
+                </el-col>
+                <el-col :span="6" key="order" class="list-container">
+                    <list-item :info="item" v-for="item in order" :key="item.id"></list-item>
+                </el-col>
+                <el-col :span="12" class="list-container assets-container">
+                    <AssetItem v-for="item in assets" :key="item.id" :info="item"></AssetItem>
+                </el-col>
             </el-row>
         </el-main>
     </el-container>
 </template>
 
 <script setup>
+import VueFeather from 'vue-feather'
 import DarkMode from '@/components/DarkMode.vue'
 import Account from '@/domain/account'
-import { ElMessage } from 'element-plus'
-import { inject } from 'vue'
+import { ref } from 'vue'
 import { onBeforeRouteLeave } from 'vue-router'
+import { http } from '@/plugins/http'
+import ListItem from '@/components/ListItem.vue'
+import AssetItem from '@/components/AssetItem.vue'
+import { useStorage } from '@vueuse/core'
 
-const http = inject('http')
+const sortOptions = [
+    { label: '日期', value: 'createdAt,desc' },
+    { label: '价格', value: 'price' }
+]
+const tagId = useStorage('tagId', '')
+
+const tags = ref(null)
+http.post('/tag/all', { size: 10000 }, { body: 'json' }).then(res => {
+    tags.value = res.content
+})
 
 // const account1 = new Account('15077886171', 'x1ongDrew')
 const account2 = new Account('17601545948', '123456')
 // account1.login()
 account2.login()
 
-const init = async () => {
-    try {
-        await http.login('150778861711', 'x1ongDrew')
-        await http.get('/user/my')
-    } catch (e) {
-        ElMessage.error(e.error || '登录失败')
-    }
+const list = ref([])
+const listSort = ref(sortOptions[1].value)
+const listLoading = ref(false)
+const getList = () => {
+    listLoading.value = true
+    http.get('/collection/byTag', { tagId: tagId.value }).then(res => {
+        listLoading.value = false
+        res.content.forEach(i => {
+            const m = /^(.*?)\s*(#\d+)$/.exec(i.name)
+            if (m) {
+                i.name = m[1]
+                i.number = m[2].replace('#', '')
+            }
+        })
+        if (res.first) {
+            list.value = []
+        }
+        list.value = [...list.value, ...res.content]
+    })
+}
+
+const order = ref([])
+const orderSort = ref(sortOptions[0].value)
+const orderLoading = ref(false)
+const getOrder = () => {
+    orderLoading.value = true
+    http.get('/order/byTag', { tagId: tagId.value }).then(res => {
+        orderLoading.value = false
+        res.content.forEach(i => {
+            const m = /^(.*?)\s*(#\d+)$/.exec(i.name)
+            if (m) {
+                i.name = m[1]
+                i.number = m[2].replace('#', '')
+            }
+        })
+        if (res.first) {
+            order.value = []
+        }
+        order.value = [...order.value, ...res.content]
+    })
+}
+
+const assets = ref([])
+const assetsLoading = ref(false)
+const getAssets = () => {
+    http.post('/asset/all', { size: 100, query: { status: 'NORMAL' }, sort: 'createdAt,desc' }, { body: 'json' }).then(
+        res => {
+            if (res.first) {
+                assets.value = []
+            }
+            assets.value = [...assets.value, ...res.content]
+        }
+    )
+}
+
+const getData = () => {
+    if (!tagId.value) return
+    getList()
+    getOrder()
+    getAssets()
+}
+getData()
+
+const onTagChange = () => {
+    getData()
 }
-init()
 
 const timer = setInterval(() => {}, 3000)
 onBeforeRouteLeave(() => {
@@ -53,10 +161,52 @@ onBeforeRouteLeave(() => {
     border-bottom: 1px solid var(--el-border-color);
     display: flex;
     align-items: center;
+    padding: 0 40px;
+    .logout-icon {
+        margin-left: 20px;
+        cursor: pointer;
+    }
 }
 .card-headers {
     color: var(--el-text-color-primary);
     font-weight: 500;
     font-size: 14px;
+    .el-col {
+        display: flex;
+        align-items: center;
+    }
+    .head-title {
+    }
+    .refresh-icon {
+        margin-left: 6px;
+        animation: spin 1s linear infinite;
+        font-size: 14px;
+    }
+    .el-select {
+        width: 80px;
+    }
+}
+@keyframes spin {
+    from {
+        transform: rotate(0deg);
+    }
+    to {
+        transform: rotate(360deg);
+    }
+}
+.list-container {
+    height: calc(100vh - 160px);
+    max-height: calc(100vh - 160px);
+    overflow: auto;
+    &::-webkit-scrollbar {
+        display: none;
+        opacity: 0;
+    }
+    &.assets-container {
+        display: flex;
+        flex-wrap: wrap;
+        align-items: center;
+        padding-right: 0;
+    }
 }
 </style>

+ 78 - 2
src/views/LoginView.vue

@@ -1,2 +1,78 @@
-<template></template>
-<script lang="ts" setup></script>
+<template>
+    <el-container>
+        <el-header class="app-header">
+            <div style="flex-grow: 1"></div>
+            <dark-mode />
+        </el-header>
+        <el-main class="login">
+            <el-form
+                label-position="top"
+                :model="loginForm"
+                :rules="rules"
+                ref="form"
+                style="max-width: 400px; margin: auto"
+            >
+                <el-form-item label="用户名" prop="username">
+                    <el-input v-model="loginForm.username">
+                        <template #prefix>
+                            <el-icon><User /></el-icon>
+                        </template>
+                    </el-input>
+                </el-form-item>
+                <el-form-item label="密码" prop="password">
+                    <el-input v-model="loginForm.password" type="password" show-password>
+                        <template #prefix>
+                            <el-icon><Lock /></el-icon>
+                        </template>
+                    </el-input>
+                </el-form-item>
+                <el-form-item>
+                    <el-button type="primary" @click="login" :loading="loading" style="width: 100%; margin-top: 10px"
+                        >登录</el-button
+                    >
+                </el-form-item>
+            </el-form>
+        </el-main>
+    </el-container>
+</template>
+<script setup>
+import DarkMode from '@/components/DarkMode.vue'
+import { inject, ref } from 'vue'
+import { ElMessage } from 'element-plus'
+import router from '@/router/index'
+import { store } from '@/stores/store'
+const loginForm = ref({ username: '', password: '' })
+const form = ref(null)
+const rules = {
+    username: [{ required: true, message: '请输入用户名' }],
+    password: [{ required: true, message: '请输入密码' }]
+}
+const http = inject('http')
+const loading = ref(false)
+const login = () => {
+    form.value
+        .validate()
+        .then(async () => {
+            try {
+                loading.value = true
+                await http.login('15077886171', 'x1ongDrew')
+                store.userInfo = await http.get('/user/my')
+                loading.value = false
+                router.replace({ name: 'home' })
+            } catch (e) {
+                ElMessage.error(e.error || '登录失败')
+            }
+        })
+        .catch(() => {})
+}
+</script>
+<style lang="less" scoped>
+.app-header {
+    border-bottom: 1px solid var(--el-border-color);
+    display: flex;
+    align-items: center;
+    padding: 0 40px;
+}
+.login {
+}
+</style>

+ 109 - 1
yarn.lock

@@ -254,6 +254,11 @@ argparse@^2.0.1:
   resolved "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
   integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
 
+asap@~2.0.3:
+  version "2.0.6"
+  resolved "https://registry.npmmirror.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
+  integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==
+
 async-validator@^4.1.1:
   version "4.1.1"
   resolved "https://registry.npmmirror.com/async-validator/-/async-validator-4.1.1.tgz#3cd1437faa2de64743f7d56649dd904c946a18fe"
@@ -347,11 +352,18 @@ copy-anything@^2.0.1:
   dependencies:
     is-what "^3.14.1"
 
-core-js@^3.1.3:
+core-js@^3.1.3, core-js@^3.6.5:
   version "3.22.8"
   resolved "https://registry.npmmirror.com/core-js/-/core-js-3.22.8.tgz#23f860b1fe60797cc4f704d76c93fea8a2f60631"
   integrity sha512-UoGQ/cfzGYIuiq6Z7vWL1HfkE9U9IZ4Ub+0XSiJTCzvbZzgPA69oDF2f+lgJ6dFFLEdjW5O6svvoKzXX23xFkA==
 
+cross-fetch@^3.1.5:
+  version "3.1.5"
+  resolved "https://registry.npmmirror.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f"
+  integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==
+  dependencies:
+    node-fetch "2.6.7"
+
 cross-spawn@^7.0.2:
   version "7.0.3"
   resolved "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@@ -371,6 +383,11 @@ csstype@^2.6.8:
   resolved "https://registry.npmmirror.com/csstype/-/csstype-2.6.20.tgz#9229c65ea0b260cf4d3d997cb06288e36a8d6dda"
   integrity sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA==
 
+date-fns@^2.28.0:
+  version "2.28.0"
+  resolved "https://registry.npmmirror.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2"
+  integrity sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==
+
 dayjs@^1.11.3:
   version "1.11.3"
   resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.3.tgz#4754eb694a624057b9ad2224b67b15d552589258"
@@ -719,6 +736,24 @@ fast-levenshtein@^2.0.6:
   resolved "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
   integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
 
+fbjs-css-vars@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz#216551136ae02fe255932c3ec8775f18e2c078b8"
+  integrity sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==
+
+fbjs@^3.0.0:
+  version "3.0.4"
+  resolved "https://registry.npmmirror.com/fbjs/-/fbjs-3.0.4.tgz#e1871c6bd3083bac71ff2da868ad5067d37716c6"
+  integrity sha512-ucV0tDODnGV3JCnnkmoszb5lf4bNpzjv80K41wd4k798Etq+UYD0y0TIfalLjZoKgjive6/adkRnszwapiDgBQ==
+  dependencies:
+    cross-fetch "^3.1.5"
+    fbjs-css-vars "^1.0.0"
+    loose-envify "^1.0.0"
+    object-assign "^4.1.0"
+    promise "^7.1.1"
+    setimmediate "^1.0.5"
+    ua-parser-js "^0.7.30"
+
 feather-icons@^4.29.0:
   version "4.29.0"
   resolved "https://registry.npmmirror.com/feather-icons/-/feather-icons-4.29.0.tgz#4e40e3cbb7bf359ffbbf700edbebdde4e4a74ab6"
@@ -910,6 +945,11 @@ isexe@^2.0.0:
   resolved "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
   integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
 
+"js-tokens@^3.0.0 || ^4.0.0":
+  version "4.0.0"
+  resolved "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+  integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
 js-yaml@^4.1.0:
   version "4.1.0"
   resolved "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
@@ -984,6 +1024,13 @@ lodash@^4.17.21:
   resolved "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
 
+loose-envify@^1.0.0:
+  version "1.4.0"
+  resolved "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
+  integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
+  dependencies:
+    js-tokens "^3.0.0 || ^4.0.0"
+
 lru-cache@^6.0.0:
   version "6.0.0"
   resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
@@ -1064,6 +1111,13 @@ needle@^3.1.0:
     iconv-lite "^0.6.3"
     sax "^1.2.4"
 
+node-fetch@2.6.7:
+  version "2.6.7"
+  resolved "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
+  integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
+  dependencies:
+    whatwg-url "^5.0.0"
+
 normalize-wheel-es@^1.1.2:
   version "1.1.2"
   resolved "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.1.2.tgz#285e43676a62d687bf145e33452ea6be435162d0"
@@ -1076,6 +1130,11 @@ nth-check@^2.0.1:
   dependencies:
     boolbase "^1.0.0"
 
+object-assign@^4.1.0:
+  version "4.1.1"
+  resolved "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+  integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
+
 object-inspect@^1.9.0:
   version "1.12.2"
   resolved "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea"
@@ -1179,6 +1238,13 @@ prettier@^2.5.1, prettier@^2.6.2:
   resolved "https://registry.npmmirror.com/prettier/-/prettier-2.6.2.tgz#e26d71a18a74c3d0f0597f55f01fb6c06c206032"
   integrity sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew==
 
+promise@^7.1.1:
+  version "7.3.1"
+  resolved "https://registry.npmmirror.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
+  integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==
+  dependencies:
+    asap "~2.0.3"
+
 prr@~1.0.1:
   version "1.0.1"
   resolved "https://registry.npmmirror.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
@@ -1251,6 +1317,11 @@ semver@^7.3.5:
   dependencies:
     lru-cache "^6.0.0"
 
+setimmediate@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
+  integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==
+
 shebang-command@^2.0.0:
   version "2.0.0"
   resolved "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
@@ -1316,6 +1387,11 @@ text-table@^0.2.0:
   resolved "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
   integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
 
+tr46@~0.0.3:
+  version "0.0.3"
+  resolved "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
+  integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
+
 tslib@^2.3.0:
   version "2.4.0"
   resolved "https://registry.npmmirror.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
@@ -1333,6 +1409,11 @@ type-fest@^0.20.2:
   resolved "https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
   integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
 
+ua-parser-js@^0.7.30:
+  version "0.7.31"
+  resolved "https://registry.npmmirror.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6"
+  integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==
+
 uri-js@^4.2.2:
   version "4.4.1"
   resolved "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
@@ -1390,6 +1471,11 @@ vue-feather@^2.0.0:
   resolved "https://registry.npmmirror.com/vue-feather/-/vue-feather-2.0.0.tgz#5bcae9908ab7b3ab3d1e821f8fd677f49830e4b4"
   integrity sha512-GBvxJWu2ycGTpB8duYWnc5S/TwWPPb2G5Ft2NbkwK1vZkUDUOTYqIb4Nh1HOL6A37Isfrd0Guun0lesS97PfxA==
 
+vue-lazyload@^3.0.0-rc.2:
+  version "3.0.0-rc.2"
+  resolved "https://registry.npmmirror.com/vue-lazyload/-/vue-lazyload-3.0.0-rc.2.tgz#8a3daa598dcbb7349893d3bb39b50695049fb0d8"
+  integrity sha512-Cg7Gqb7jAoiImMPH3EMfAwJ8gZuFoBJCEtA7+TGRljrtktDtV7JAHIyKvuTgQR8DFAd7MhKi1Ys8QHdSrnGdZQ==
+
 vue-router@^4.0.15:
   version "4.0.16"
   resolved "https://registry.npmmirror.com/vue-router/-/vue-router-4.0.16.tgz#9477beeeef36e80e04d041a1738801a55e6e862e"
@@ -1397,6 +1483,15 @@ vue-router@^4.0.15:
   dependencies:
     "@vue/devtools-api" "^6.0.0"
 
+vue-virtualised@^0.1.8:
+  version "0.1.8"
+  resolved "https://registry.npmmirror.com/vue-virtualised/-/vue-virtualised-0.1.8.tgz#8209fb7fcefaa6ead9d4e011472c7690378efee0"
+  integrity sha512-Z7mqq5Q/0PpSukmbVUx8y/PPmzk2rWUlWIm1nHfeBZ3q5VLf/qXc5zZyaJQ+7OAYcYejpnD8d+i7IPxf5qzt5g==
+  dependencies:
+    core-js "^3.6.5"
+    fbjs "^3.0.0"
+    lodash "^4.17.21"
+
 vue@^3.2.36:
   version "3.2.37"
   resolved "https://registry.npmmirror.com/vue/-/vue-3.2.37.tgz#da220ccb618d78579d25b06c7c21498ca4e5452e"
@@ -1408,6 +1503,19 @@ vue@^3.2.36:
     "@vue/server-renderer" "3.2.37"
     "@vue/shared" "3.2.37"
 
+webidl-conversions@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
+  integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
+
+whatwg-url@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
+  integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
+  dependencies:
+    tr46 "~0.0.3"
+    webidl-conversions "^3.0.0"
+
 which@^2.0.1:
   version "2.0.2"
   resolved "https://registry.npmmirror.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"