nieyuge vor 2 Jahren
Ursprung
Commit
a6ec6e5830

+ 1 - 0
package.json

@@ -12,6 +12,7 @@
   },
   "dependencies": {
     "@vue/composition-api": "^1.7.0",
+    "ant-design-vue": "^3.2.11",
     "axios": "^0.27.2",
     "core-js": "^3.8.3",
     "element-plus": "2.1.10",

+ 234 - 0
src/components/v-head.vue

@@ -0,0 +1,234 @@
+<template>
+    <div class="head" :class="{ 'border': show_state != 'home', 'home': show_state == 'home' }">
+        <template v-if="show_state == 'home'">
+            <div class="desc">
+                <img class="img" :src="user_info.avatarUrl" />
+                <font class="name">{{user_info.nickName}}</font>
+            </div>
+            <div class="logo">
+                <img :src="require('@/assets/svg/icon-denet-logo.svg')" />
+            </div>
+        </template>
+        <template v-else>
+            <img :src="require('@/assets/svg/icon-back.svg')" alt="" class="back" @click="clickBack">
+            <div class="title">{{ props.title }}</div>
+            <img :src="require('@/assets/svg/icon-back-head-list.svg')"
+                v-if="show_list" 
+                class="list" 
+                @click="clickList" />
+            <img :src="require('@/assets/svg/icon-refresh.svg')" alt="" class="refresh" v-if="show_refresh"
+                @click="clickRefresh" :class="{ transform_rotate: state.rotate }">
+            <img :src="require('@/assets/svg/icon-head-help.svg')" alt="" class="help" v-if="props.show_help"
+                @click="clickHelp">
+            <img :src="require('@/assets/svg/icon-more-l.svg')" alt="" class="more" v-if="props.show_more"
+                @click="state.show_option = true">
+            <div class="area-option" v-if="state.show_option" @click="state.show_option = false">
+                <div class="option">
+                    <div class="item" @click="clickItem('/transactions')">
+                        <img :src="require('@/assets/svg/icon-menu.svg')" alt="">
+                        <span>Transactions History</span>
+                    </div>
+                </div>
+            </div>
+        </template>
+    </div>
+</template>
+<script setup>
+import { defineProps, defineEmits, reactive, ref } from "vue";
+import { useRouter } from "vue-router";
+
+let props = defineProps({
+    title: String,
+    show_state: String,
+    show_refresh: Boolean,
+    show_option: Boolean,
+    show_more: Boolean,
+    show_help: Boolean,
+    back_url: String,
+    user_info: Object,
+    show_list: Boolean,
+    transactionsRouterParams: Object,
+})
+
+let state = reactive({
+    show_option: ref(props.show_option),
+    rotate: false
+})
+
+const emit = defineEmits(['on-refresh'])
+
+const router = useRouter()
+
+function clickBack() {
+    if (props.back_url) {
+        router.replace(props.back_url)
+    } else {
+        router.back()
+    }
+
+}
+
+function clickRefresh() {
+    if (state.rotate) {
+        return
+    }
+    state.rotate = true
+    emit('on-refresh')
+    setTimeout(() => {
+        state.rotate = false
+    }, 1000)
+}
+
+function clickItem(path) {
+    let params = props.transactionsRouterParams || {};
+    router.push({
+        path: path,
+        query: {
+            params: JSON.stringify(params)
+        }
+    })
+}
+
+function clickHelp() {
+    window.open(`https://aboard-cattle-610.notion.site/How-to-withdraw-assets-from-DeNet-to-MetaMask-01c679bb9ff441429e31e8f7c1f67411`)
+}
+
+function clickList() {
+    let params = props.transactionsRouterParams || {};
+    console.log('transactionsRouterParams',params);
+    router.push({
+        path: '/transactions',
+        query: {
+            params: JSON.stringify(params)
+        }
+    })
+}
+
+</script>
+<style lang="scss" scoped>
+.border {
+    border-bottom: 1px solid #DBDBDB;
+}
+
+.head {
+    height: 48px;
+    display: flex;
+    flex-wrap: nowrap;
+    justify-content: space-between;
+    align-items: center;
+    padding: 0 12px;
+    overflow: hidden;
+
+    &.home {
+        height: 64px;
+    }
+
+    .logo {
+        display: flex;
+        align-items: center;
+        img {
+            width: 26px;
+            height: 26px;
+        }
+    }
+
+    .desc {
+        .img {
+            width: 34px;
+            height: 34px;
+            overflow: hidden;
+            border-radius: 50%;
+            margin-right: 10px;
+        }
+        .name {
+            display: inline-block;
+            width: 200px;
+            color: #000000;
+            font-size: 16px;
+            font-weight: bold;
+        }
+    }
+
+    .area-option {
+        width: 100%;
+        height: 100%;
+        position: absolute;
+        top: 0;
+        left: 0;
+        z-index: 111;
+
+        .option {
+            position: absolute;
+            top: 43px;
+            right: 15px;
+            background: #fff;
+            filter: drop-shadow(0px 3px 20px rgba(0, 0, 0, 0.2));
+            width: 240px;
+            border-radius: 15px;
+            overflow: hidden;
+
+            .item {
+                width: 100%;
+                height: 50px;
+                display: flex;
+                align-items: center;
+                cursor: pointer;
+                border-top: 1px solid #E9E9E9;
+
+                img {
+                    margin-left: 15px;
+                    width: 30px;
+                    height: 30px;
+                    margin-right: 6px;
+                }
+
+                span {
+                    font-weight: 500;
+                    font-size: 14px;
+                }
+            }
+
+            .item:first-child {
+                border-top: 0;
+            }
+
+            .item:hover {
+                background: #F5F5F5;
+            }
+        }
+    }
+
+    .transform_rotate {
+        transform: rotate(360deg);
+        transition-duration: 1s;
+    }
+
+    img {
+        cursor: pointer;
+        width: 24px;
+        height: 24px;
+    }
+
+    .list {
+        margin-right: 12px;
+    }
+
+    .refresh {
+        
+    }
+
+    .help {
+        margin-left: 20px;
+        margin-right: 12px;
+    }
+
+    .title {
+        padding-left: 16px;
+        flex: 1;
+        color: #000000;
+        font-size: 16px;
+        font-weight: 500;
+        word-break: break-all;
+    }
+}
+</style>

+ 4 - 4
src/http/configAPI.js

@@ -9,9 +9,9 @@ const api = {
 }
 
 const logApi = {
-	production: 'https://log.weiqumeta.com',
-	pre: 'https://prelog.weiqumeta.com',
-	development: 'https://testlog.weiqumeta.com'
+	production: 'https://log.denetme.net',
+	pre: 'https://prelog.denetme.net',
+	development: 'https://testlog.denetme.net'
 }
 
 const page = {
@@ -22,7 +22,7 @@ const page = {
 
 export const baseAPIUrl = api[process.env.NODE_ENV] + '/denet'
 
-export const logAPIUrl = logApi[process.env.NODE_ENV] + '/log-center'
+export const logAPIUrl = logApi[process.env.NODE_ENV] + '/denet/log'
 
 export const pageUrl = page[process.env.NODE_ENV]
 

+ 3 - 3
src/http/fetch.js

@@ -1,12 +1,12 @@
 import { appVersionCode, baseAPIUrl } from '@/http/configAPI.js'
-import { getChromeStorage } from '@/uilts/chromeExtension.js'
+import { getChromeStorageFromExtension } from '@/uilts/chromeExtension.js'
 
 export async function commonFetch({ url = '', method = 'POST' , params = {}, baseInfo = {}}) {
 
-    let storage_mid = await getChromeStorage('mid') || ''
+    let storage_mid = await getChromeStorageFromExtension('mid') || ''
     const { mid } = storage_mid || {}
     if (!baseInfo.token || !baseInfo.uid) {
-        const { accessToken: token = '', uid = '' } = await getChromeStorage('userInfo') || {}
+        const { accessToken: token = '', uid = '' } = await getChromeStorageFromExtension('userInfo') || {}
         baseInfo.token = token
         baseInfo.uid = uid
     }

+ 1 - 11
src/http/logApi.js

@@ -1,19 +1,9 @@
-import { service } from "./request";
 import { logAPIUrl } from '@/http/configAPI.js'
 import { commonFetch } from '@/http/fetch'
 
-// export function logApi(params) {
-//     return service({
-//         url: `${logAPIUrl}/statistics/uploadLogFromFrontend
-//         `,
-//         method: 'post',
-//         data: params
-//     })
-// }
-
 export function logApi(params = {}) {
     return commonFetch({
-        url: `${logAPIUrl}/statistics/uploadLogFromFrontend`,
+        url: `${logAPIUrl}/uploadLogFromFrontend`,
         baseInfo: {
             pageSource: params.params.pageSource || ''
         },

+ 24 - 0
src/http/nft.js

@@ -71,4 +71,28 @@ export function getTwitterNftGroupInfo(params) {
         method: 'post',
         data: params
     })
+}
+
+export function transferRequest(params) {
+    return service({
+        url: `/nft/transfer/request`,
+        method: 'post',
+        data: params
+    })
+}
+
+export function redeemNft(params) {
+    return service({
+        url: `/nft/project/redeemNft`,
+        method: 'post',
+        data: params
+    })
+}
+
+export function listPossessNftProject(params) {
+    return service({
+        url: `/nft/possess/listPossessNftProject`,
+        method: 'post',
+        data: params
+    })
 }

+ 31 - 0
src/log-center/autoLog/click.js

@@ -0,0 +1,31 @@
+// 点击埋点自定义属性
+
+import { reportLog } from '../logger';
+import { getTargetElementWhenClick } from '@/uilts/help';
+import Report from "@/log-center/log";
+
+let clickDataMap = new Map();
+
+const clickHandle = (e) => { 
+    const target = getTargetElementWhenClick(e);
+    const { extParams, ...eventData } = clickDataMap.get(target?.denetClickLogkey);
+    return eventData && reportLog({
+        businessType: Report.businessType.buttonClick,
+        ...eventData
+    }, extParams)
+}
+
+const clickLog =  {
+    mounted: (el, binding) => { 
+        const { value } = binding;
+        el.denetClickLogkey = el.denetClickLogkey || Math.random().toString(36).slice(-6);
+        clickDataMap.set(el.denetClickLogkey, value);
+        el.addEventListener('click', clickHandle,true)
+    },
+    unmounted(el) { 
+        // remove EventListener
+        el.removeEventListener('click', clickHandle, true)
+    }
+}
+
+export default clickLog;

+ 23 - 0
src/log-center/autoLog/index.js

@@ -0,0 +1,23 @@
+// 埋点插件
+
+import { showReportDialog } from '@sentry/vue';
+import clickLog from './click';
+import ShowLogObserver from './show';
+
+const AutoLog = {};
+
+AutoLog.install = (app) => { 
+    app.directive('click-log', clickLog);
+    app.directive('show-log', {
+        mounted(el, binding) {
+            // 加载阶段设置随机key标记当前元素
+            el.denetShowLogkey = el.denetShowLogkey || Math.random().toString(36).slice(-6);
+            ShowLogObserver.add(el, binding);
+        },
+        unmounted(el) {
+            ShowLogObserver.remove(el);
+        },
+    });
+}
+
+export default AutoLog;

+ 50 - 0
src/log-center/autoLog/show.js

@@ -0,0 +1,50 @@
+import { reportLog } from '../logger';
+import { getActiveKeyAfterClick } from '@/uilts/help';
+import Report from "@/log-center/log"
+
+// 每个窗口共享一个Observer实例
+class ShowLogObserver { 
+    constructor() { 
+        this._observe = null;
+        this.showLogMap = new Map();
+        this.init();
+    }
+
+    init() {
+        this._observe = new IntersectionObserver((entries, observer) => {
+            entries.forEach((entry) => {
+                if (entry.intersectionRatio > 0.5) {
+                    this.report(entry);
+                    // show-log-once  ===  '1' &&  曝光之后取消观察
+                    if (entry?.target?.getAttribute('show-log-once') === '1') {
+                        this.remove(entry.target);
+                    }
+                }
+            })
+        }, {
+            root: null,
+            rootMargin: '0px',
+            threshold: 1
+        })
+    }
+
+    remove(el) { 
+        this._observe.unobserve(el);
+        this.showLogMap.delete(el.denetShowLogkey);
+    }
+
+    add(el, binding) { 
+        this._observe.observe(el);
+        this.showLogMap.set(el.denetShowLogkey, binding.value)
+    }
+
+    report(el) { 
+        const { extParams, ...eventData } = this.showLogMap.get(el?.target?.denetShowLogkey);
+        return eventData && reportLog({
+            businessType: Report.businessType.pageView,
+            ...eventData
+        }, extParams)
+    }
+}
+
+export default new ShowLogObserver();

+ 9 - 0
src/log-center/log.js

@@ -0,0 +1,9 @@
+import * as logger from './logger'
+import * as logEnum from './logEnum'
+
+
+
+export default {
+  ...logger,
+  ...logEnum
+}

+ 165 - 0
src/log-center/logEnum.js

@@ -0,0 +1,165 @@
+import { PlayType } from '@/types';
+
+export const logType = {
+    'denet': '150',//denet-event-log
+}
+
+export const redPacketType = {
+    nftSale: 2,
+    nftGroupSale: 3,
+    treasure: 4,
+    postEditor: 5,
+    giveaway: 0,
+    lottery: 1
+}
+
+export const businessType = {
+    buttonView: "buttonView",
+    buttonClick: "buttonClick",
+    // 页面曝光
+    pageView: "pageView",
+}
+
+export const objectType = {
+    buttonMain: "button-main",
+    buttonSecond: "button-second",
+    confirmButton: "confirm-button",
+    tweetPostBinded: "TweetPostBinded",
+    loginButton: "login-button",
+    withdrawButton: "withdraw-button",
+    topupButton: "topup-button",
+    previewNextButton: 'preview-next-button',
+    setPublishContent: 'set-publish-content',
+
+
+    getMoreGiveaway: "get-more-giveaway",
+    nextButton: "next-button",
+    openChestButton: "open-chest-button",
+    copyButton: "copy-button",
+    repostSuccess: "repostSuccess",
+    channelButton: 'channel-button',
+
+    //discord
+    getDiscordGuildNoData: 'get-discord-guild-no-data',
+    getDiscordGuildCatch: 'get-discord-guild-catch',
+    getDiscordGuildOpenApiNoData: 'get-discord-guild-openapi-no-data',
+    getDiscordGuildOpenApiCatch: 'get-discord-guild-openapi-catch',
+    saveDiscordGuildData: 'save-discord-guild-data',
+
+    // 按钮点击
+    open_button: 'open-button',
+    // 关注全部
+    follow_button: 'follow-button',
+    follow: 'follow',
+    retweet: 'retweet',
+    like: 'like',
+    comment_and_tag: 'comment-and-tag',
+    join_discord: 'discord',
+    share_facebook: 'share-facebook',
+
+    // 查看已领取红包列表
+    received_list: 'received-list',
+    // 点击检测任务
+    get_giveaway: 'get-giveaway',
+    // 成功领取到钱包
+    wallet_button: 'wallet-button',
+    // 卡片解析
+    parse_card_error: 'parse-card-error',
+    // 安装成功
+    chrome_extension_installed: 'chrome-extension-installed',
+    // 发送事件异常
+    chrome_extension_sendmessage_error: 'chrome-extension-sendmessage-error',
+    // background文件安装catch异常
+    background_function_catch: 'background-function-catch',
+    // background 文件chrome 函数 try
+    background_function_try: 'background-function-try',
+    // create Nft
+    create_nfts_button: 'create-nfts-button',
+    confirm_transfer_button: 'confirm-transfer-button',
+    redeem_button: 'redeem-button',
+    buy_button: 'buy-button',
+    buy_nft_button: 'buy-nft-button',
+    custom_link_button: 'custom-link-button',
+    history_button: 'history-button',
+    app_button: 'app-button',
+    enter_url_button: 'enter-url-button',
+    top_right_button: 'top-right-button',
+    fullscreen_button: 'fullscreen-button',
+    encrypte_nft_button: 'encrypte-nft-button',
+    preRepost: 'preRepost',
+}
+
+export const pageSource = {
+    mainPage: "main-page",
+    publisherDialog: "publisher-dialog",
+    currencySelectorPage: "currency-selector-page",
+    rechargePage: "recharge-page",
+    previewPage: "preview-page",
+    denetLogin: "denet-login",
+    denetHomePage: "denet-home-page",
+    denetWithdrawSelector: "denet-withdraw-selector",
+    denetWithdrawForm: "denet-withdraw-form",
+    denetWithdrawConfirm: "denet-withdraw-confirm",
+    denetTopupSelector: "denet-topup-selector",
+    denetMorePage: "denet-more-page",
+    denetSelector: "denet-selector",
+    nftShopPage: "nft-shop-page",
+    nftPreviewPage: "nft-preview-page",
+    denetNftTransferPage: "denet-nft-transfer-page",
+
+
+    newFansRewardPage: "new-fans-reward-page",
+    inviteFriendsPage: "invite-friends-page",
+    openTreasurePage: "open-treasure-page",
+    beenInvitedPage: "been-invited-page",
+    waitingLotteryPage: "waiting-lottery-page",
+    missingLotteryPage: "missing-lottery-page",
+    expiredPage: "expired-page",
+
+    // 待开红包页
+    pending_page: 'pending-page',
+    // 已领取任务页
+    task_page: 'task-page',
+    // 领取列表页
+    received_list_page: 'received-list-page',
+    // 红包过期
+    expired_page: 'expired-page',
+    // 红包被领完
+    been_claimed_page: 'been-claimed-page',
+    // 机器人检测未通过
+    robot_detection_failed_page: 'robot-detection-failed-page',
+    // 成功领取到钱包
+    received_success_page: 'received-success-page',
+    received_empty_rewards_page: 'received-empty-rewards-page',
+    pe_loading_page: 'pe-loading-page',
+    pe_display_page: 'pe-display-page',
+    nft_sales_window: 'nft-sales-window',
+    nft_post_page: 'nft-post-page',
+    main_page_dashboard: 'main-page-dashboard',
+    post_editor_guide_page_left: 'post-editor-guide-page-left',
+    post_editor_guide_page_right: 'post-editor-guide-page-right',
+    buy_posteditor_nft_dialog: 'buy-posteditor-nft-dialog',
+
+}
+
+export const extParams = {
+    success: 'success',
+    failure: 'failure'
+}
+
+export const bizType = {
+    Treasure: 0,
+    Lottery: 1,
+    RedPacket: 2,
+    ToolBox: 3,
+}
+
+export const getCurrentBizType = (type) => {
+    let obj = {};
+    obj[PlayType.common] = bizType.RedPacket;
+    obj[PlayType.lottery] = bizType.Lottery;
+    obj[PlayType.treasure] = bizType.Treasure;
+    obj[PlayType.postEditor] = bizType.ToolBox;
+
+    return obj[type];
+}

+ 120 - 0
src/log-center/logger.js

@@ -0,0 +1,120 @@
+import { logApi, reportFrontLogApi } from '@/http/logApi'
+import { getBrowser } from '@/uilts/help.js';
+import { logType } from './logEnum.js';
+import { getChromeStorageFromExtension } from '@/uilts/chromeExtension'
+let userInfo = null;
+let mid = '';
+/**
+ * @eventData 以键值对存储,会在最终上报里解开的参数
+ * @extParams 最终上报到阿里云以json字符串存储的参数,如果extparams传入的不是obj会转换成obj
+ */
+export async function reportLog(eventData = {}, extParams = {}) {
+    // 过滤空值
+    let dataKey = Object.keys(eventData);
+    dataKey.forEach(key => {
+        if (eventData[key] === '') {
+            delete eventData[key]
+        }
+    })
+    // 2.reportLog 异常 存储到本地,再上报
+    try {
+        if (!userInfo) {
+            userInfo = await getChromeStorageFromExtension('userInfo').catch((error) => {console.log(error) }) || null;
+        }
+        if (!mid) {
+            mid = await getChromeStorageFromExtension('mid').catch((error) => {console.log(error) }) || '';
+        }
+        let isMobile = navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i);
+        let platform = isMobile ? `mobile` : `pc`;
+        let browser = getBrowser();
+        if (chrome && chrome.tabs) {
+            chrome.tabs.getCurrent((tab) => {
+                if (tab && tab.url) {
+                    let { url = '' } = tab;
+                    let extData = {
+                        url,
+                        platform,
+                        browser,
+                        twitterId: userInfo && userInfo.nickName || '',
+                        ...eventData,
+                    }
+                    paramsPretreatmentAndRequest(logType.denet, extData, extParams)
+                } else {
+                    let extData = {
+                        platform,
+                        browser,
+                        twitterId: userInfo && userInfo.nickName || '',
+                        ...eventData,
+                    }
+                    paramsPretreatmentAndRequest(logType.denet, extData, extParams)
+                }
+            })
+        } else {
+            paramsPretreatmentAndRequest(logType.denet, eventData, extParams)
+        }
+    } catch (error) {
+        reportFrontLogApi({
+            logData: JSON.stringify({
+                funcName: 'reportLog',
+                errmsg: error.message
+            })
+        })
+    }
+}
+
+function paramsPretreatmentAndRequest(logType, eventData, extParams) {
+    extParams = wrapObject(extParams)
+    let obj = {};
+    let pageSource = eventData.pageSource;
+    if (eventData.pageSource) {
+        delete eventData.pageSource;
+    }
+    obj.logType = logType;
+    obj.eventData = JSON.stringify(eventData)
+    obj.extParams = JSON.stringify(extParams)
+    logApi({
+        params: {
+            pageSource,
+            ...obj
+        }
+    }).then(() => {
+    }).catch(err => {
+        reportFrontLogApi({
+            logData: JSON.stringify(err)
+        })
+    })
+}
+
+function wrapObject(extParams) {
+    if (typeDecide(extParams, 'Object')) {
+        return extParams
+    }
+    return { 'defaultExt': extParams }
+}
+
+/**
+ * 检测对象类型
+ */
+function typeDecide(o, type) {
+    return Object.prototype.toString.call(o) === `[object ${type}]`;
+}
+
+export async function getReportCommonParams () {
+  let commonParams = {};
+  if (!userInfo) {
+      userInfo = await getChromeStorageFromExtension('userInfo') || null;
+  }
+  let isMobile = navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i);
+  let platform = isMobile ? `mobile` : `pc`;
+
+  if (chrome && chrome.tabs) {
+      let tab = await chrome.tabs.getCurrent();
+      commonParams = {
+          url: tab && tab.url ? tab.url : '',
+          platform,
+          browser: getBrowser(),
+          twitterId: userInfo && userInfo.nickName || '',
+      }
+  }
+  return commonParams;
+}

+ 304 - 305
src/pages/nav-nft-detail.vue

@@ -1,95 +1,94 @@
 <template>
-  <div class="nft-detail-wrapper">
-    <div class="back-bar">
-      <img :src="require('@/assets/svg/icon-nft-back-arrow.svg')" class="icon-arrow" @click="back" />
-      {{ NFTInfo.nftItemName }}
-    </div>
-    <div class="content">
-      <div class="nft-img">
-        <img class="img" :src="NFTInfo.imagePath" @click="clickNFTImg" v-if="NFTInfo.imagePath" />
-        <nft-card :nftItemId="NFTInfo.nftItemId" :item="NFTInfo.createImageInfo" :width="343" v-else>
-        </nft-card>
-      </div>
-      <div class="desc item" v-if="nftMetaData.description">
-        <div class="title">Description</div>
-        <div class="desc-content" v-html="nftMetaData.description"></div>
-      </div>
-      <div class="prop item" v-if="nftMetaData.properties && nftMetaData.properties.length">
-        <div class="title">Properties</div>
-        <div class="prop-content">
-          <div class="prop-item" v-for="(filedValueItem, filedValueIndex) in nftMetaData.properties"
-            :key="filedValueIndex">
-            {{  filedValueItem.name  }}
-            <div class="prop-name">
-              {{  filedValueItem.value  }}
-            </div>
-            {{  filedValueItem.description  }}
-          </div>
+    <div class="nft-detail-wrapper">
+        <div class="back-bar">
+            <img :src="require('@/assets/svg/icon-nft-back-arrow.svg')" class="icon-arrow" @click="back" />
+            {{  NFTInfo.nftItemName  }}
         </div>
-      </div>
-
-      <div class="about item" v-if="nftMetaData.about">
-        <div class="title">About</div>
-        <div class="about-content" v-html="nftMetaData.about"></div>
-      </div>
-      <div class="detail item" v-if="nftDetailData.details">
-        <div class="title">Details</div>
-        <div class="detail-content">
-          <div class="detail-item">
-            <div class="left">Contract Address</div>
-            <div class="right address" @click="clickAddress">
-              <span>{{ nftDetailData.details.contractAddress }}</span>
-              <span>{{ nftDetailData.details.contractAddress }}</span>
+        <div class="content">
+            <div class="nft-img">
+                <img class="img" :src="NFTInfo.imagePath" @click="clickNFTImg" v-if="NFTInfo.imagePath" />
+                <nft-card :nftItemId="NFTInfo.nftItemId" :item="NFTInfo.createImageInfo" :width="343" v-else></nft-card>
             </div>
-          </div>
-          <div class="detail-item">
-            <div class="left">Token ID</div>
-            <div class="right token" @click="clickToken">
-              {{ nftDetailData.details.tokenId }}
+            <div class="desc item" v-if="nftMetaData.description">
+                <div class="title">Description</div>
+                <div class="desc-content" v-html="nftMetaData.description"></div>
             </div>
-          </div>
-          <div class="detail-item">
-            <div class="left">Token Standard</div>
-            <div class="right">
-              {{ nftDetailData.details.tokenStandard }}
+            <div class="prop item" v-if="nftMetaData.properties && nftMetaData.properties.length">
+                <div class="title">Properties</div>
+                <div class="prop-content">
+                    <div class="prop-item" v-for="(filedValueItem, filedValueIndex) in nftMetaData.properties"
+                        :key="filedValueIndex">
+                        {{  filedValueItem.name  }}
+                        <div class="prop-name">
+                            {{  filedValueItem.value  }}
+                        </div>
+                        {{  filedValueItem.description  }}
+                    </div>
+                </div>
             </div>
-          </div>
-          <div class="detail-item">
-            <div class="left">Blockchain</div>
-            <div class="right">
-              {{ nftDetailData.details.blockChain }}
+
+            <div class="about item" v-if="nftMetaData.about">
+                <div class="title">About</div>
+                <div class="about-content" v-html="nftMetaData.about"></div>
             </div>
-          </div>
-          <div class="detail-item">
-            <div class="left">Creator Fees</div>
-            <div class="right">
-              {{ nftDetailData.details.creatorFees }}
+            <div class="detail item" v-if="nftDetailData.details">
+                <div class="title">Details</div>
+                <div class="detail-content">
+                    <div class="detail-item">
+                        <div class="left">Contract Address</div>
+                        <div class="right address" @click="clickAddress">
+                            <span>{{  nftDetailData.details.contractAddress  }}</span>
+                            <span>{{  nftDetailData.details.contractAddress  }}</span>
+                        </div>
+                    </div>
+                    <div class="detail-item">
+                        <div class="left">Token ID</div>
+                        <div class="right token" @click="clickToken">
+                            {{  nftDetailData.details.tokenId  }}
+                        </div>
+                    </div>
+                    <div class="detail-item">
+                        <div class="left">Token Standard</div>
+                        <div class="right">
+                            {{  nftDetailData.details.tokenStandard  }}
+                        </div>
+                    </div>
+                    <div class="detail-item">
+                        <div class="left">Blockchain</div>
+                        <div class="right">
+                            {{  nftDetailData.details.blockChain  }}
+                        </div>
+                    </div>
+                    <div class="detail-item">
+                        <div class="left">Creator Fees</div>
+                        <div class="right">
+                            {{  nftDetailData.details.creatorFees  }}
+                        </div>
+                    </div>
+                    <div class="detail-item">
+                        <div class="left">Transaction Royalties</div>
+                        <div class="right">
+                            {{  nftDetailData.details.transactionRoyalties  }}
+                        </div>
+                    </div>
+                </div>
             </div>
-          </div>
-          <div class="detail-item">
-            <div class="left">Transaction Royalties</div>
-            <div class="right">
-              {{ nftDetailData.details.transactionRoyalties }}
+            <div class="date item" v-if="nftDetailData.dateOfPossession">
+                <div class="title">Date of possession</div>
+                <div class="date-content">{{  nftDetailData.dateOfPossession  }}</div>
+            </div>
+
+            <div class="price item" v-if="nftDetailData.purchasePrice">
+                <div class="title">Purchase price</div>
+                <div class="price-content">{{  nftDetailData.purchasePrice  }}</div>
+            </div>
+        </div>
+        <div class="bottom-bar">
+            <div class="sale" @click="transfer">
+                <span>Transfer</span>
             </div>
-          </div>
         </div>
-      </div>
-      <div class="date item" v-if="nftDetailData.dateOfPossession">
-        <div class="title">Date of possession</div>
-        <div class="date-content">{{ nftDetailData.dateOfPossession }}</div>
-      </div>
-
-      <div class="price item" v-if="nftDetailData.purchasePrice">
-        <div class="title">Purchase price</div>
-        <div class="price-content">{{ nftDetailData.purchasePrice }}</div>
-      </div>
-    </div>
-    <div class="bottom-bar">
-      <div class="sale" @click="transfer">
-        <span>Transfer</span>
-      </div>
     </div>
-  </div>
 </template>
 
 <script setup>
@@ -106,80 +105,80 @@ let nftDetailData = ref({});
 let router = useRouter();
 
 let NFTInfo = ref({
-  imagePath: '',
-  nftItemName: ''
+    imagePath: '',
+    nftItemName: ''
 });
 
 const back = () => {
-  messageCenter.send({
-    actionType: MESSAGE_ENUM.IFRAME_SHOW_FOOTER_MENU,
-    data: {
-      showMenu: true,
-    }
-  })
-  router.back();
+    messageCenter.send({
+        actionType: MESSAGE_ENUM.IFRAME_SHOW_FOOTER_MENU,
+        data: {
+            showMenu: true,
+        }
+    })
+    router.back();
 };
 
 const clickAddress = () => {
-  let { contractAddressUrl = '' } = nftDetailData.value.details;
-  if (contractAddressUrl) {
-    window.open(contractAddressUrl);
-  }
+    let { contractAddressUrl = '' } = nftDetailData.value.details;
+    if (contractAddressUrl) {
+        window.open(contractAddressUrl);
+    }
 }
 
 const clickToken = () => {
-  let { tokenIdUrl = '' } = nftDetailData.value.details;
-  if (tokenIdUrl) {
-    window.open(tokenIdUrl);
-  }
+    let { tokenIdUrl = '' } = nftDetailData.value.details;
+    if (tokenIdUrl) {
+        window.open(tokenIdUrl);
+    }
 }
 
 const clickNFTImg = () => {
-  // window.open(NFTInfo.value.imagePath);
+    // window.open(NFTInfo.value.imagePath);
 };
 
 const getDetail = () => {
-  getNFTDetail({
-    params: {
-      nftItemId: NFTInfo.value.nftItemId
-    }
-  }).then(res => {
-    if (res.code == 0) {
-      console.log(res)
-      let { metadata = '{}' } = res.data || {};
-      nftDetailData.value = res.data;
-      nftMetaData.value = JSON.parse(metadata);
-    }
-  }).catch(() => {
-  })
+    getNFTDetail({
+        params: {
+            nftItemId: NFTInfo.value.nftItemId
+        }
+    }).then(res => {
+        if (res.code == 0) {
+            console.log(res)
+            let { metadata = '{}' } = res.data || {};
+            nftDetailData.value = res.data;
+            nftMetaData.value = JSON.parse(metadata);
+        }
+    }).catch(() => {
+    })
 }
 
 const transfer = () => {
-  if (Object.keys(nftDetailData.value).length) {
-    clearTimeout(timer.value);
-    router.push({
-      name: 'NFTTransfer',
-      query: {
-        params: JSON.stringify({
-          nftItemId: nftDetailData.value?.nftItemId,
-          chainInfo: nftDetailData.value?.chainInfo,
-          transferFeeCurrencyInfo: nftDetailData.value?.transferFeeCurrencyInfo,
-          transferFeeAmountValue: nftDetailData.value?.transferFeeAmountValue,
+    if (Object.keys(nftDetailData.value).length) {
+        clearTimeout(timer.value);
+        router.push({
+            name: 'navNftTransfer',
+            query: {
+                params: JSON.stringify({
+                    nftItemId: nftDetailData.value?.nftItemId,
+                    chainInfo: nftDetailData.value?.chainInfo,
+                    transferFeeCurrencyInfo: nftDetailData.value?.transferFeeCurrencyInfo,
+                    transferFeeAmountValue: nftDetailData.value?.transferFeeAmountValue,
+                })
+            }
         })
-      }
-    })
-  } else {
-    clearTimeout(timer.value);
-    timer.value = setTimeout(() => {
-      transfer()
-    }, 300)
-  }
+    } else {
+        clearTimeout(timer.value);
+        timer.value = setTimeout(() => {
+            transfer()
+        }, 300)
+    }
 }
 
 onMounted(() => {
-  let { params = '{}' } = router.currentRoute.value.query;
-  NFTInfo.value = JSON.parse(params);
-  getDetail();
+    let { params = '{}' } = router.currentRoute.value.query;
+    NFTInfo.value = JSON.parse(params);
+    getDetail();
 })
 
 
@@ -187,200 +186,200 @@ onMounted(() => {
 
 <style scoped lang="scss">
 .nft-detail-wrapper {
-  width: 100%;
-  height: 100%;
-
-  .back-bar {
-    height: 48px;
-    background: #ffffff;
-    box-shadow: 0px 0.5px 0px #d1d9dd;
-    box-sizing: border-box;
-    padding: 14px;
-    font-weight: 500;
-    font-size: 16px;
-    display: flex;
-    align-items: center;
-
-    .icon-arrow {
-      width: 24px;
-      margin-right: 12px;
-      cursor: pointer;
-    }
-  }
-
-  .content {
     width: 100%;
-    height: calc(100% - 120px);
-    padding: 0 16px;
-    box-sizing: border-box;
-    overflow-y: auto;
-
-    .nft-img {
-      margin-top: 23px;
-      margin-bottom: 20px;
-      text-align: center;
-      cursor: pointer;
-
-      .img {
-        width: 280px;
-        border-radius: 10px;
-      }
-    }
-
-    .item {
-      border: 1px solid #e3e3e3;
-      border-radius: 10px;
-      padding: 14px;
-      box-sizing: border-box;
-      margin-bottom: 12px;
-
-      .title {
-        font-weight: 600;
-        font-size: 14px;
-      }
-    }
-
-    .desc {
-      margin-top: 10px;
-
-      .desc-content {
+    height: 100%;
+
+    .back-bar {
+        height: 48px;
+        background: #ffffff;
+        box-shadow: 0px 0.5px 0px #d1d9dd;
+        box-sizing: border-box;
+        padding: 14px;
         font-weight: 500;
-        font-size: 14px;
-        color: #929292;
+        font-size: 16px;
+        display: flex;
+        align-items: center;
 
-        span {
-          color: #1d9bf0;
+        .icon-arrow {
+            width: 24px;
+            margin-right: 12px;
+            cursor: pointer;
         }
-      }
     }
 
-    .prop {
-      .prop-content {
-        display: flex;
-        flex-wrap: wrap;
-        margin-top: 12px;
-
-        .prop-item {
-          width: 48%;
-          min-height: 88px;
-          background: #f8f8f8;
-          border-radius: 10px;
-          display: flex;
-          flex-direction: column;
-          justify-content: center;
-          padding: 8px;
-          box-sizing: border-box;
-          align-items: center;
-          font-weight: 500;
-          font-size: 12px;
-          color: #929292;
-          margin-bottom: 10px;
-
-          .prop-name {
-            font-weight: 700;
-            font-size: 17px;
-            margin-top: 6px;
-            margin-bottom: 8px;
-            color: #000;
-            word-break: break-all;
-          }
+    .content {
+        width: 100%;
+        height: calc(100% - 120px);
+        padding: 0 16px;
+        box-sizing: border-box;
+        overflow-y: auto;
+
+        .nft-img {
+            margin-top: 23px;
+            margin-bottom: 20px;
+            text-align: center;
+            cursor: pointer;
+
+            .img {
+                width: 280px;
+                border-radius: 10px;
+            }
         }
 
-        .prop-item:nth-child(odd) {
-          margin-right: 8px;
+        .item {
+            border: 1px solid #e3e3e3;
+            border-radius: 10px;
+            padding: 14px;
+            box-sizing: border-box;
+            margin-bottom: 12px;
+
+            .title {
+                font-weight: 600;
+                font-size: 14px;
+            }
         }
-      }
-    }
 
-    .about-content {
-      margin-top: 22px;
+        .desc {
+            margin-top: 10px;
 
-      .section {
-        font-weight: 400;
-        font-size: 14px;
-        margin-bottom: 20px;
-      }
-    }
+            .desc-content {
+                font-weight: 500;
+                font-size: 14px;
+                color: #929292;
 
-    .section {
-      font-weight: 400;
-      font-size: 14px;
-      margin-bottom: 10px;
-    }
+                span {
+                    color: #1d9bf0;
+                }
+            }
+        }
 
-    .detail-content {
-      margin-top: 15px;
+        .prop {
+            .prop-content {
+                display: flex;
+                flex-wrap: wrap;
+                margin-top: 12px;
+
+                .prop-item {
+                    width: 48%;
+                    min-height: 88px;
+                    background: #f8f8f8;
+                    border-radius: 10px;
+                    display: flex;
+                    flex-direction: column;
+                    justify-content: center;
+                    padding: 8px;
+                    box-sizing: border-box;
+                    align-items: center;
+                    font-weight: 500;
+                    font-size: 12px;
+                    color: #929292;
+                    margin-bottom: 10px;
+
+                    .prop-name {
+                        font-weight: 700;
+                        font-size: 17px;
+                        margin-top: 6px;
+                        margin-bottom: 8px;
+                        color: #000;
+                        word-break: break-all;
+                    }
+                }
+
+                .prop-item:nth-child(odd) {
+                    margin-right: 8px;
+                }
+            }
+        }
 
-      .detail-item {
-        display: flex;
-        align-items: center;
-        justify-content: space-between;
-        height: 24px;
-        font-weight: 400;
-        font-size: 14px;
+        .about-content {
+            margin-top: 22px;
 
-        .right {
-          color: #929292;
+            .section {
+                font-weight: 400;
+                font-size: 14px;
+                margin-bottom: 20px;
+            }
         }
 
-        .token {
-          color: #1d9bf0 !important;
-          cursor: pointer;
+        .section {
+            font-weight: 400;
+            font-size: 14px;
+            margin-bottom: 10px;
         }
 
-        .address {
-          width: 100px;
-          white-space: nowrap;
-          color: #1d9bf0 !important;
-          cursor: pointer;
-
-          >span {
-            display: inline-block;
-            overflow: hidden;
-            text-overflow: ellipsis;
-            width: 50%;
-
-            +span {
-              width: calc(50% + 10px);
-              direction: rtl;
-              margin-left: -11px;
+        .detail-content {
+            margin-top: 15px;
+
+            .detail-item {
+                display: flex;
+                align-items: center;
+                justify-content: space-between;
+                height: 24px;
+                font-weight: 400;
+                font-size: 14px;
+
+                .right {
+                    color: #929292;
+                }
+
+                .token {
+                    color: #1d9bf0 !important;
+                    cursor: pointer;
+                }
+
+                .address {
+                    width: 100px;
+                    white-space: nowrap;
+                    color: #1d9bf0 !important;
+                    cursor: pointer;
+
+                    >span {
+                        display: inline-block;
+                        overflow: hidden;
+                        text-overflow: ellipsis;
+                        width: 50%;
+
+                        +span {
+                            width: calc(50% + 10px);
+                            direction: rtl;
+                            margin-left: -11px;
+                        }
+                    }
+                }
             }
-          }
         }
-      }
-    }
 
-    .date-content,
-    .price-content {
-      margin-top: 10px;
-      font-weight: 500;
-      font-size: 14px;
-      color: #929292;
+        .date-content,
+        .price-content {
+            margin-top: 10px;
+            font-weight: 500;
+            font-size: 14px;
+            color: #929292;
+        }
     }
-  }
-
-  .bottom-bar {
-    background: #ffffff;
-    box-shadow: inset 0px 1px 0px #ececec;
-    height: 70px;
-    padding: 0 16px;
-    box-sizing: border-box;
-    display: flex;
-    align-items: center;
-    justify-content: right;
-
-    .sale {
-      display: flex;
-      align-items: center;
-      justify-content: center;
-      cursor: pointer;
-      width: 97px;
-      height: 37px;
-      font-size: 14px;
-      color: #1D9BF0;
-      border-radius: 20px;
-      border: 1px solid #1D9BF0;
+
+    .bottom-bar {
+        background: #ffffff;
+        box-shadow: inset 0px 1px 0px #ececec;
+        height: 70px;
+        padding: 0 16px;
+        box-sizing: border-box;
+        display: flex;
+        align-items: center;
+        justify-content: right;
+
+        .sale {
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            cursor: pointer;
+            width: 97px;
+            height: 37px;
+            font-size: 14px;
+            color: #1D9BF0;
+            border-radius: 20px;
+            border: 1px solid #1D9BF0;
+        }
     }
-  }
 }
 </style>

+ 97 - 97
src/pages/nav-nft-index.vue

@@ -1,17 +1,17 @@
 <template>
-  <div class="nft-page-wrapper" ref="pageWrapperDom" @scroll="pageScroll">
-    <div class="content" ref="pageListDom">
-      <div class="item" v-for="(item, index) in listData" :key="index" @click="clickNFT(item)">
-        <img :src="item.imagePath" class="img" v-if="item.imagePath" />
-        <nft-card :nftItemId="item.nftItemId" :item="item.createImageInfo" :width="103" v-else></nft-card>
-        <div class="name">{{  item.nftItemName  }}</div>
-      </div>
+    <div class="nft-page-wrapper" ref="pageWrapperDom" @scroll="pageScroll">
+        <div class="content" ref="pageListDom">
+            <div class="item" v-for="(item, index) in listData" :key="index" @click="clickNFT(item)">
+                <img :src="item.imagePath" class="img" v-if="item.imagePath" />
+                <nft-card :nftItemId="item.nftItemId" :item="item.createImageInfo" :width="103" v-else></nft-card>
+                <div class="name">{{  item.nftItemName  }}</div>
+            </div>
+        </div>
+        <join-group-finish-dialog :dialogVisible="joinGroupFinishShow" :position="'absolute'"
+            :contentStyle="{ width: '315px' }" :iconStyle="{ width: '80px', marginTop: '26px' }"
+            :descStyle="{ marginTop: '24px', marginBottom: '25px', fontSize: '19px' }" @confirm="confirmFinish">
+        </join-group-finish-dialog>
     </div>
-    <join-group-finish-dialog :dialogVisible="joinGroupFinishShow" :position="'absolute'"
-      :contentStyle="{ width: '315px' }" :iconStyle="{ width: '80px', marginTop: '26px' }"
-      :descStyle="{ marginTop: '24px', marginBottom: '25px', fontSize: '19px' }" @confirm="confirmFinish">
-    </join-group-finish-dialog>
-  </div>
 </template>
 
 <script setup>
@@ -26,11 +26,11 @@ import joinGroupFinishDialog from "@/components/join-group-finish-dialog.vue";
 let listData = ref([]);
 
 let NFTReqParams = {
-  params: {
-    pageNum: 1,
-    pageSize: 30,
-  },
-  loadMore: false,
+    params: {
+        pageNum: 1,
+        pageSize: 30,
+    },
+    loadMore: false,
 };
 
 let pageWrapperDom = ref(null);
@@ -39,111 +39,111 @@ let joinGroupFinishShow = ref(false);
 let router = useRouter()
 
 const clickNFT = (params) => {
-  console.log(params)
-  messageCenter.send({
-    actionType: MESSAGE_ENUM.IFRAME_SHOW_FOOTER_MENU,
-    data: {
-      showMenu: false,
-    }
-  })
-  router.push({
-    name: 'navNftDetail',
-    query: {
-      params: JSON.stringify(params)
-    }
-  })
+    console.log(params)
+    messageCenter.send({
+        actionType: MESSAGE_ENUM.IFRAME_SHOW_FOOTER_MENU,
+        data: {
+            showMenu: false,
+        }
+    })
+    router.push({
+        name: 'navNftDetail',
+        query: {
+            params: JSON.stringify(params)
+        }
+    })
 }
 
 const getNFTListMine = () => {
-  nftListMine({
-    params: NFTReqParams.params,
-  }).then((res) => {
-    if (res.data && res.data.length) {
-      if (NFTReqParams.params.pageNum < 2) {
-        listData.value = res.data;
-      } else {
-        let data = listData.value;
-        data = data.concat(res.data);
-        listData.value = data;
-      }
-      NFTReqParams.loadMore = false;
-    }
-  });
+    nftListMine({
+        params: NFTReqParams.params,
+    }).then((res) => {
+        if (res.data && res.data.length) {
+            if (NFTReqParams.params.pageNum < 2) {
+                listData.value = res.data;
+            } else {
+                let data = listData.value;
+                data = data.concat(res.data);
+                listData.value = data;
+            }
+            NFTReqParams.loadMore = false;
+        }
+    });
 }
 
 const pageScroll = (e) => {
-  let wrapperHeight = pageWrapperDom.value.offsetHeight;
-  let pageListHeight = pageListDom.value.offsetHeight;
-  let scrollTop = e.target.scrollTop || 0;
-  if (
-    NFTReqParams.loadMore === false &&
-    wrapperHeight + scrollTop >= pageListHeight - 60
-  ) {
-    NFTReqParams.loadMore = true;
-    NFTReqParams.params.pageNum++;
-    getNFTListMine();
-  }
+    let wrapperHeight = pageWrapperDom.value.offsetHeight;
+    let pageListHeight = pageListDom.value.offsetHeight;
+    let scrollTop = e.target.scrollTop || 0;
+    if (
+        NFTReqParams.loadMore === false &&
+        wrapperHeight + scrollTop >= pageListHeight - 60
+    ) {
+        NFTReqParams.loadMore = true;
+        NFTReqParams.params.pageNum++;
+        getNFTListMine();
+    }
 };
 
 const onMessage = () => {
-  messageCenter.listen(MESSAGE_ENUM.CONTENT_POPUP_PAGE_SHOW, (req) => {
-    getNFTListMine();
-    showJoinFinishHandler(req.data);
-  })
+    messageCenter.listen(MESSAGE_ENUM.CONTENT_POPUP_PAGE_SHOW, (req) => {
+        getNFTListMine();
+        showJoinFinishHandler(req.data);
+    })
 }
 
 const showJoinFinishHandler = (params) => {
-  let { path, showJoinGroupFinish } = params;
-  if (path == '/NFT' && showJoinGroupFinish) {
-    joinGroupFinishShow.value = true;
-  } else if (joinGroupFinishShow.value) {
-    joinGroupFinishShow.value = false;
-  }
+    let { path, showJoinGroupFinish } = params;
+    if (path == '/NFT' && showJoinGroupFinish) {
+        joinGroupFinishShow.value = true;
+    } else if (joinGroupFinishShow.value) {
+        joinGroupFinishShow.value = false;
+    }
 }
 
 const confirmFinish = () => {
-  joinGroupFinishShow.value = false;
+    joinGroupFinishShow.value = false;
 }
 
 onMounted(() => {
-  onMessage();
-  getNFTListMine();
+    onMessage();
+    getNFTListMine();
 })
 </script>
 
 <style scoped lang="scss">
 .nft-page-wrapper {
-  width: 100%;
-  height: 100%;
-  overflow-y: auto;
-
-  .content {
     width: 100%;
-    display: flex;
-    flex-wrap: wrap;
-    padding: 5px 2px 0 16px;
-    box-sizing: border-box;
-
-    .item {
-      width: 33%;
-      box-sizing: border-box;
-      padding-right: 14px;
-      margin-top: 15px;
-      cursor: pointer;
-
-      .img {
+    height: 100%;
+    overflow-y: auto;
+
+    .content {
         width: 100%;
-        border-radius: 5px;
-        height: 104px;
-        object-fit: cover;
-      }
-
-      .name {
-        font-weight: 400;
-        font-size: 12px;
-        margin-top: 6px;
-      }
+        display: flex;
+        flex-wrap: wrap;
+        padding: 5px 2px 0 16px;
+        box-sizing: border-box;
+
+        .item {
+            width: 33%;
+            box-sizing: border-box;
+            padding-right: 14px;
+            margin-top: 15px;
+            cursor: pointer;
+
+            .img {
+                width: 100%;
+                border-radius: 5px;
+                height: 104px;
+                object-fit: cover;
+            }
+
+            .name {
+                font-weight: 400;
+                font-size: 12px;
+                margin-top: 6px;
+            }
+        }
     }
-  }
 }
 </style>

+ 329 - 0
src/pages/nav-nft-transfer.vue

@@ -0,0 +1,329 @@
+<template>
+    <div class="info">
+        <v-head
+            :title="'Transfer'"
+            :show_more="true"
+            :transactionsRouterParams="{
+                backUrl: 'back'
+            }">
+        </v-head>
+        <template v-if="!isSuccess">
+            <div class="content">
+                <div class="token">
+                    <div class="title">Network</div>
+                    <div class="box">
+                        <img :src="transChainInfo?.iconPath" alt="" />
+                        <span>{{  transChainInfo?.chainName  }}</span>
+                    </div>
+                </div>
+                <div class="token">
+                    <div class="title">Address</div>
+                    <div class="box">
+                        <input type="text" v-model="address" placeholder="Click to enter" />
+                    </div>
+                </div>
+            </div>
+            <div class="footer">
+                <div class="left">
+                    <div class="txt">Network fee</div>
+                    <span class="money">
+                        <a-tooltip :title="feeAmountValue || 0">{{ getBit(feeAmountValue || 0) }}</a-tooltip> {{
+                         feeCurrencyInfo?.tokenSymbol  }}
+                    </span>
+                    <div class="tips" v-if="showTips">Insufficient balance</div>
+                </div>
+                <div class="right">
+                    <div class="btn enter" @click="next" v-if="isNext">Confirm</div>
+                    <div class="btn" v-else>Confirm</div>
+                </div>
+            </div>
+        </template>
+        <template v-else>
+            <div class="withdraw-status">
+                <img :src="require('@/assets/svg/icon-withdraw-status.svg')" alt="" />
+                <div>
+                    <div class="title">Submitted successfully</div>
+                    <div class="desc">
+                        Please check the status at the message tab
+                    </div>
+                </div>
+            </div>
+            <div class="confirm-btn" @click="doneHandle">Done</div>
+        </template>
+    </div>
+</template>
+
+<script setup>
+import Report from "@/log-center/log"
+import { ref, onMounted, watchEffect } from 'vue'
+import { message } from 'ant-design-vue';
+import { getCurrencyInfoByCode } from '@/http/publishApi'
+import { transferRequest } from '@/http/nft'
+import { useRouter } from "vue-router";
+import messageCenter from "@/uilts/messageCenter";
+import MESSAGE_ENUM from "@/uilts/messageCenter/messageEnum";
+import VHead from '@/components/v-head.vue'
+import { getBit } from "@/uilts/help";
+
+const isNext = ref(false)
+const isSuccess = ref(false)
+const isPayment = ref(false)
+const showTips = ref(false)
+const transChainInfo = ref({})
+const feeAmountValue = ref(0)
+const feeCurrencyInfo = ref({})
+const address = ref('')
+const nftId = ref('')
+const router = useRouter()
+
+const getCurrentyInfo = (data) => {
+    getCurrencyInfoByCode({
+        params: {
+            currencyCode: data?.currencyCode
+        }
+    }).then(res => {
+        let { code, data } = res;
+        if (code === 0) {
+            isPayment.value = Number(data?.balance) >= Number(feeAmountValue.value)
+            showTips.value = Number(data?.balance) < Number(feeAmountValue.value)
+        }
+    })
+}
+
+const next = () => {
+    transferRequest({
+        params: {
+            nftItemId: nftId.value,
+            receiveAddress: address.value
+        }
+    }).then(res => {
+        let { code } = res;
+        let transfer;
+        if (code === 0) {
+            isSuccess.value = true;
+            transfer = 'success'
+        } else if (code === 5302) {
+            transfer = 'fail'
+            message.error(`NFT transfer is under system maintenance, try again in two or three hours.`);
+        } else {
+            transfer = 'fail'
+            message.error(`Transfer failed, please try again`);
+        }
+        // report
+        Report.reportLog({
+            pageSource: Report.pageSource.denetNftTransferPage,
+            businessType: Report.businessType.buttonClick,
+            objectType: Report.objectType.confirm_transfer_button,
+        }, {
+            transfer: transfer
+        });
+    })
+}
+
+const doneHandle = () => {
+    messageCenter.send({
+        actionType: MESSAGE_ENUM.IFRAME_SHOW_FOOTER_MENU,
+        data: {
+            showMenu: true,
+        }
+    })
+    router.push({ name: 'navNftIndex' })
+}
+
+watchEffect(() => {
+    isNext.value = address.value !== '' && isPayment.value
+})
+
+onMounted(() => {
+    let { params = '{}' } = router.currentRoute.value.query;
+    let { chainInfo, nftItemId, transferFeeAmountValue, transferFeeCurrencyInfo } = JSON.parse(params);
+    // set
+    nftId.value = nftItemId;
+    transChainInfo.value = chainInfo
+    feeAmountValue.value = transferFeeAmountValue
+    feeCurrencyInfo.value = transferFeeCurrencyInfo
+
+    getCurrentyInfo(feeCurrencyInfo.value)
+
+    // report
+    Report.reportLog({
+        pageSource: Report.pageSource.denetNftTransferPage,
+        businessType: Report.businessType.pageView,
+    });
+})
+</script>
+
+<style lang="scss" scoped>
+.info {
+    position: relative;
+    height: 100%;
+
+    .content {
+        height: calc(100% - 48px - 80px);
+        overflow: auto;
+        padding: 13px 16px 0 13px;
+
+        .token {
+            margin-bottom: 20px;
+
+            .title {
+                font-weight: 500;
+                font-size: 12px;
+                margin-bottom: 6px;
+                color: #8B8B8B;
+            }
+
+            .box {
+                border: 1px solid #DBDBDB;
+                border-radius: 8px;
+                height: 44px;
+                display: flex;
+                align-items: center;
+                padding: 0 15px 0 10px;
+                display: flex;
+                flex-wrap: nowrap;
+                justify-content: space-between;
+
+                img {
+                    width: 20px;
+                    height: 20px;
+                }
+
+                .up {
+                    width: 13px;
+                    height: 12px;
+                    cursor: pointer;
+                }
+
+                input {
+                    outline: none;
+                    border: 0;
+                    flex: 1;
+                    height: 18px;
+                    padding: 0 6px;
+                    font-weight: 500;
+                    font-size: 15px;
+
+                    &::placeholder {
+                        color: #B8B8B8;
+                    }
+                }
+
+                input::-webkit-outer-spin-button,
+                input::-webkit-inner-spin-button {
+                    -webkit-appearance: none;
+                }
+
+                input[type='number'] {
+                    -moz-appearance: textfield;
+                }
+
+                span {
+                    flex: 1;
+                    margin-left: 6px;
+                    font-size: 14px;
+                    font-weight: 500;
+                }
+            }
+        }
+    }
+
+    .footer {
+        z-index: 11;
+        background: #fff;
+        border-top: 1px solid #DBDBDB;
+        bottom: 0;
+        height: 80px;
+        display: flex;
+        position: absolute;
+        justify-content: space-between;
+        width: 100%;
+        bottom: 0;
+        align-items: center;
+
+        .left {
+            margin-left: 16px;
+
+            .txt {
+                color: #9D9D9D;
+                font-weight: 400;
+                font-size: 12px;
+            }
+
+            .money {
+                color: #000000;
+                margin-right: auto;
+                font-size: 15px;
+                font-weight: bold;
+            }
+
+            .tips {
+                color: #FF0000;
+                font-weight: 500;
+                font-size: 12px;
+            }
+        }
+
+        .right {
+            margin-right: 16px;
+
+            .btn {
+                cursor: pointer;
+                width: 140px;
+                height: 46px;
+                line-height: 46px;
+                text-align: center;
+                font-weight: 600;
+                font-size: 18px;
+                color: #FFFFFF;
+                background: #D2D2D2;
+                border-radius: 100px;
+            }
+
+            .enter {
+                background: #1D9BF0;
+            }
+        }
+    }
+}
+
+.withdraw-status {
+    text-align: center;
+
+    img {
+        margin-top: 40px;
+        margin-bottom: 34px;
+    }
+
+    .title {
+        font-weight: 500;
+        font-size: 20px;
+        margin-bottom: 10px;
+    }
+
+    .desc {
+        font-size: 15px;
+        color: rgba($color: #000000, $alpha: 0.5);
+    }
+}
+
+.confirm-btn {
+    width: 335px;
+    height: 60px;
+    text-align: center;
+    line-height: 60px;
+    border-radius: 100px;
+    background: #1D9BF0;
+    font-weight: 600;
+    font-size: 18px;
+    color: #fff;
+    position: absolute;
+    left: 50%;
+    bottom: 35px;
+    transform: translateX(-50%);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+}
+</style>

+ 5 - 0
src/router/index.js

@@ -22,6 +22,11 @@ const routes = [
         path: '/nav-nft-detail',
         name: 'navNftDetail',
         component: () => require('../pages/nav-nft-detail')
+    },
+    {
+        path: '/nav-nft-transfer',
+        name: 'navNftTransfer',
+        component: () => require('../pages/nav-nft-transfer')
     }
 ]
 

+ 134 - 5
yarn.lock

@@ -19,6 +19,26 @@
     "@jridgewell/gen-mapping" "^0.1.0"
     "@jridgewell/trace-mapping" "^0.3.9"
 
+"@ant-design/colors@^6.0.0":
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/@ant-design/colors/-/colors-6.0.0.tgz#9b9366257cffcc47db42b9d0203bb592c13c0298"
+  integrity sha512-qAZRvPzfdWHtfameEGP2Qvuf838NhergR35o+EuVyB5XvSA98xod5r4utvi4TJ3ywmevm290g9nsCG5MryrdWQ==
+  dependencies:
+    "@ctrl/tinycolor" "^3.4.0"
+
+"@ant-design/icons-svg@^4.2.1":
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/@ant-design/icons-svg/-/icons-svg-4.2.1.tgz#8630da8eb4471a4aabdaed7d1ff6a97dcb2cf05a"
+  integrity sha512-EB0iwlKDGpG93hW8f85CTJTs4SvMX7tt5ceupvhALp1IF44SeUFOMhKUOYqpsoYWQKAOuTRDMqn75rEaKDp0Xw==
+
+"@ant-design/icons-vue@^6.1.0":
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/@ant-design/icons-vue/-/icons-vue-6.1.0.tgz#f9324fdc0eb4cea943cf626d2bf3db9a4ff4c074"
+  integrity sha512-EX6bYm56V+ZrKN7+3MT/ubDkvJ5rK/O2t380WFRflDcVFgsvl3NLH7Wxeau6R8DbrO5jWR6DSTC3B6gYFp77AA==
+  dependencies:
+    "@ant-design/colors" "^6.0.0"
+    "@ant-design/icons-svg" "^4.2.1"
+
 "@babel/code-frame@7.12.11":
   version "7.12.11"
   resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f"
@@ -918,7 +938,7 @@
     "@babel/types" "^7.4.4"
     esutils "^2.0.2"
 
-"@babel/runtime@^7.12.13", "@babel/runtime@^7.8.4":
+"@babel/runtime@^7.10.5", "@babel/runtime@^7.12.13", "@babel/runtime@^7.8.4":
   version "7.18.9"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a"
   integrity sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==
@@ -958,7 +978,7 @@
     "@babel/helper-validator-identifier" "^7.18.6"
     to-fast-properties "^2.0.0"
 
-"@ctrl/tinycolor@^3.4.1":
+"@ctrl/tinycolor@^3.4.0", "@ctrl/tinycolor@^3.4.1":
   version "3.4.1"
   resolved "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.4.1.tgz#75b4c27948c81e88ccd3a8902047bcd797f38d32"
   integrity sha512-ej5oVy6lykXsvieQtqZxCOaLT+xD4+QNarq78cIYISHmZXshCvROLudpQN3lfL8G0NL7plMSSK+zlyvCaIJ4Iw==
@@ -1129,6 +1149,14 @@
   resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df"
   integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==
 
+"@simonwep/pickr@~1.8.0":
+  version "1.8.2"
+  resolved "https://registry.yarnpkg.com/@simonwep/pickr/-/pickr-1.8.2.tgz#96dc86675940d7cad63d69c22083dd1cbb9797cb"
+  integrity sha512-/l5w8BIkrpP6n1xsetx9MWPWlU6OblN5YgZZphxan0Tq4BByTCETL6lyIeY8lagalS2Nbt4F2W034KHLIiunKA==
+  dependencies:
+    core-js "^3.15.1"
+    nanopop "^2.1.0"
+
 "@soda/friendly-errors-webpack-plugin@^1.8.0":
   version "1.8.1"
   resolved "https://registry.yarnpkg.com/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.1.tgz#4d4fbb1108993aaa362116247c3d18188a2c6c85"
@@ -1969,6 +1997,29 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
   dependencies:
     color-convert "^2.0.1"
 
+ant-design-vue@^3.2.11:
+  version "3.2.11"
+  resolved "https://registry.yarnpkg.com/ant-design-vue/-/ant-design-vue-3.2.11.tgz#034b2a2adef82a34440c10b90a5e02bcd25b376b"
+  integrity sha512-QKCAcOY5EJF0PepiVGA4X5PzUetYUvG5qALmA+2TON40pc2+brOEiVTwr3kjF9N+f7q4MpyiLPu4pIErwoajOQ==
+  dependencies:
+    "@ant-design/colors" "^6.0.0"
+    "@ant-design/icons-vue" "^6.1.0"
+    "@babel/runtime" "^7.10.5"
+    "@ctrl/tinycolor" "^3.4.0"
+    "@simonwep/pickr" "~1.8.0"
+    array-tree-filter "^2.1.0"
+    async-validator "^4.0.0"
+    dayjs "^1.10.5"
+    dom-align "^1.12.1"
+    dom-scroll-into-view "^2.0.0"
+    lodash "^4.17.21"
+    lodash-es "^4.17.15"
+    resize-observer-polyfill "^1.5.1"
+    scroll-into-view-if-needed "^2.2.25"
+    shallow-equal "^1.0.0"
+    vue-types "^3.0.0"
+    warning "^4.0.0"
+
 any-promise@^1.0.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
@@ -2004,6 +2055,11 @@ array-flatten@^2.1.2:
   resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099"
   integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==
 
+array-tree-filter@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/array-tree-filter/-/array-tree-filter-2.1.0.tgz#873ac00fec83749f255ac8dd083814b4f6329190"
+  integrity sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==
+
 array-union@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
@@ -2014,7 +2070,7 @@ astral-regex@^2.0.0:
   resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
   integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
 
-async-validator@^4.0.7:
+async-validator@^4.0.0, async-validator@^4.0.7:
   version "4.2.5"
   resolved "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz#c96ea3332a521699d0afaaceed510a54656c6339"
   integrity sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==
@@ -2469,6 +2525,11 @@ compression@^1.7.4:
     safe-buffer "5.1.2"
     vary "~1.1.2"
 
+compute-scroll-into-view@^1.0.17:
+  version "1.0.17"
+  resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.17.tgz#6a88f18acd9d42e9cf4baa6bec7e0522607ab7ab"
+  integrity sha512-j4dx+Fb0URmzbwwMUrhqWM2BEWHdFGx+qZ9qqASHRPqvTYdqvWnHg0H1hIbcyLnvgnoNAVMlwkepyqM3DaIFUg==
+
 concat-map@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@@ -2535,6 +2596,11 @@ core-js-compat@^3.21.0, core-js-compat@^3.22.1, core-js-compat@^3.8.3:
     browserslist "^4.21.2"
     semver "7.0.0"
 
+core-js@^3.15.1:
+  version "3.25.0"
+  resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.25.0.tgz#be71d9e0dd648ffd70c44a7ec2319d039357eceb"
+  integrity sha512-CVU1xvJEfJGhyCpBrzzzU1kjCfgsGUxhEvwUV2e/cOedYWHdmluamx+knDnmhqALddMG16fZvIqvs9aijsHHaA==
+
 core-js@^3.8.3:
   version "3.24.0"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.24.0.tgz#4928d4e99c593a234eb1a1f9abd3122b04d3ac57"
@@ -2706,6 +2772,11 @@ csstype@^2.6.8:
   resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.20.tgz#9229c65ea0b260cf4d3d997cb06288e36a8d6dda"
   integrity sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA==
 
+dayjs@^1.10.5:
+  version "1.11.5"
+  resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.5.tgz#00e8cc627f231f9499c19b38af49f56dc0ac5e93"
+  integrity sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==
+
 dayjs@^1.11.0:
   version "1.11.4"
   resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.4.tgz#3b3c10ca378140d8917e06ebc13a4922af4f433e"
@@ -2820,6 +2891,11 @@ doctrine@^3.0.0:
   dependencies:
     esutils "^2.0.2"
 
+dom-align@^1.12.1:
+  version "1.12.3"
+  resolved "https://registry.yarnpkg.com/dom-align/-/dom-align-1.12.3.tgz#a36d02531dae0eefa2abb0c4db6595250526f103"
+  integrity sha512-Gj9hZN3a07cbR6zviMUBOMPdWxYhbMI+x+WS0NAIu2zFZmbK8ys9R79g+iG9qLnlCwpFoaB+fKy8Pdv470GsPA==
+
 dom-converter@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768"
@@ -2827,6 +2903,11 @@ dom-converter@^0.2.0:
   dependencies:
     utila "~0.4"
 
+dom-scroll-into-view@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/dom-scroll-into-view/-/dom-scroll-into-view-2.0.1.tgz#0decc8522801fd8d3f1c6ba355a74d382c5f989b"
+  integrity sha512-bvVTQe1lfaUr1oFzZX80ce9KLDlZ3iU+XGNE/bz9HnGdklTieqsbmsLHe+rT2XWqopvL0PckkYqN7ksmm5pe3w==
+
 dom-serializer@^1.0.1:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30"
@@ -3859,6 +3940,11 @@ is-plain-obj@^3.0.0:
   resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7"
   integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==
 
+is-plain-object@3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-3.0.1.tgz#662d92d24c0aa4302407b0d45d21f2251c85f85b"
+  integrity sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==
+
 is-plain-object@^2.0.4:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
@@ -3942,7 +4028,7 @@ js-message@1.0.7:
   resolved "https://registry.yarnpkg.com/js-message/-/js-message-1.0.7.tgz#fbddd053c7a47021871bb8b2c95397cc17c20e47"
   integrity sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==
 
-js-tokens@^4.0.0:
+"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
   integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
@@ -4084,7 +4170,7 @@ locate-path@^5.0.0:
   dependencies:
     p-locate "^4.1.0"
 
-lodash-es@^4.17.21:
+lodash-es@^4.17.15, lodash-es@^4.17.21:
   version "4.17.21"
   resolved "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
   integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
@@ -4156,6 +4242,13 @@ log-update@^2.3.0:
     cli-cursor "^2.0.0"
     wrap-ansi "^3.0.1"
 
+loose-envify@^1.0.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.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"
+
 lower-case@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28"
@@ -4366,6 +4459,11 @@ nanoid@^3.3.4:
   resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
   integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
 
+nanopop@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/nanopop/-/nanopop-2.1.0.tgz#23476513cee2405888afd2e8a4b54066b70b9e60"
+  integrity sha512-jGTwpFRexSH+fxappnGQtN9dspgE2ipa1aOjtR24igG0pv6JCxImIAmrLRHX+zUF5+1wtsFVbKyfP51kIGAVNw==
+
 natural-compare@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@@ -5217,6 +5315,11 @@ requires-port@^1.0.0:
   resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
   integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==
 
+resize-observer-polyfill@^1.5.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
+  integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
+
 resolve-from@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
@@ -5331,6 +5434,13 @@ schema-utils@^4.0.0:
     ajv-formats "^2.1.1"
     ajv-keywords "^5.0.0"
 
+scroll-into-view-if-needed@^2.2.25:
+  version "2.2.29"
+  resolved "https://registry.yarnpkg.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.29.tgz#551791a84b7e2287706511f8c68161e4990ab885"
+  integrity sha512-hxpAR6AN+Gh53AdAimHM6C8oTN1ppwVZITihix+WqalywBeFcQ6LdQP5ABNl26nX8GTEL7VT+b8lKpdqq65wXg==
+  dependencies:
+    compute-scroll-into-view "^1.0.17"
+
 select-hose@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
@@ -5431,6 +5541,11 @@ shallow-clone@^3.0.0:
   dependencies:
     kind-of "^6.0.2"
 
+shallow-equal@^1.0.0:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/shallow-equal/-/shallow-equal-1.2.1.tgz#4c16abfa56043aa20d050324efa68940b0da79da"
+  integrity sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==
+
 shebang-command@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
@@ -5999,6 +6114,13 @@ vue-template-es2015-compiler@^1.9.0:
   resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
   integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
 
+vue-types@^3.0.0:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/vue-types/-/vue-types-3.0.2.tgz#ec16e05d412c038262fc1efa4ceb9647e7fb601d"
+  integrity sha512-IwUC0Aq2zwaXqy74h4WCvFCUtoV0iSWr0snWnE9TnU18S66GAQyqQbRf2qfJtUuiFsBf6qp0MEwdonlwznlcrw==
+  dependencies:
+    is-plain-object "3.0.1"
+
 vue@^3.2.13:
   version "3.2.37"
   resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.37.tgz#da220ccb618d78579d25b06c7c21498ca4e5452e"
@@ -6010,6 +6132,13 @@ vue@^3.2.13:
     "@vue/server-renderer" "3.2.37"
     "@vue/shared" "3.2.37"
 
+warning@^4.0.0:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
+  integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
+  dependencies:
+    loose-envify "^1.0.0"
+
 watchpack@^2.4.0:
   version "2.4.0"
   resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"