nieyuge 2 سال پیش
والد
کامیت
49bbb2e92b

+ 15 - 2
README.md

@@ -5,7 +5,20 @@ npm run dev
 
 ## 部署
 
+### 本地
+1. npm run dev
+
+### 测试
+1. npm run build-test
+2. npm run test
+3. pm2 start npm -- run test (保活)
+
+### 预发布
+1. npm run build-pre
+2. npm run pre
+3. pm2 start npm -- run pre (保活)
+
+### 线上
 1. npm run build
 2. npm run serve
-
-pm2 保活
+3. pm2 start npm -- run serve (保活)

+ 1 - 0
index.html

@@ -6,6 +6,7 @@
     <meta name="keywords" content="DeNet web3">
     <meta name="description" content="Best Giveaway / Airdrop Web3 Tool, An Awesome Twitter Marketing Tool">
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta name="referrer" content="no-referrer">
     <title>DeNet App</title>
     <!--preload-links-->
   </head>

+ 13 - 0
package-lock.json

@@ -314,6 +314,11 @@
         "delayed-stream": "~1.0.0"
       }
     },
+    "commander": {
+      "version": "9.4.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-9.4.0.tgz",
+      "integrity": "sha512-sRPT+umqkz90UA8M1yqYfnHlZA7fF6nSphDtxeywPZ49ysjxDQybzk13CL+mXekDRG92skbcqCLVovuCusNmFw=="
+    },
     "compressible": {
       "version": "2.0.18",
       "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
@@ -975,6 +980,14 @@
         "mime-db": "1.52.0"
       }
     },
+    "mockjs": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/mockjs/-/mockjs-1.1.0.tgz",
+      "integrity": "sha512-eQsKcWzIaZzEZ07NuEyO4Nw65g0hdWAyurVol1IPl1gahRwY+svqzfgfey8U8dahLwG44d6/RwEzuK52rSa/JQ==",
+      "requires": {
+        "commander": "*"
+      }
+    },
     "ms": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",

+ 9 - 2
package.json

@@ -5,17 +5,24 @@
   "scripts": {
     "dev": "vite",
     "test": "cross-env NODE_ENV=development node server",
+    "build-test": "npm run build-test:client && npm run build-test:server",
+    "build-test:client": "vite build --mode development --ssrManifest --outDir dist/client",
+    "build-test:server": "vite build --mode development --ssr src/entry-server.js --outDir dist/server",
+    "pre": "cross-env NODE_ENV=pre node server",
+    "build-pre": "npm run build-pre:client && npm run build-pre:server",
+    "build-pre:client": "vite build --mode pre --ssrManifest --outDir dist/client",
+    "build-pre:server": "vite build --mode pre --ssr src/entry-server.js --outDir dist/server",
     "serve": "cross-env NODE_ENV=production node server",
     "build": "npm run build:client && npm run build:server",
     "build:client": "vite build --ssrManifest --outDir dist/client",
-    "build:server": "vite build --ssr src/entry-server.js --outDir dist/server",
-    "preview": "vite preview"
+    "build:server": "vite build --ssr src/entry-server.js --outDir dist/server"
   },
   "dependencies": {
     "animate.css": "^4.1.1",
     "axios": "^0.27.2",
     "element-plus": "^2.2.9",
     "js-cookie": "^3.0.1",
+    "mockjs": "^1.1.0",
     "vue": "^3.2.25",
     "vue-router": "^4.0.15"
   },

+ 25 - 25
server.js

@@ -21,46 +21,46 @@ async function createServer(
      * @type {import('vite').ViteDevServer}
      */
     let vite
-    if (!isProd) {
-        vite = await require('vite').createServer({
-            root,
-            logLevel: isTest ? 'error' : 'info',
-            server: {
-                middlewareMode: 'ssr',
-                watch: {
-                    // During tests we edit the files too fast and sometimes chokidar
-                    // misses change events, so enforce polling for consistency
-                    usePolling: true,
-                    interval: 100
-                }
-            }
-        })
-        // use vite's connect instance as middleware
-        app.use(vite.middlewares)
-    } else {
+    // if (!isProd) {
+    //     vite = await require('vite').createServer({
+    //         root,
+    //         logLevel: isTest ? 'error' : 'info',
+    //         server: {
+    //             middlewareMode: 'ssr',
+    //             watch: {
+    //                 // During tests we edit the files too fast and sometimes chokidar
+    //                 // misses change events, so enforce polling for consistency
+    //                 usePolling: true,
+    //                 interval: 100
+    //             }
+    //         }
+    //     })
+    //     // use vite's connect instance as middleware
+    //     app.use(vite.middlewares)
+    // } else {
         app.use(require('compression')())
         app.use(
             require('serve-static')(resolve('dist/client'), {
                 index: false
             })
         )
-    }
+    // }
 
     app.use('*', async (req, res) => {
         try {
             const url = req.originalUrl
 
             let template, render
-            if (!isProd) {
-                // always read fresh template in dev
-                template = fs.readFileSync(resolve('index.html'), 'utf-8')
-                template = await vite.transformIndexHtml(url, template)
-                render = (await vite.ssrLoadModule('/src/entry-server.js')).render
-            } else {
+            // if (!isProd) {
+            //     // always read fresh template in dev
+            //     template = fs.readFileSync(resolve('index.html'), 'utf-8')
+            //     template = await vite.transformIndexHtml(url, template)
+            //     render = (await vite.ssrLoadModule('/src/entry-server.js')).render
+            // } else {
                 template = indexProd
                 // @ts-ignore
                 render = require('./dist/server/entry-server.js').render
-            }
+            // }
 
             const [appHtml, preloadLinks] = await render(url, manifest)
 

+ 306 - 0
src/components/currency-list.vue

@@ -0,0 +1,306 @@
+<template>
+    <template v-if="!isSelect">
+        <div class="header">
+            <input
+                class="input" 
+                @input="onInput"
+                v-model="keywords"
+                placeholder="Search name" />
+            <img
+                class="icon-clear"
+                src="../static/img/icon-clear-search.svg" 
+                v-if="keywords"
+                @click="clearIpt" >
+        </div>
+        <div class="list-wrapper" :class="{min: showSearch}">
+            <template v-if="!showSearch">
+                <div
+                    class="list-item"
+                    :key="index"
+                    v-for="(item, index) in currencyInfoList">
+                    <div
+                        class="item-title"
+                        v-if="item.data.length">
+                        <template v-if="item.type == 1">
+                            <img class="icon" src="../static/img/icon-currency-category-01.svg" />
+                            <span>Cash</span>
+                        </template>
+                        <template v-else>
+                            <img class="icon" src="../static/img/icon-currency-category-02.svg" />
+                            <span>Crypto</span>
+                        </template>
+                    </div>
+                    <div
+                        class="item-detail"
+                        v-for="(data, idx) in item.data"
+                        :key="idx"
+                        @click="selectCurrency(data)">
+                        <div class="left">
+                            <img class="icon-currency" :src="data.currencies[0].iconPath" />
+                            <div class="currency-info">
+                                <div class="name">{{ data.currencies[0].currencyCode == 'USD' ? 'USD' : data.currencies[0].tokenSymbol }}</div>
+                                <div class="desc">{{ data.currencies[0].currencyCode == 'USD' ? 'Paypal' : data.currencies[0].currencyName }}
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                <div class="no-data" v-if="show_empty">
+                    Not found
+                </div>
+            </template>
+            <template v-else>
+                <div
+                    class="item-detail"
+                    :key="idx"
+                    v-for="(data, idx) in searchList"
+                    @click="selectCurrency(data)">
+                    <div class="left">
+                        <img class="icon-currency" :src="data.currencies[0].iconPath" />
+                        <div class="currency-info">
+                            <div class="name">{{ data.currencies[0].currencyCode == 'USD' ? 'USD' : data.currencies[0].tokenSymbol }}</div>
+                            <div class="desc">{{ data.currencies[0].currencyName }}</div>
+                        </div>
+                    </div>
+                </div>
+                <div class="no-data" v-if="!searchList.length">
+                    Not found
+                </div>
+            </template>
+        </div>
+    </template>
+    <template v-else>
+        <div class="list-wrapper">
+            <div class="list-item">
+                <div
+                    :key="idx"
+                    v-for="(data, idx) in isSelectData"
+                    @click="selectItem(data)">
+                    <div class="item-title">
+                        <img class="icon" :src="data.chainInfo.iconPath" />
+                        {{data.chainInfo.chainName}}
+                    </div>
+                    <div class="item-detail">
+                        <div class="left">
+                            <img class="icon-currency" :src="data?.iconPath" />
+                            <div class="currency-info">
+                                <div class="name">{{ data.currencyCode == 'USD' ? 'USD' : data.tokenSymbol }}</div>
+                                <div class="desc">{{ data.currencyCode == 'USD' ? 'Paypal' : data.currencyName }}</div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </template>
+</template>
+
+<script lang="ts" setup>
+import { ref, onBeforeMount, defineEmits } from 'vue';
+import Api from '../static/http/api';
+import { postRequest } from '../static/http';
+import { debounce } from '../static/utils';
+
+const keywords = ref('');
+const showSearch = ref(false);
+const currencyInfoList = ref([]);
+const searchList = ref([]);
+const show_empty = ref(false);
+const isSelect = ref(false);
+const isSelectData = ref(null);
+const listReqParams = {
+    params: {
+        pageNum: 1,
+        pageSize: 100,
+    },
+    loadMore: false,
+};
+const emits = defineEmits(['selectCurrencyItem'])
+
+const clearIpt = () => {
+    keywords.value = '';
+    showSearch.value = false;
+    searchList.value = [];
+}
+const onInput = (val: any) => {
+    console.log(keywords.value);
+    if (keywords.value) {
+        showSearch.value = true;
+        searchCurrency(keywords.value);
+    } else {
+        showSearch.value = false;
+        searchList.value = [];
+    }
+}
+
+const searchCurrency = debounce(function (searchWords: any) {
+    postRequest(Api.searchCurrencyInfo, {
+        params: {
+            pageNum: 1,
+            pageSize: 100,
+            searchWords,
+            filterFiatCurrency: false
+        }
+    }).then(res => {
+        if (res.code == 0) {
+            if (res.data.currencyCategories && res.data.currencyCategories.length) {
+                let list = res.data.currencyCategories[0];
+                if (list && list.data && list.data.length) {
+                    searchList.value = list.data;
+                } else {
+                    searchList.value = [];
+                }
+            } else {
+                searchList.value = [];
+            }
+        }
+    })
+}, 500)
+
+const getCurrencyInfoList = () => {
+    postRequest(Api.getCurrencyInfo, {
+        params: {
+            pageNum: listReqParams.params.pageNum,
+            pageSize: listReqParams.params.pageSize,
+            filterFiatCurrency: true,
+            filterEmptyBalance: false
+        }
+    }).then(res => {
+        if (res.code == 0) {
+            let resData = res.data;
+            if (resData && resData.currencyCategories.length) {
+                if (listReqParams.params.pageNum < 2) {
+                    currencyInfoList.value = res.data.currencyCategories;   
+                    if(resData.currencyCategories.length == 1 && (!resData.currencyCategories[0]['data'] || !resData.currencyCategories[0]['data'].length)) {
+                        show_empty.value = true
+                    }
+                } else {
+                    let data = currencyInfoList.value;
+                    let currencyCategories = resData.currencyCategories;
+                    data = data.concat(currencyCategories);
+                    currencyInfoList.value = data;
+                }
+                listReqParams.loadMore = false;
+            } else {
+                show_empty.value = true
+            }
+        }
+    })
+}
+
+const selectItem = (data: any) => {
+    emits('selectCurrencyItem', data);
+}
+
+const selectCurrency = (data: any) => {
+    if (data.currencies.length > 1) {
+        isSelect.value = true;
+        isSelectData.value = data.currencies;
+    } else {
+        emits('selectCurrencyItem', data.currencies[0]);
+    }
+}
+
+onBeforeMount(() => {
+    getCurrencyInfoList()
+})
+
+</script>
+
+<style lang="less" scoped>
+.header {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    height: 60px;
+    background: #F7F7F7;
+    .input {
+        width: calc(100% - 20px);
+        height: calc(100% - 20px);
+        background: #f1f1f1;
+        border-radius: 110px;
+        box-sizing: border-box;
+        border: none;
+        outline: none;
+        padding: 10px 80px 10px 22px;
+        color: #adadad;
+    }
+    input::placeholder {
+        color: #adadad;
+    }
+    .icon-clear {
+        position: absolute;
+        right: 6%;
+        cursor: pointer;
+    }
+}
+
+.list-wrapper {
+    overflow-y: auto;
+    max-height: calc(420px - 60px);
+    &.min {
+        min-height: 200px;
+    }
+    .list-item {
+        .item-title {
+            display: flex;
+            align-items: center;
+            height: 42px;
+            padding: 0 10px;
+            box-sizing: border-box;
+            background: #fafafa;
+            font-size: 14px;
+            color: #a2a2a2;
+
+            .icon {
+                width: 24px;
+                height: 24px;
+                margin-right: 6px;
+            }
+        }
+    }
+    .item-detail {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        padding: 12px 20px;
+        box-sizing: border-box;
+        cursor: pointer;
+
+        .left {
+            display: flex;
+
+            .icon-currency {
+                width: 24px;
+                height: 24px;
+                margin-right: 12px;
+                margin-top: 4px;
+            }
+
+            .currency-info {
+                .name {
+                    font-weight: 500;
+                    font-size: 15px;
+                    margin-bottom: 5px;
+                    word-break: break-all;
+                }
+
+                .desc {
+                    font-weight: 400;
+                    font-size: 12px;
+                    color: #a2a2a2;
+                }
+            }
+        }
+    }
+    .no-data {
+        font-weight: 500;
+        font-size: 22px;
+        color: #BBBBBB;
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+    }
+}
+</style>

+ 0 - 2
src/components/footer.vue

@@ -27,8 +27,6 @@
 </template>
 
 <script lang="ts" setup>
-import { postRequest } from '../static/http'
-
 const twitter = ()  => {
     window.open(`https://twitter.com/denet2022`);
 }

+ 64 - 18
src/components/header.vue

@@ -2,10 +2,10 @@
     <div class="header">
         <img class="logo" src="../static/img/logo.svg" alt="DeNet" />
         <div class="operation">
-            <!-- <div class="login" @click="twitterAuth">
-                <img class="add" src="../static/img/header-add.svg" alt="">
+            <div class="login" @click="login">
+                <img class="add" src="../static/img/header-add.svg" alt="" />
                 <span>Create NFTs</span>
-            </div> -->
+            </div>
             <div class="down" @click="install">
                 <div class="text">Install DeNet</div>
             </div>
@@ -17,26 +17,70 @@
 <script lang="ts" setup>
 import Api from '../static/http/api'
 import { postRequest } from '../static/http'
-import { getMid, appVersionCode, getOauthUrl, createWindow, callBackUrl } from '../static/utils'
+import { getOauthUrl, createWindow, callBackUrl } from '../static/utils'
 import { getStorage, removeStorage, setStorage, storageKey } from '../static/utils/storage'
 import { ref } from 'vue'
-import { useRouter } from 'vue-router'
 import { ElMessage } from 'element-plus'
+import { Report } from '../static/report'
+import { businessType, pageSource, objectType } from '../static/report/enum'
 
 const timer = ref(0)
 
-const router = useRouter()
-
 const install =  () => {
     window.open(`https://chrome.google.com/webstore/detail/denet/inlfbeejfdgkknpiodhemfcokbdgofja`);
+    // Report
+    Report({
+        baseInfo: {
+            pageSource: pageSource.homePage,
+        },
+        params: {
+            eventData: {
+                businessType: businessType.buttonClick,
+                objectType: objectType.installDenetButton,
+            }
+        }
+    })
 }
 
-const twitterAuth = () => {
-    postRequest(Api.twitterRequestToken, {
+const checkInstall = () => {
+    return new Promise((resolve, reject) => {
+        // chrome-extension://inlfbeejfdgkknpiodhemfcokbdgofja/img/icon-denet-logo.svg
+        let dom = document.querySelector('#denet_message');
+        if (dom) {
+            resolve(true)
+        } else {
+            reject(false)
+        }
+    })
+}
+
+const login = () => {
+    checkInstall().then(() => {
+        let userInfo = getStorage(storageKey.userInfo);
+        if (userInfo) {
+            location.href = `/nft/list`
+        } else {
+            twitterAuth()
+        }
+    }).catch(() => {
+        install()
+    })
+    // Report
+    Report({
         baseInfo: {
-            mid: getMid(),
-            appVersionCode,
+            pageSource: pageSource.homePage,
         },
+        params: {
+            eventData: {
+                businessType: businessType.buttonClick,
+                objectType: objectType.createNftsButton,
+            }
+        }
+    })
+}
+
+const twitterAuth = () => {
+    postRequest(Api.twitterRequestToken, {
         params: {
             oauthCallback: callBackUrl
         }
@@ -53,7 +97,6 @@ const twitterAuth = () => {
             }, 500)
         } else {
             ElMessage({
-                offset: 100,
                 type: 'error',
                 message: msg,
             })
@@ -65,21 +108,22 @@ const twitterLogin = (data: { authToken: string, consumerKey: string }) => {
     let verifier = getStorage(storageKey.verifier)
     if (verifier) {
         postRequest(Api.twitterLogin,  {
-            baseInfo: {
-                mid: getMid(),
-                appVersionCode,
-            },
             params: {
                 consumerKey: data.consumerKey,
                 oauthToken: data.authToken,
                 oauthVerifier: verifier
             }
         }).then(res => {
-            let { code, data } = res;
+            let { code, data, msg } = res;
             if ( code === 0 ) {
                 setStorage(storageKey.userInfo, data);
                 removeStorage(storageKey.verifier);
-                router.push(`/nft/list`)
+                location.href = `/nft/list`
+            } else {
+                ElMessage({
+                    type: 'error',
+                    message: msg,
+                })
             }
         })
     }
@@ -116,6 +160,7 @@ const twitterLogin = (data: { authToken: string, consumerKey: string }) => {
         cursor: pointer;
         padding: 0 18px;
         font-size: 15px;
+        font-weight: 600;
         color: #1D9BF0;
         margin-right: 19px;
         border-radius: 20px;
@@ -138,6 +183,7 @@ const twitterLogin = (data: { authToken: string, consumerKey: string }) => {
         .text {
             color: #fff;
             font-size: 15px;
+            font-weight: 600;
             margin-top: -2px;
         }
     }

+ 9 - 4
src/pages/close.vue

@@ -1,6 +1,6 @@
 <template>
     <div class="welcome">
-        <span class="text">授权成功</span>
+        <span class="text">Success</span>
     </div>
 </template>
 
@@ -21,7 +21,7 @@ onMounted(() => {
     if (verifier) {
         setStorage(storageKey.verifier, verifier)
         // @ts-ignore
-        let time = process.env.NODE_ENV === 'production' ? 500 : 2000;
+        let time = process.env.NODE_ENV === 'production' ? 200 : 500;
         setTimeout(() => {
             close()
         }, time)
@@ -29,7 +29,11 @@ onMounted(() => {
 })
 </script>
 
-<style lang="less" scoped>
+<style lang="less">
+body {
+    background-color: #F5F5F5;
+}
+
 .welcome {
     display: flex;
     align-items: center;
@@ -37,7 +41,8 @@ onMounted(() => {
     width: 100%;
     height: 100%;
     .text {
-        font-size: .5rem;
+        font-size: 22px;
+        color: #1D9BF0;
     }
 }
 </style>

+ 27 - 1
src/pages/index.vue

@@ -72,10 +72,17 @@
 
 <script setup lang="ts">
 import { onMounted } from 'vue';
+import { useRoute } from 'vue-router'
+import { Report } from '../static/report'
+import { businessType, pageSource } from '../static/report/enum'
+import { setStorage, storageKey } from '../static/utils/storage'
+import { getCookie, removeCookie } from '../static/utils'
 import headerLayer from '../components/header.vue';
 import footerLayer from '../components/footer.vue';
 
-function install() {
+const route = useRoute()
+
+const install = () => {
     window.open(`https://chrome.google.com/webstore/detail/denet/inlfbeejfdgkknpiodhemfcokbdgofja`);
 }
 
@@ -104,6 +111,25 @@ onMounted(() => {
             observer.observe(el);
         });
     }
+
+    // plugin login
+    let userInfo = getCookie(storageKey.userInfo);
+    if (userInfo) {
+        setStorage(storageKey.userInfo, JSON.parse(userInfo));
+        removeCookie(storageKey.userInfo)
+    }
+
+    // Report
+    Report({
+        baseInfo: {
+            pageSource: pageSource.homePage,
+        },
+        params: {
+            eventData: {
+                businessType: businessType.pageView,
+            }
+        }
+    })
 })
 </script>
 

+ 539 - 86
src/pages/nft/add.vue

@@ -4,58 +4,80 @@
         <div class="content">
             <div class="l">
                 <div class="name">Preview</div>
-                <div class="show">
-                    <div class="card">
-                        <div class="absolute">
-                            <div class="logo"></div>
-                            <div class="member">LegalDAO Members</div>
-                            <div class="number">0001</div>
+                <template v-if="configList.length">
+                    <div class="show">
+                        <div class="card" :class="selectItem.modelName">
+                            <div class="logo">
+                                <img v-if="tempUrl" :src="tempUrl" alt="">
+                            </div>
+                            <div class="member">{{ projectName === '' ? 'xxxx' : projectName }}</div>
+                            <div class="number">{{ projectNo === '' ? '0000' : projectNo }}</div>
                         </div>
-                        <img class="bg" src="" alt="" />
+                        <img class="bg" :src="selectItem.mainImagePath" />
                     </div>
-                </div>
-                <div class="list">
-                    <div class="item on"></div>
-                    <div class="item"></div>
-                    <div class="item"></div>
-                    <div class="item"></div>
+                    <div class="list">
+                        <div
+                            class="item"
+                            :class="{ on: item.modelName === selectItem.modelName }"
+                            @click="select(item, index)"
+                            v-for="(item, index) in configList"
+                            :key="index">
+                            <img :src="item.mainImagePath" alt="" />
+                        </div>
+                    </div>
+                </template>
+                <div class="wait" v-else>
+                    <img width="30" src="../../static/img/icon-loading-gray.png" alt="" />
                 </div>
             </div>
             <div class="r">
                 <div class="name">Card face</div>
                 <div class="face">
-                    <div class="off" v-if="false">
-                        <img src="../../static/img/icon-add-cover-off.svg" alt="" />
-                    </div>
-                    <div class="on" v-else>
+                    <input type="file" class="file" @change="uploadImg" accept="image/png,image/jpg,image/jpeg"  />
+                    <div class="on" v-if="tempUrl">
                         <img src="../../static/img/icon-add-cover-on.svg" alt="" />
-                        <img class="img" src="" alt="" />
+                        <img class="img" :src="tempUrl" alt="" />
+                    </div>
+                    <div class="off" v-else>
+                        <img src="../../static/img/icon-add-cover-off.svg" alt="" />
                     </div>
                 </div>
                 <div class="desc">Recommended size 500*500 or more</div>
                 <div class="name">Project Name</div>
-                <div class="input"><input type="text" /></div>
+                <div class="input"><input type="text" maxlength="30" v-model="projectName" placeholder="Your NFT Project Name" /></div>
 
                 <div class="name">Project Description</div>
-                <div class="input"><input type="text" /></div>
+                <div class="textarea"><textarea placeholder="Your NFT Project Description" maxlength="250" v-model="projectDesc"></textarea></div>
 
                 <div class="name">Collection Size</div>
-                <div class="input"><input type="text" /></div>
+                <div class="input"><input type="text" v-model="projectSize" placeholder="0" /></div>
+                <div class="showNo" v-if="showNoStr">No.{{ projectNo === '' ? '0000' : projectNo }}~{{projectSize}}</div>
 
                 <div class="name">NFTs Price</div>
                 <div class="price">
-                    <div class="currency">
-                        <img class="head" src="" alt="" />
-                        <div class="font">SHBI</div>
+                    <div class="currency" @click="showCurrencyDialog" v-if="currencyItem">
+                        <img class="head" :src="currencyItem.iconPath" alt="" />
+                        <div class="font">{{currencyItem.tokenSymbol}}</div>
                         <img class="arrow" src="../../static/img/icon-add-arrow.svg" alt="" />
                     </div>
-                    <div class="input"><input type="text" /></div>
+                    <div class="no-select" @click="showCurrencyDialog" v-else>
+                        <div class="font">Select a reward</div>
+                        <img class="arrow" src="../../static/img/icon-add-arrow-white.svg" alt="" />
+                    </div>
+                    <div class="input">
+                        <div class="tips" v-if="showMinPrice">the price of each NFT must be above $0.1 USD</div>
+                        <input type="text" v-model="projectPrice" @input="changePrice" placeholder="0" />
+                    </div>
+                    <!-- 货币列表 -->
+                    <div class="currency-pop" v-if="currencyDialog">
+                        <currency-list @selectCurrencyItem="selectCurrencyItem"></currency-list>
+                    </div>
                 </div>
 
                 <div class="explain">
                     <ul>
+                        <li class="special">The NFT colection is minted on BNB Smart Chain (BEP20)</li>
                         <li>NFT will be released in blind box mode</li>
-                        <li>Users can buy 5 NFTs in one go, there will be a 20% discount</li>
                         <li>Users need to pay service fees when transferring NFTs</li>
                     </ul>
                 </div>
@@ -63,15 +85,18 @@
             </div>
         </div>
         <div class="create">
-            <button>Create</button>
+            <button class="on" v-if="isNext" @click="next">Create</button>
+            <button class="off" v-else>Create</button>
         </div>
     </div>
 
-    <div class="feedBack">
-        <div class="mail">
-            <img src="../../static/img/icon-feedback.svg" alt="" />
-        </div>
-        <div class="font">Feedback</div>
+    <div class="feedBack" @click="feedback">
+        <a href="mailto:service@cybertogether.net">
+            <div class="mail">
+                <img src="../../static/img/icon-feedback.svg" alt="" />
+            </div>
+            <div class="font">Feedback</div>
+        </a>
     </div>
 
     <div class="loading" v-if="showLoading">
@@ -80,17 +105,284 @@
 
     <div class="succ" v-if="showSuccess">
         <img class="icon" src="../../static/img/icon-notice-succ.svg" alt="" />
-        <div class="notic">Your NFTs are Created</div>
-        <button class="btn">Done</button>
+        <div class="notic">We have created your NFT collection</div>
+        <button class="btn"  @click="jumpList">Done</button>
     </div>
     <div class="succ-bg" v-if="showSuccess"></div>
+
+    <div class="mask-bg" v-if="currencyDialog" @click="hideCurrencyDialog"></div>
 </template>
 
 <script lang="ts" setup>
-import { ref } from 'vue';
+import { ref, onMounted, watchEffect } from 'vue';
+import Api from '../../static/http/api';
+import { postRequest } from '../../static/http';
+import { uploadFile } from '../../static/utils/upload'
+import { Report } from '../../static/report'
+import { debounce } from '../../static/utils'
+import { businessType, pageSource, objectType } from '../../static/report/enum'
+import currencyList from '../../components/currency-list.vue';
 
+const isNext = ref(false);
 const showLoading = ref(false);
-const showSuccess = ref(false)
+const showSuccess = ref(false);
+const maxSize = ref(100000);
+const minUsdAmount = ref(0)
+const configList = ref([]);
+const selectItem = ref(null);
+const uploadItem = ref({});
+const currencyDialog = ref(false);
+const currencyItem = ref(null);
+const projectName = ref('');
+const projectDesc = ref('');
+const projectSize = ref('');
+const projectNo = ref('');
+const projectPrice = ref('');
+const showNoStr = ref(false);
+const showMinPrice = ref(false);
+const tempFile = ref('');
+const tempUrl = ref('');
+const buttonType = {
+    feedback: 'feedback-button',
+    create: 'create-button',
+}
+
+const getConfig = () => {
+    postRequest(Api.createConfig).then(res => {
+        let { code, data } = res;
+        if ( code === 0 ) {
+            configList.value = data.itemModels;
+            maxSize.value = data.maxCollectionSize;
+            selectItem.value = data.itemModels[0];
+            minUsdAmount.value = data.sellMinUsdAmount;
+
+            // 预加载图片
+            if (data.itemModels) {
+                data.itemModels.forEach(item => {
+                    let img = new Image()
+                        img.src = item.previewImagePath;
+                })
+            }
+        }
+    })
+}
+
+const next = () => {
+    // show loading
+    showLoading.value = true;
+    uploadFile(tempFile.value).then(res => {
+        // @ts-ignore
+        uploadItem.value = res;
+        // post 
+        postRequest(Api.userNftAdd, {
+            params: {
+                // 项目图标
+                cardFaceImagePath: uploadItem.value && uploadItem.value.objectKey || '',
+                // 发行数量
+                nftCollectionSize: projectSize.value || '',
+                // 选中的模版id
+                nftItemImageModel: selectItem.value && selectItem.value.modelName || '',
+                // 项目描述
+                nftProjectDescription: projectDesc.value.trim() || '',
+                // 项目名称
+                nftProjectName: projectName.value.trim() || '',
+                // 销售价格
+                saleCurrencyAmount: projectPrice.value || '',
+                // 销售的货币code
+                saleCurrencyCode: currencyItem.value && currencyItem.value.currencyCode || '',
+            }
+        }).then(res => {
+            if (res.code === 0) {
+                showSuccess.value = true;
+            }
+        }).finally(() => {
+            showLoading.value = false;
+        })
+
+        // Report
+        Report({
+            baseInfo: {
+                pageSource: pageSource.creatorPage,
+            },
+            params: {
+                eventData: {
+                    businessType: businessType.buttonClick,
+                    objectType: buttonType.create,
+                }
+            }
+        })
+    }).catch(() => {
+        showLoading.value = false;
+    })
+}
+
+const select = (item: any, index: number) => {
+    selectItem.value = item;
+    let objectType;
+    switch(index) {
+        case 0:
+            objectType = `first-NFT-selection-button`;
+            break;
+        case 1:
+            objectType = `second-NFT-selection-button`;
+            break;
+        case 2:
+            objectType = `third-NFT-selection-button`;
+            break;
+        case 3:
+            objectType = `forth-NFT-selection-button`;
+            break;
+    }
+    // Report
+    Report({
+        baseInfo: {
+            pageSource: pageSource.creatorPage,
+        },
+        params: {
+            eventData: {
+                businessType: businessType.buttonClick,
+                objectType: objectType,
+            }
+        }
+    })
+}
+
+const showCurrencyDialog = () => {
+    currencyDialog.value = true;
+}
+
+const hideCurrencyDialog = () => {
+    currencyDialog.value = false;
+}
+
+const selectCurrencyItem = (data: any) => {
+    currencyItem.value = data;
+    hideCurrencyDialog();
+}
+
+const uploadImg = (e: any) => {
+    let file = e.target.files[0];
+    // 清空file
+    e.target.value = '';
+    // 预览
+    tempFile.value = file;
+    tempUrl.value = URL.createObjectURL(file);
+}
+
+const hideSuccess = () => {
+    showSuccess.value = false;
+}
+
+const jumpList = () => {
+    location.href = `/nft/list`
+}
+
+const changePrice = (e: any) => {
+    let val = projectPrice.value;
+
+    val = val.replace(/^\D*(\d*(?:\.\d{0,18})?).*$/g, '$1');
+
+    if (val == '00') {
+        val = '0'
+    }
+
+    if (val.indexOf('.') > -1){
+        let arr = val.split('.');
+        if(arr[0].startsWith('0')) {
+            let num = +arr[0];
+            val = num + '.' + arr[1];
+        }
+    }
+
+    if (val !== '') {
+        projectPrice.value = val
+    } else {
+        projectPrice.value = ''
+    }
+}
+
+const feedback = () => {
+    // Report
+    Report({
+        baseInfo: {
+            pageSource: pageSource.creatorPage,
+        },
+        params: {
+            eventData: {
+                businessType: businessType.buttonClick,
+                objectType: buttonType.feedback,
+            }
+        }
+    })
+}
+
+let timer = ref(0);
+
+watchEffect(() => {
+    let emojiReg = /[\uD83C|\uD83D|\uD83E][\uDC00-\uDFFF][\u200D|\uFE0F]|[\uD83C|\uD83D|\uD83E][\uDC00-\uDFFF]|[0-9|*|#]\uFE0F\u20E3|[0-9|#]\u20E3|[\u203C-\u3299]\uFE0F\u200D|[\u203C-\u3299]\uFE0F|[\u2122-\u2B55]|\u303D|[\A9|\AE]\u3030|\uA9|\uAE|\u3030/gi;
+
+    // 数量
+    let num = projectSize.value;
+    num = num.replace(/\D/g, '');
+    if (Number(num) > maxSize.value) {
+        num = String(maxSize.value);
+    }
+    if (num) {
+        projectSize.value = String(Number(num));
+    } else {
+        projectSize.value = '';
+    }
+    
+    // 编号
+    if (projectSize.value !== '') {
+        projectNo.value = String(1).padStart(String(projectSize.value).length, '0')
+    } else {
+        projectNo.value = ''
+    }
+    showNoStr.value = projectNo.value != '' && Number(projectSize.value) > 0;
+
+    // 是否可以创建
+    let ifUpload = tempUrl.value != '' || false;
+    let ifName = projectName.value !== '';
+    let ifDesc = projectDesc.value !== '';
+    let ifSize = projectSize.value !== '' && Number(projectSize.value) > 0;
+    let ifCurrency = currencyItem.value && currencyItem.value.currencyCode;
+    let ifPrice = projectPrice.value && projectPrice.value !== '' && Number(projectPrice.value) > 0;
+
+    // setTimeout
+    clearTimeout(timer.value);
+    timer.value = setTimeout(() => {
+        projectName.value = projectName.value.replace(emojiReg, '');
+        // 最低金额
+        if (ifCurrency && ifPrice) {
+            let usdPrice = currencyItem.value && currencyItem.value.usdPrice;
+            if (Number(usdPrice) * Number(projectPrice.value) < Number(minUsdAmount.value)) {
+                showMinPrice.value = true;
+            } else {
+                showMinPrice.value = false;
+            }
+        }
+
+        // show next
+        isNext.value = ifUpload && ifName && ifDesc && ifSize && ifCurrency && ifPrice && !showMinPrice.value;
+    }, 400)
+})
+
+onMounted(() => {
+    // config
+    getConfig()
+    // Report
+    Report({
+        baseInfo: {
+            pageSource: pageSource.creatorPage,
+        },
+        params: {
+            eventData: {
+                businessType: businessType.pageView,
+            }
+        }
+    })
+})
 </script>
 
 
@@ -98,12 +390,15 @@ const showSuccess = ref(false)
 .center {
     overflow-y: auto;
     padding: 0 50px;
-    max-width: 1100px;
+    width: 1100px;
     margin: 10px auto 0;
     box-sizing: border-box;
     height: calc(100% - 10px);
     background-color: #fff;
     border-radius: 20px 20px 0 0;
+    &::-webkit-scrollbar {
+        width: 0 !important;
+    }
     .title {
         padding: 22px 0;
         font-size: 20px;
@@ -117,82 +412,127 @@ const showSuccess = ref(false)
         justify-content: space-between;
         .name {
             height: 24px;
+            font-size: 12px;
+            color: #939393;
+        }
+        .showNo {
             color: #939393;
+            font-size: 12px;
+            margin-top: -10px;
+            margin-bottom: 24px;
         }
         .l {
             width: 400px;
+            .wait {
+                padding: 200px 0;
+                text-align: center;
+                img {
+                    opacity: .6;
+                    animation: rotate 1s infinite linear;
+                }
+            }
             .show {
-                display: flex;
-                align-items: center;
-                justify-content: center;
+                position: relative;
+                overflow: hidden;
+                width: 400px;
                 height: 400px;
-                border-radius: 5px;
-                background: #F2F2F2;
-                border: 1px solid #F0F0F0;
+                border-radius: 10px;
                 .card {
-                    position: relative;
-                    overflow: hidden;
-                    width: 336px;
-                    height: 189px;
-                    border-radius: 12px;
-                    background: #353535;
-                    .absolute {
+                    position: absolute;
+                    left: 53px;
+                    top: 103px;
+                    width: 294px;
+                    height: 186px;
+                    .logo {
                         position: absolute;
-                        top: 0;
-                        left: 0;
-                        width: 100%;
-                        height: 100%;
-                        text-align: center;
-                        .logo {
-                            margin: auto;
-                            width: 80px;
-                            height: 80px;
-                            margin-top: 20px;
-                            margin-bottom: 12px;
+                        top: 50%;
+                        left: 50%;
+                        transform: translate(-50%, -50%);
+                        width: 100px;
+                        height: 100px;
+                        border-radius: 50%;
+                        background-color: #fff;
+                        img {
+                            width: 100%;
+                            height: 100%;
                             border-radius: 50%;
-                            background-color: #225dab;
+                            object-fit: cover;
                         }
-                        .member {
-                            color: #fff;
-                            font-size: 28px;
-                            font-weight: 800;
-                            line-height: 33px;
+                    }
+                    .member {
+                        position: absolute;
+                        top: 11px;
+                        left: 11px;
+                        width: 228px;
+                        font-size: 12px;
+                        text-align: left;
+                        font-weight: 800;
+                        line-height: 13px;
+                    }
+                    .number {
+                        position: absolute;
+                        top: 11px;
+                        right: 10px;
+                        font-size: 12px;
+                        font-weight: 800;
+                        line-height: 13px;
+                        letter-spacing: 1px;
+                    }
+                    &.s1 {
+                        .member, .number {
+                            color: #ffffff;
                         }
-                        .number {
-                            margin-top: 4px;
-                            font-size: 24px;
-                            font-weight: 800;
-                            letter-spacing: 0.12px;
-                            color: rgba(255, 255, 255, .4);
+                    }
+                    &.s2 {
+                        .member, .number {
+                            color: #4AC3E1;
                         }
                     }
-                    
-                    .bg {
-                        width: 100%;
-                        height: 100%;
+                    &.s3 {
+                        .member, .number {
+                            color: #606C94;
+                        }
+                    }
+                    &.s4 {
+                        .member, .number {
+                            color: #504215;
+                        }
                     }
                 }
+                .bg {
+                    width: 100%;
+                    height: 100%;
+                }
             }
             .list {
                 margin-top: 20px;
                 display: flex;
                 .item {
                     width: calc(100% / 4);
-                    height: 90px;
                     cursor: pointer;
+                    overflow: hidden;
                     margin-right: 13px;
                     border-radius: 5px;
-                    background: #F2F2F2;
-                    border: 1px solid rgba(0, 0, 0, 0.1);
+                    img {
+                        width: 100%;
+                        border-radius: 5px;
+                    }
                     &:last-child {
                         margin: 0;
                     }
                     &.on {
-                        border: 1px solid rgba(0, 0, 0, 0.1);
-                        background-color: #D2D2D2;
-                        background-repeat: no-repeat;
-                        background-position: right top;
-                        background-image: url('../../static/img/icon-add-select.svg');
+                        position: relative;
+                        &::before {
+                            position: absolute;
+                            display: block;
+                            top: 0;
+                            right: 0;
+                            content: '';
+                            width: 44px;
+                            height: 44px;
+                            background-size: 100%;
+                            background-image: url('../../static/img/icon-add-select.svg');
+                        }
                     }
                 }
             }
@@ -200,7 +540,17 @@ const showSuccess = ref(false)
         .r {
             width: 520px;
             .face {
+                position: relative;
+                width: 80px;
                 margin-bottom: 10px;
+                .file {
+                    position: absolute;
+                    z-index: 2;
+                    width: 100%;
+                    height: 100%;
+                    opacity: 0;
+                    cursor: pointer;
+                }
                 .off {
                     display: flex;
                     align-items: center;
@@ -240,6 +590,7 @@ const showSuccess = ref(false)
                 margin-bottom: 22px;
             }
             .input {
+                position: relative;
                 display: flex;
                 align-items: center;
                 justify-content: center;
@@ -252,13 +603,50 @@ const showSuccess = ref(false)
                     border: 0;
                     outline: none;
                     padding: 3px 0;
+                    color: #777;
                     font-size: 16px;
                     font-weight: 500;
                     letter-spacing: 0.3px;
                     width: calc(100% - 24px);
+                    &::placeholder {
+                        color: rgba(0, 0, 0, .3);
+                    }
+                }
+                .tips {
+                    position: absolute;
+                    left: 0;
+                    top: -22px;
+                    font-size: 12px;
+                    color: #E29015;
+                }
+            }
+            .textarea {
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                height: 72px;
+                margin-bottom: 22px;
+                border-radius: 5px;
+                background: #FFFFFF;
+                border: 1px solid #E0E0E0;
+                textarea {
+                    resize: none;
+                    border: 0;
+                    height: 52px;
+                    outline: none;
+                    color: #777;
+                    font-size: 16px;
+                    font-weight: 500;
+                    letter-spacing: 0.3px;
+                    width: calc(100% - 24px);
+                    font-family: "Segoe UI", Helvetica, Arial, sans-serif;
+                    &::placeholder {
+                        color: rgba(0, 0, 0, .3);
+                    }
                 }
             }
             .price {
+                position: relative;
                 display: flex;
                 justify-content: space-between;
                 .currency {
@@ -292,10 +680,47 @@ const showSuccess = ref(false)
                         margin-left: 8px
                     }
                 }
+                .no-select {
+                    display: flex;
+                    align-items: center;
+                    justify-content: center;
+                    flex-direction: row;
+                    cursor: pointer;
+                    height: 43px;
+                    padding: 0 14px;
+                    margin-right: 13px;
+                    border-radius: 25px;
+                    background: #1d9bf0;
+                    .font {
+                        max-width: 250px;
+                        color: #fff;
+                        font-size: 15px;
+                        font-weight: 500;
+                        white-space: nowrap;
+                        overflow: hidden;
+                        text-overflow: ellipsis;
+                    }
+                    .arrow {
+                        margin-left: 8px
+                    }
+                }
                 .input {
                     flex: 1;
                     margin-bottom: 0;
                 }
+
+                .currency-pop {
+                    position: absolute;
+                    overflow: hidden;
+                    z-index: 3;
+                    left: 0;
+                    bottom: 50px;
+                    width: 375px;
+                    max-height: 420px;
+                    border-radius: 20px;
+                    background-color: #fff;
+                    box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.25);
+                }
             }
             .explain {
                 color: #B4B4B4;
@@ -306,6 +731,9 @@ const showSuccess = ref(false)
                     padding: 0;
                     margin-left: 14px;
                 }
+                .special {
+                    color: #E29015;
+                }
             }
             .footer {
                 height: 80px;
@@ -323,7 +751,7 @@ const showSuccess = ref(false)
         box-sizing: border-box;
         background-color: #fff;
         border-top: solid 1px #ECECEC;
-        button {
+        .on {
             border: 0;
             height: 50px;
             color: #FFFFFF;
@@ -335,6 +763,18 @@ const showSuccess = ref(false)
             border-radius: 25px;
             background: #1D9BF0;
         }
+        .off {
+            border: 0;
+            height: 50px;
+            color: #FFFFFF;
+            cursor: pointer;
+            padding: 0 50px;
+            font-size: 18px;
+            font-weight: 700;
+            letter-spacing: 0.3px;
+            border-radius: 25px;
+            background: #E4E4E4;
+        }
     }
 }
 
@@ -343,7 +783,12 @@ const showSuccess = ref(false)
     right: 44px;
     bottom: 88px;
     cursor: pointer;
+    font-size: 12px;
     text-align: center;
+    a:link, a:visited {
+        color: #A8A8A8;
+        text-decoration: none;
+    }
     .mail {
         display: flex;
         align-items: center;
@@ -432,4 +877,12 @@ const showSuccess = ref(false)
     height: 100%;
     background: rgba(0, 0, 0, .8);
 }
+.mask-bg {
+    position: absolute;
+    top: 0;
+    left: 0;
+    z-index: 2;
+    width: 100%;
+    height: 100%;
+}
 </style>

+ 0 - 16
src/pages/nft/index.vue

@@ -2,22 +2,6 @@
     <router-view></router-view>
 </template>
 
-<script lang="ts" setup>
-import { onMounted } from 'vue'
-import { useRouter } from 'vue-router'
-import { getStorage, storageKey } from '../../static/utils/storage'
-
-let router = useRouter()
-
-onMounted(() => {
-    let userInfo = getStorage(storageKey.userInfo);
-    if (!userInfo) {
-        location.href = '/'
-    }
-})
-</script>
-
-
 <style lang="less">
 body {
     overflow: hidden;

+ 369 - 28
src/pages/nft/list.vue

@@ -9,21 +9,33 @@
                 <div class="logout" @click="logout">Sign out</div>
             </div>
             <div class="list">
-                <div class="item">
-                    <div class="logo">
-                        <img src="" alt="" />
+                <template v-if="pageList.length">
+                    <div class="item" v-for="(item, index) in pageList" :key="index">
+                        <div class="logo">
+                            <img :src="item.nftCardFaceImage" alt="" />
+                        </div>
+                        <div class="desc">
+                            <div class="name">{{item.nftProjectName}}</div>
+                            <div class="font">Sell <span>{{item.sellCount}}/{{item.totalCount}}</span></div>
+                            <div class="font">Earnings <span>{{item.earningAmount}} {{item.currencySymbol}}</span></div>
+                        </div>
+                        <div class="opt">
+                            <template v-if="item.publishStatus === 1">
+                                <el-button link type="danger" @click="unpublish(item)">Unlist</el-button>
+                                <el-button link type="primary" @click="view">View</el-button>
+                            </template>
+                            <template v-else>
+                                <el-button link type="danger" @click="remove(item)">Delete</el-button>
+                                <el-button link type="primary" @click="publish(item)">Sell</el-button>
+                            </template>
+                        </div>
                     </div>
-                    <div class="desc">
-                        <div class="name">LegalDAO Members</div>
-                        <div class="font">Sell <span>175/1000</span></div>
-                        <div class="font">Earnings <span>107.5 USDT</span></div>
+                </template>
+                <template v-else>
+                    <div class="empty">
+                        <img src="../../static/img/icon-empty.svg" alt="" />
                     </div>
-                    <div class="opt">
-                        <el-button link type="danger">Delete</el-button>
-                        <el-button link type="primary">Publish</el-button>
-                    </div>
-                </div>
-                <!-- <el-empty description="description" /> -->
+                </template>
             </div>
             <div class="add" @click="add">
                 <img src="../../static/img/header-add.svg" alt="">
@@ -34,10 +46,13 @@
 
     <!-- publish -->
     <div class="publish" v-if="publishDialog">
-        <div class="msg">Irrevocable after publish</div>
+        <div class="msg">
+            <template v-if="publishType === 1">You are going to list the NFT collection</template>
+            <template v-else>Do you wish to unlist the NFT collection</template>
+        </div>
         <div class="buttons">
-            <button>Cancel</button>
-            <button class="confirm">Continue</button>
+            <button @click="hidePublishLayer">Cancel</button>
+            <button class="confirm" @click="confirmPublishLayer">Continue</button>
         </div>
     </div>
     <div class="bg" v-if="publishDialog"></div>
@@ -46,37 +61,322 @@
     <div class="publish delete" v-if="deleteDialog">
         <div class="msg">Once deleted, it cannot be restored</div>
         <div class="buttons">
-            <button>Cancel</button>
-            <button class="confirm">Continue</button>
+            <button @click="hideDeleteLayer">Cancel</button>
+            <button class="confirm" @click="confirmDeleteLayer">Continue</button>
         </div>
     </div>
     <div class="bg" v-if="deleteDialog"></div>
 
+    <div class="feedBack" @click="feedback">
+        <a href="mailto:service@cybertogether.net">
+            <div class="mail">
+                <img src="../../static/img/icon-feedback.svg" alt="" />
+            </div>
+            <div class="font">Feedback</div>
+        </a>
+    </div>
 </template>
 
 <script lang="ts" setup>
-import { onMounted, ref } from 'vue'
-import { useRouter } from  'vue-router'
-import { getStorage, storageKey, removeStorage } from '../../static/utils/storage'
-
-const userInfo: any = ref({});
-
-const router = useRouter()
+import { onMounted, ref, nextTick } from 'vue'
+import { useRoute } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import Api from '../../static/http/api';
+import { postRequest } from '../../static/http'
+import { getStorage, setStorage, storageKey, removeStorage } from '../../static/utils/storage'
+import { Report } from '../../static/report'
+import { businessType, pageSource, objectType } from '../../static/report/enum'
 
+const userInfo: any = ref({})
+const route = useRoute()
+const publishType = ref(0)
+const publishItem = ref(null)
 const publishDialog = ref(false)
+
+const deleteItem = ref(null)
 const deleteDialog = ref(false)
 
+const pageNum = ref(1)
+const pageSize = 1000
+const pageList = ref([])
+const buttonType = {
+    publish: 'publish-button',
+    delete: 'delete-button',
+    unlist: 'unlist-button',
+    view: 'view-button',
+    feedback: 'feedback-button',
+    cancel: 'cancel-button',
+    continue: 'continue-button',
+}
+
 const logout = () => {
     removeStorage(storageKey.userInfo)
-    router.push({ name: 'home' })
+    location.href = `/`
 }
 
 const add = () => {
-    router.push({ name: 'nft-add' })
+    location.href = `/nft/add`
+}
+
+const remove = (item: any) => {
+    deleteItem.value = JSON.parse(JSON.stringify(item));
+    showDeleteLayer()
+    // Report
+    Report({
+        baseInfo: {
+            pageSource: pageSource.managerPage,
+        },
+        params: {
+            eventData: {
+                businessType: businessType.buttonClick,
+                objectType: buttonType.delete,
+            }
+        }
+    })
+}
+
+const publish = (item: any) => {
+    publishType.value = 1;
+    publishItem.value = item;
+    showPublishLayer()
+    // Report
+    Report({
+        baseInfo: {
+            pageSource: pageSource.managerPage,
+        },
+        params: {
+            eventData: {
+                businessType: businessType.buttonClick,
+                objectType: buttonType.publish,
+            }
+        }
+    })
+}
+
+const unpublish = (item: any) => {
+    publishType.value = 2;
+    publishItem.value = item;
+    showPublishLayer()
+    // Report
+    Report({
+        baseInfo: {
+            pageSource: pageSource.managerPage,
+        },
+        params: {
+            eventData: {
+                businessType: businessType.buttonClick,
+                objectType: buttonType.unlist,
+            }
+        }
+    })
+}
+
+const view = () => {
+    let userInfo = getStorage(storageKey.userInfo);
+    let nickName = userInfo && userInfo.nickName || '';
+    // open
+    window.open(`https://twitter.com/${nickName}`);
+    // Report
+    Report({
+        baseInfo: {
+            pageSource: pageSource.managerPage,
+        },
+        params: {
+            eventData: {
+                businessType: businessType.buttonClick,
+                objectType: buttonType.view,
+            }
+        }
+    })
+}
+
+const feedback = () => {
+    // Report
+    Report({
+        baseInfo: {
+            pageSource: pageSource.managerPage,
+        },
+        params: {
+            eventData: {
+                businessType: businessType.buttonClick,
+                objectType: buttonType.feedback,
+            }
+        }
+    })
+}
+
+const getList = () => {
+    postRequest(Api.userNftList, {
+        params: {
+            pageNum: pageNum.value,
+            pageSize: pageSize
+        }
+    }).then(res => {
+        console.log(333, res)
+        let { code, data } = res;
+        if (code === 0) {
+            pageList.value = data;
+        }
+    })
+}
+
+const hideDeleteLayer = () => {
+    deleteDialog.value = false;
+}
+
+const showDeleteLayer = () => {
+    deleteDialog.value = true;
+}
+
+const confirmDeleteLayer = () => {
+    postRequest(Api.userNftDel, {
+        params: {
+            nftProjectId: deleteItem.value.nftProjectId,
+        }
+    }).then(res => {
+        let { code, msg } = res;
+        if ( code === 0 ) {
+            // filter
+            pageList.value.some((item, index) => {
+                if (item.nftProjectId === deleteItem.value.nftProjectId) {
+                    pageList.value.splice(index, 1);
+                    return true;
+                }
+            })
+            ElMessage({
+                type: 'success',
+                message: 'Delete Success!',
+            })
+            
+        } else {
+            ElMessage({
+                type: 'error',
+                message: msg,
+            })
+        }
+        hideDeleteLayer()
+    })
+}
+
+const hidePublishLayer = (ifReport = true) => {
+    publishDialog.value = false;
+    if (publishType.value === 1 && ifReport) {
+        // Report
+        Report({
+            baseInfo: {
+                pageSource: pageSource.confirmationPage,
+            },
+            params: {
+                eventData: {
+                    businessType: businessType.buttonClick,
+                    objectType: buttonType.cancel,
+                }
+            }
+        })
+    }
+}
+
+const showPublishLayer = () => {
+    publishDialog.value = true;
+}
+
+const confirmPublishLayer = () => {
+    let item = JSON.parse(JSON.stringify(publishItem.value));
+    let status = publishType.value === 1 ? 1 : 0;
+    // request
+    postRequest(Api.userNftSetStatus, {
+        params: {
+            nftProjectId : item.nftProjectId,
+            publishStatus : status
+        }
+    }).then(res => {
+        let { code, msg } = res;
+        if ( code === 0 ) {
+            // list
+            getList()
+
+            ElMessage({
+                type: 'success',
+                message: publishType.value === 1 ? 'Published Successfully!' : 'We have listed your NFT collection!',
+            })
+            if (publishType.value === 1) {
+                // Report
+                Report({
+                    baseInfo: {
+                        pageSource: pageSource.confirmationPage,
+                    },
+                    params: {
+                        eventData: {
+                            businessType: businessType.buttonClick,
+                            objectType: buttonType.continue,
+                        },
+                        extParams: {
+                            Publish: 'success'
+                        }
+                    }
+                })
+            }
+        } else {
+            if (code === 5004) {
+                ElMessage({
+                    type: 'error',
+                    message: `You can list at most one NFT collection, unlist one collection to list another.`
+                })
+            } else {
+                ElMessage({
+                    type: 'error',
+                    message: msg
+                })
+            }
+            if (publishType.value === 1) {
+                // Report
+                Report({
+                    baseInfo: {
+                        pageSource: pageSource.confirmationPage,
+                    },
+                    params: {
+                        eventData: {
+                            businessType: businessType.buttonClick,
+                            objectType: buttonType.continue,
+                        },
+                        extParams: {
+                            Publish: 'fail'
+                        }
+                    }
+                })
+            }
+        }
+        hidePublishLayer(false)
+    })
 }
 
 onMounted(() => {
-    userInfo.value = getStorage(storageKey.userInfo);
+    // jump login
+    let str = route.query && route.query.params || ''
+    if (str) {
+        // @ts-ignore
+        let params = JSON.parse(atob(str));
+        setStorage(storageKey.userInfo, params)
+        userInfo.value = params;
+    } else {
+        userInfo.value = getStorage(storageKey.userInfo);
+    }
+
+    nextTick(() => {
+        // 获取列表
+        getList()
+        // Report
+        Report({
+            baseInfo: {
+                pageSource: pageSource.managerPage,
+            },
+            params: {
+                eventData: {
+                    businessType: businessType.pageView,
+                }
+            }
+        })
+    })
 })
 </script>
 
@@ -126,6 +426,12 @@ onMounted(() => {
         margin: 20px;
         height: 360px;
         overflow-y: auto;
+        .empty {
+            position: absolute;
+            top: 50%;
+            left: 50%;
+            transform: translate(-50%, -50%);
+        }
         .item {
             display: flex;
             align-items: center;
@@ -137,11 +443,17 @@ onMounted(() => {
                 margin-bottom: unset;
             }
             .logo {
+                overflow: hidden;
                 width: 70px;
                 height: 70px;
                 margin: 0 24px;
                 border-radius: 50%;
                 background-color: #f5f5f5;
+                img {
+                    width: 100%;
+                    height: 100%;
+                    object-fit: cover;
+                }
             }
             .desc {
                 flex: 1;
@@ -242,4 +554,33 @@ onMounted(() => {
     height: 100%;
     background: rgba(0, 0, 0, .8);
 }
+
+.feedBack {
+    position: absolute;
+    right: 44px;
+    bottom: 88px;
+    cursor: pointer;
+    font-size: 12px;
+    text-align: center;
+    a:link, a:visited {
+        color: #A8A8A8;
+        text-decoration: none;
+    }
+    .mail {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        margin: auto;
+        width: 50px;
+        height: 50px;
+        margin-bottom: 10px;
+        border-radius: 50%;
+        background-color: #fff;
+    }
+    .font {
+        opacity: 0.7;
+        color: #A8A8A8;
+        letter-spacing: 0.3px;
+    }
+}
 </style>

+ 8 - 0
src/static/http/api.ts

@@ -1,4 +1,12 @@
 export default {
     'twitterLogin' : '/denet/user/twitterLogin',
     'twitterRequestToken' : '/denet/user/twitterRequestToken',
+    'userNftAdd' : '/denet/nft/project/create/new',
+    'userNftDel' : '/denet/nft/project/create/delete',
+    'userNftList' : '/denet/nft/project/create/list',
+    'userNftSetStatus' : '/denet/nft/project/create/setPublishStatus',
+    'createConfig' : '/denet/nft/project/create/config',
+    'mediaUpload' : '/denet/media/uploadSignature',
+    'getCurrencyInfo' : '/denet/currency/v2/getCurrencyInfo',
+    'searchCurrencyInfo' : '/denet/currency/v2/searchCurrencyInfo',
 }

+ 22 - 3
src/static/http/index.ts

@@ -1,6 +1,9 @@
 // http封装库
-import { getEnvConfig } from '../utils';
 import axios from 'axios';
+import { removeStorage, storageKey } from '../utils/storage'
+import { getEnvConfig, getMid, getUserInfo, appVersionCode, appType } from '../utils';
+// 测试数据(需手动开启关闭)
+// import '../mockjs/index';
 
 // axios config
 const { host } = getEnvConfig();
@@ -15,17 +18,33 @@ const instance = axios.create({
 
 // 响应拦截器
 instance.interceptors.response.use((res) => {
-    return res.data;
+    if (res.data.code === -107) {
+        // token失效
+        removeStorage(storageKey.userInfo);
+        location.href = `/`;
+    } else {
+        return res.data;
+    }
 }, function (err) {
     return Promise.reject(err);
 });
 
 export const postRequest = (url: string, params = {}, config = null) => {
     const myConfig = {};
+    const userInfo = getUserInfo();
     if (config) {
         Object.assign(myConfig, config);
     }
-    params = Object.assign({}, params);
+    params = Object.assign({
+        baseInfo: {
+            mid: getMid(),
+            machineCode: getMid(),
+            loginUid: userInfo && userInfo.uid || '',
+            token: userInfo && userInfo.accessToken || '',
+            appType,
+            appVersionCode,
+        },
+    }, params);
 
     return instance.post(url, params, myConfig).then(res => {
         return res;

+ 3 - 0
src/static/img/icon-add-arrow-white.svg

@@ -0,0 +1,3 @@
+<svg width="13" height="13" viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10.7246 4.34766L6.37193 9L2.01925 4.34766" stroke="white" stroke-width="1.5"/>
+</svg>

+ 3 - 3
src/static/img/icon-add-select.svg

@@ -1,4 +1,4 @@
-<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M0 0H30V30L0 0Z" fill="#1D9BF0"/>
-<path d="M15.3984 9.79688L19.1143 13.7604L25.3074 5.83331" stroke="white" stroke-width="1.875"/>
+<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M0 0H39C41.7614 0 44 2.23858 44 5V44L0 0Z" fill="#1D9BF0"/>
+<path d="M26.3984 12.7969L30.8101 17.5027L38.163 8.09106" stroke="white" stroke-width="2.5"/>
 </svg>

+ 3 - 0
src/static/img/icon-clear-search.svg

@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12 23C18.0751 23 23 18.0751 23 12C23 5.92487 18.0751 1 12 1C5.92487 1 1 5.92487 1 12C1 18.0751 5.92487 23 12 23ZM15.4115 7.31454L16.5933 8.49625L13.1818 11.9995L16.9753 15.6988L15.7935 16.8806L12.0001 13.1812L8.15904 16.8806L6.97733 15.6988L10.8184 11.9995L7.17299 8.49625L8.3547 7.31454L12.0001 10.8178L15.4115 7.31454Z" fill="#BCBCBC"/>
+</svg>

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 0
src/static/img/icon-currency-category-01.svg


+ 3 - 0
src/static/img/icon-currency-category-02.svg

@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12.1243 5.01728L12.1 5.00382L12.0757 5.01728L6.75674 7.96928L6.67795 8.01301L6.75674 8.05672L9.46574 9.55972L9.4994 9.57839L9.52606 9.55064C10.1741 8.87593 11.0883 8.45 12.1 8.45C13.1117 8.45 14.0259 8.87593 14.6739 9.55064L14.7006 9.57839L14.7343 9.55972L17.4433 8.05672L17.5221 8.01301L17.4433 7.96928L12.1243 5.01728ZM11.1757 18.4877L11.25 18.5289V18.444V15.483V15.4441L11.2123 15.4345C9.67711 15.0441 8.55 13.6507 8.55 12C8.55 11.6886 8.5856 11.3875 8.66502 11.1139L8.67613 11.0757L8.64129 11.0563L5.82429 9.4903L5.75 9.449V9.534V15.447V15.4764L5.77574 15.4907L11.1757 18.4877ZM12.95 18.444V18.5289L13.0243 18.4877L18.4243 15.4907L18.45 15.4764V15.447V9.534V9.449L18.3757 9.4903L15.5587 11.0563L15.5242 11.0755L15.5349 11.1135C15.6144 11.3964 15.65 11.6977 15.65 12.009C15.65 13.6597 14.5229 15.0531 12.9877 15.4435L12.95 15.4531V15.492V18.444ZM12.1 3.0572L20.15 7.52942V16.4706L12.1 20.9428L4.05 16.4706V7.52942L12.1 3.0572ZM12.1 10.15C11.0824 10.15 10.25 10.9824 10.25 12C10.25 13.0176 11.0824 13.85 12.1 13.85C13.1176 13.85 13.95 13.0176 13.95 12C13.95 10.9824 13.1176 10.15 12.1 10.15Z" fill="#1D9BF0" stroke="#F7F7F7" stroke-width="0.1"/>
+</svg>

BIN
src/static/img/icon-loading-gray.png


+ 13 - 0
src/static/mockjs/index.ts

@@ -0,0 +1,13 @@
+// @ts-ignore
+import mockJs from 'mockjs';
+import Api from '../http/api';
+import { getEnvConfig } from '../utils';
+import { userNftList, userNftDel, userNftSetStatus } from './nft-data';
+
+// const
+const { host } = getEnvConfig();
+
+// mock
+mockJs.mock(host + Api.userNftList, 'post', userNftList);
+mockJs.mock(host + Api.userNftDel, 'post', userNftDel);
+mockJs.mock(host + Api.userNftSetStatus, 'post', userNftSetStatus);

+ 24 - 0
src/static/mockjs/nft-data.ts

@@ -0,0 +1,24 @@
+export const userNftList = {
+    'code' : 0,
+    'msg' : '',
+    'data|20' : [{
+        "currencySymbol" : "@character('$¥€')",
+        "earningAmount" : "@integer(2000, 5000)",
+        "nftCardFaceImage" : "@image('70x70')",
+        "nftProjectId" : "@integer(20000, 500000000000)",
+        "nftProjectName" : "@word",
+        "publishStatus|1" : [0, 1],
+        "sellCount" : "@integer(1, 5000)",
+        "totalCount" : 5000
+    }]
+};
+
+export const userNftDel = {
+    'code|1' : [0, 1],
+    "msg": "@word"
+}
+
+export const userNftSetStatus = {
+    'code|1' : [0, 1],
+    "msg": "@word"
+}

+ 22 - 0
src/static/report/enum.ts

@@ -0,0 +1,22 @@
+export const logType = {
+    //denet-event-log
+    'denet': '150',
+}
+
+export const businessType = {
+    buttonView: "buttonView",
+    buttonClick: "buttonClick",
+    pageView: "pageView",
+}
+
+export const objectType = {
+    createNftsButton: 'create-nfts-button',
+    installDenetButton: 'install-denet-button',
+}
+
+export const pageSource = {
+    homePage: 'denet-web-home-page',
+    managerPage: 'denet-nft-manager-page',
+    creatorPage: 'denet-nft-creator-page',
+    confirmationPage: 'denet-nft-manager-publish-confirmation-page',
+}

+ 50 - 0
src/static/report/index.ts

@@ -0,0 +1,50 @@
+import axios from 'axios';
+import { logType } from './enum';
+import { getBrowser, getEnvConfig, getMid, getUserInfo, appVersionCode, appType } from '../utils';
+
+const { logHost } = getEnvConfig();
+const logAPIUrl = logHost + '/log-center';
+
+export function Report(params: any) {
+    let baseInfo = params.baseInfo || {}
+    let {eventData = {}, extParams = {}} = params.params ||  {
+        params: {}
+    }
+    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();
+    let userInfo = getUserInfo();
+    let extData = {
+        url: location.href,
+        browser,
+        platform,
+        twitterId: userInfo && userInfo.nickName || '',
+        ...eventData,
+    }
+    eventData = wrapObject(extData)
+    params.baseInfo = {
+        mid: getMid(),
+        machineCode: getMid(),
+        loginUid: userInfo && userInfo.uid || '',
+        token: userInfo && userInfo.accessToken || '',
+        appType,
+        appVersionCode,
+        ...baseInfo,
+    }
+    params.params.logType = logType.denet;
+    params.params.eventData = JSON.stringify(eventData)
+    params.params.extParams  = JSON.stringify(extParams)
+    
+    axios.post(`${logAPIUrl}/statistics/uploadLogFromFrontend`, params)
+}
+
+function wrapObject(extParams: any) {
+    if (typeDecide(extParams, 'Object')) {
+        return extParams
+    }
+    return { 'defaultExt': extParams }
+}
+
+function typeDecide(o: any, type: string) {
+    return Object.prototype.toString.call(o) === `[object ${type}]`;
+}

+ 53 - 4
src/static/utils/index.ts

@@ -1,30 +1,35 @@
 // @ts-ignore
 import Cookie from 'js-cookie';
+import { getStorage, storageKey } from './storage'
 
-export const appVersionCode = 12;
+export const appVersionCode = 17;
 export const appType = 1;
 // @ts-ignore
-export const callBackUrl = process.env.NODE_ENV === `production` ? `https://denet.me/close` : `https://test.denet.me/close`
+export const callBackUrl = process.env.NODE_ENV === `production` ? `https://denet.me/close` : process.env.NODE_ENV === `pre` ? `https://pre.denet.me/close` : `https://test.denet.me/close`
 
 // 获取host
 export const getEnvConfig = () => {
-    let host
+    let host, logHost
 
     // @ts-ignore
     switch(process.env.NODE_ENV) {
         case `production`:
             host = `https://api.denetme.net`
+            logHost = `https://log.weiqumeta.com`
             break;
         case `pre`:
             host = `https://preapi.denetme.net`
+            logHost = `https://prelog.weiqumeta.com`
             break;
         default:
             host = `https://testapi.denetme.net`
+            logHost = `https://testlog.weiqumeta.com`
             break;
     }
 
     return {
-        host
+        host,
+        logHost,
     };
 }
 
@@ -41,6 +46,15 @@ export const getMid = () => {
     return _mid;
 }
 
+export const getUserInfo = () => {
+    let userInfo = getStorage(storageKey.userInfo) || null;
+    if (userInfo) {
+        return userInfo;
+    } else {
+        return null
+    }
+}
+
 // 推特授权url
 export const getOauthUrl = (token: string) => {
     return `https://api.twitter.com/oauth/authenticate?oauth_token=${token}`
@@ -74,4 +88,39 @@ export const getCookie = (name: string) => {
 // 设置cookie
 export const setCookie = (name: string, val: any) => {
     Cookie.set(name, JSON.stringify(val), { expires: 1000 })
+}
+
+// 删除cookie
+export const removeCookie = (name: string) => {
+    Cookie.remove(name)
+}
+
+export function debounce(fn: any, delay: number) {
+    let timer: number; // 定时器
+    return function (...args) {
+        let context = this;
+        timer && clearTimeout(timer);
+        timer = setTimeout(function () {
+            fn.apply(context, args);
+        }, delay);
+    };
+}
+
+export function getBrowser() {
+    let browser;
+    let UserAgent = navigator.userAgent.toLowerCase();
+    if (UserAgent.indexOf('chrome') > -1 || UserAgent.indexOf('crios') > -1) {
+        browser = `Chrome`
+    } else if (UserAgent.indexOf('firefox') > -1) {
+        browser = `Firefox`
+    } else if (UserAgent.indexOf('opera') > -1) {
+        browser = `Opera`
+    } else if (UserAgent.indexOf('safari') > -1 && UserAgent.indexOf('chrome') == -1) {
+        browser = `Safari`
+    } else if (UserAgent.indexOf('edge') > -1) {
+        browser = `Edge`
+    } else {
+        browser = `Other`
+    }
+    return browser;
 }

+ 69 - 0
src/static/utils/upload.ts

@@ -0,0 +1,69 @@
+import axios from 'axios';
+import Api from '../http/api';
+import { postRequest } from '../http'
+import { ElMessage } from 'element-plus'
+
+export const uploadFile = async (file: any) => {
+    let imgInfo = await getImgInfo(file);
+    let status = await upload(imgInfo, file);
+    if (status) {
+        return imgInfo
+    } else {
+        return false;
+    }
+}
+
+const getImgInfo = (file: any) => {
+    return new Promise((resolve) => {
+        let type = file.type || 'image/jpg';
+        postRequest(Api.mediaUpload, {
+            params: {
+                bizType: 2,
+                contentType: type,
+                fileSuffix: type.split('/')[1],
+                fileType: 1,
+            }
+        }).then(res => {
+            let { code, data } = res;
+            if ( code === 0 ) {
+                resolve(data)
+            }
+        }).catch(error => {
+            ElMessage({
+                type: 'error',
+                message: 'network error!'
+            })
+        })
+    })
+}
+ 
+const upload = (params: any, file: any) => {
+    let type = file.type || 'image/jpg';
+    return new Promise((resolve) => {
+        axios({
+            method: 'PUT',
+            url: params.url,
+            data: new Blob([file]),
+            headers: {
+                'Authorization': params.authorization,
+                'x-amz-date': params.date,
+                'Content-Type': type,
+            }
+        }).then(res => {
+            let { status } = res
+            if (status == 200) {
+                resolve(true)
+            } else {
+                ElMessage({
+                    type: 'error',
+                    message: 'upload error!'
+                })
+            }
+        }).catch(error => {
+            ElMessage({
+                type: 'error',
+                message: 'upload error!'
+            })
+        })
+    })
+}

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است