-
5.env
-
7config/webpackDevServer.config.js
-
8env.md
-
2package.json
-
2src/App.tsx
-
35src/api/axios_config.ts
-
15src/api/index.ts
-
65src/api/service.ts
-
10src/assets/iconfont/iconfont.css
-
2src/assets/iconfont/iconfont.js
-
7src/assets/iconfont/iconfont.json
-
BINsrc/assets/iconfont/iconfont.ttf
-
BINsrc/assets/iconfont/iconfont.woff
-
BINsrc/assets/iconfont/iconfont.woff2
-
BINsrc/assets/tabbar/tabbar-3-o.png
-
BINsrc/assets/tabbar/tabbar-3.png
-
3src/global.d.ts
-
87src/hooks/useConnectWallet.ts
-
1src/index.tsx
-
43src/pages/cart/index.tsx
-
43src/pages/home/index.tsx
-
87src/pages/personal/index.tsx
-
71src/pages/share/index.tsx
-
56src/router/index.tsx
-
78src/router/layout/ConnectButton.tsx
-
29src/router/layout/Navbar.tsx
-
0src/router/layout/Notify.tsx
-
6src/router/layout/RenderRouter.tsx
-
36src/router/layout/Tabbar.tsx
-
47src/router/layout/index.tsx
-
12src/router/routes.tsx
-
64src/store/index.ts
-
60src/styles/cart.scss
-
60src/styles/home.scss
-
21src/styles/layout.scss
-
80src/styles/personal.scss
-
44src/styles/share.scss
-
12src/types/api.d.ts
-
2src/types/index.ts
-
6src/types/store.d.ts
-
14src/utils/index.ts
-
21src/utils/sign/sign.ts
-
35src/utils/sign/sort.ts
-
23yarn.lock
@ -0,0 +1,5 @@ |
|||||
|
SKIP_PREFLIGHT_CHECK=true |
||||
|
GENERATE_SOURCEMAP=false |
||||
|
REACT_APP_BASE_URL='http://14.29.101.215:30304' |
||||
|
REACT_APP_SHARE_LINK='http://14.29.101.215:30305/#/' |
||||
|
REACT_APP_SIGN_KEY='finance_ad123' |
@ -0,0 +1,8 @@ |
|||||
|
``` |
||||
|
SKIP_PREFLIGHT_CHECK=true |
||||
|
GENERATE_SOURCEMAP=false |
||||
|
|
||||
|
<!-- dev --> |
||||
|
REACT_APP_BASE_URL='http://14.29.101.215:30304' |
||||
|
REACT_APP_SHARE_LINK='http://14.29.101.215:30305/#/' |
||||
|
REACT_APP_SIGN_KEY='finance_ad123' |
@ -0,0 +1,35 @@ |
|||||
|
export default { |
||||
|
baseURL: process.env.NODE_ENV === 'development' ? '/api' : process.env.REACT_APP_BASE_URL + '/api', |
||||
|
method: "post", |
||||
|
//`timeout`选项定义了请求发出的延迟毫秒数
|
||||
|
//如果请求花费的时间超过延迟的时间,那么请求会被终止
|
||||
|
timeout: 60 * 1000, |
||||
|
//发送请求前允许修改数据
|
||||
|
transformRequest: [ |
||||
|
function (data: any) { |
||||
|
return data; |
||||
|
}, |
||||
|
], |
||||
|
//数据发送到then/catch方法之前允许数据改动
|
||||
|
transformResponse: [ |
||||
|
function (data: any) { |
||||
|
return data; |
||||
|
}, |
||||
|
], |
||||
|
// headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
|
headers: { "Content-Type": "application/json; charset=UTF-8" }, |
||||
|
// withCredentials: false,//跨域请求时是否携带cookie
|
||||
|
responseType: "json", //响应数据类型
|
||||
|
// xsrfCookieName: 'XSRF-TOKEN',
|
||||
|
// xsrfHeaderName: 'X-XSRF-TOKEN',
|
||||
|
onUploadProgress: function (progressEvent: any) {}, //上传进度事件
|
||||
|
onDownloadProgress: function (progressEvent: any) {}, //下载进度事件
|
||||
|
//`validateStatus`定义了是否根据http相应状态码,来resolve或者reject promise
|
||||
|
//如果`validateStatus`返回true(或者设置为`null`或者`undefined`),那么promise的状态将会是resolved,否则其状态就是rejected
|
||||
|
validateStatus: function (status: number) { |
||||
|
return status >= 200 && status < 300; // 默认的
|
||||
|
}, |
||||
|
|
||||
|
//`maxRedirects`定义了在nodejs中重定向的最大数量
|
||||
|
maxRedirects: 5, |
||||
|
} as any; |
@ -0,0 +1,15 @@ |
|||||
|
import { PerformSignin, PerformSNonce } from "~/types" |
||||
|
import request from './service' |
||||
|
|
||||
|
/** |
||||
|
* @description 获取随机数 |
||||
|
*/ |
||||
|
export const getNonce = (query: PerformSNonce) => request({ |
||||
|
url: '/v1/nonce', |
||||
|
data: query |
||||
|
}) |
||||
|
|
||||
|
/** |
||||
|
* @description 签名 |
||||
|
*/ |
||||
|
export const performSignin = (query: PerformSignin) => request({ url: '/v1/signin', data: query }) |
@ -0,0 +1,65 @@ |
|||||
|
import axiosConfig from "./axios_config"; |
||||
|
import axios from 'axios'; |
||||
|
import signGenerator from "../utils/sign/sign"; |
||||
|
import { Toast } from "react-vant"; |
||||
|
import sortParam from "../utils/sign/sort"; |
||||
|
import store from '../store'; |
||||
|
|
||||
|
const service = axios.create(axiosConfig); |
||||
|
|
||||
|
// 请求拦截
|
||||
|
service.interceptors.request.use( |
||||
|
config => { |
||||
|
(config.headers as any).token = store.state.token; |
||||
|
if (!config.data) config.data = {}; |
||||
|
let ps = config.params ? sortParam(config.params) : ""; |
||||
|
let timestamp = new Date().getTime(); |
||||
|
let signData = { |
||||
|
uri: '/api' + config.url, |
||||
|
timestamp: timestamp, |
||||
|
args: ps, |
||||
|
}; |
||||
|
let sign = signGenerator(signData); |
||||
|
(config.headers as any).sign = sign; |
||||
|
(config.headers as any).timestamp = timestamp; |
||||
|
config.data = JSON.stringify(config.data); |
||||
|
return config; |
||||
|
}, |
||||
|
error => { |
||||
|
return Promise.reject(error); |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
// 响应拦截
|
||||
|
service.interceptors.response.use( |
||||
|
(res: any) => { |
||||
|
try { |
||||
|
let data = JSON.parse(res.data); |
||||
|
if (data.code === 101) { //Token 过期
|
||||
|
store.removeAddr() |
||||
|
store.removeToken() |
||||
|
}; |
||||
|
if (data.code !== 0) { |
||||
|
Toast.info({ |
||||
|
message: data.msg, |
||||
|
duration: 2000 |
||||
|
}); |
||||
|
}; |
||||
|
return data |
||||
|
} catch (error) { |
||||
|
return null; |
||||
|
} |
||||
|
}, |
||||
|
error => { |
||||
|
try { |
||||
|
if(error.response){ |
||||
|
let data = JSON.parse(error.response.data); |
||||
|
Toast.info(data.err); |
||||
|
return data |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error(error); |
||||
|
} |
||||
|
} |
||||
|
) |
||||
|
export default service; |
2
src/assets/iconfont/iconfont.js
File diff suppressed because it is too large
View File
Before Width: 79 | Height: 71 | Size: 1.5 KiB After Width: 72 | Height: 72 | Size: 1.9 KiB |
Before Width: 79 | Height: 71 | Size: 1.8 KiB After Width: 72 | Height: 72 | Size: 3.3 KiB |
@ -0,0 +1,3 @@ |
|||||
|
interface Window { |
||||
|
ethereum?: any; |
||||
|
} |
@ -0,0 +1,87 @@ |
|||||
|
import { Toast } from "react-vant"; |
||||
|
import { getNonce, performSignin } from "~/api"; |
||||
|
import $store from "../store"; |
||||
|
import { toNumber } from "ethers"; |
||||
|
|
||||
|
export default function useConnectWallet() { |
||||
|
const connect = async () => { |
||||
|
if (!(window as any).ethereum) { |
||||
|
return; |
||||
|
} |
||||
|
let res = await (window as any).ethereum.request({ |
||||
|
method: "eth_requestAccounts", |
||||
|
}); |
||||
|
if (res.length <= 0) { |
||||
|
removeTokenAndAddress(); |
||||
|
} else { |
||||
|
return res; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const removeTokenAndAddress = () => { |
||||
|
$store.removeAddr(); |
||||
|
$store.removeToken(); |
||||
|
}; |
||||
|
|
||||
|
const getWallet = async () => { |
||||
|
if (!(window as any).ethereum) { |
||||
|
return; |
||||
|
} |
||||
|
let res = await (window as any).ethereum.request({ |
||||
|
method: "eth_accounts", |
||||
|
}); |
||||
|
if (res.length <= 0) { |
||||
|
removeTokenAndAddress(); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const sign = async (nonce: string, address: string) => { |
||||
|
let random = ""; |
||||
|
if ( |
||||
|
(window as any).ethereum.isTokenPocket || |
||||
|
(window as any).ethereum.isTrust |
||||
|
) { |
||||
|
random = nonce; |
||||
|
} else { |
||||
|
const Buffer = require("buffer").Buffer; |
||||
|
const buff = Buffer.from(nonce, "utf-8"); |
||||
|
random = "0x" + buff.toString("hex"); |
||||
|
} |
||||
|
|
||||
|
const signature = await (window as any).ethereum.request({ |
||||
|
method: "personal_sign", |
||||
|
params: [random, address], |
||||
|
}); |
||||
|
return signature; |
||||
|
}; |
||||
|
|
||||
|
const login = async () => { |
||||
|
try { |
||||
|
let [address] = await connect(); |
||||
|
const chain_id = await window.ethereum.request({ |
||||
|
method: "eth_chainId", |
||||
|
params: [], |
||||
|
}); |
||||
|
let nonce: any = await getNonce({ |
||||
|
address: address, |
||||
|
chainId: toNumber(chain_id), |
||||
|
}); |
||||
|
if (nonce.code !== 0) return; |
||||
|
let random = nonce.data.nonce; |
||||
|
let signature = await sign(random, address); |
||||
|
let res = await performSignin({ |
||||
|
address, |
||||
|
nonce: random, |
||||
|
signature, |
||||
|
// chain_id
|
||||
|
}); |
||||
|
Toast.success("登錄成功"); |
||||
|
$store.setAddress(address); |
||||
|
$store.setToken(res.data.token); |
||||
|
} catch (error) { |
||||
|
console.log(error); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
return { login, getWallet }; |
||||
|
} |
@ -1,43 +0,0 @@ |
|||||
import { Divider } from 'react-vant' |
|
||||
import '~/styles/cart.scss' |
|
||||
|
|
||||
const Cart = () => { |
|
||||
return ( |
|
||||
<div className='plr-2 cart'> |
|
||||
<div className='fz-wb-550 mtb-2'>购物车</div> |
|
||||
<div> |
|
||||
{ |
|
||||
Array.from({ length: 5 }).map((_, index) => ( |
|
||||
<div className='row-items mt-3' key={index}> |
|
||||
<img src={require('~/assets/cover.png')} className="cover" alt="" /> |
|
||||
<div className='box p-2'> |
|
||||
<div className='fz-14'>The Unkown</div> |
|
||||
<div className='row-items mt-5px'> |
|
||||
<div className='price-tag'>ETH 2.25</div> |
|
||||
<div className='user-tag ml-1'> |
|
||||
<img src={require('~/assets/user.png')} className="img" alt="" /> |
|
||||
<div className='ml-3px'>iamjackrider</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
<div className='fz-12 sub-text mt-1'>Top Bid is By You</div> |
|
||||
<div className='fz-12 sub-text mt-1'>Time Remaining</div> |
|
||||
<div className='row-items mt-5px'> |
|
||||
<div className='timing-box fz-12'> |
|
||||
<i className='iconfont icon-clock fz-12 fz-wb-550' style={{ color: '#F96900' }} /> |
|
||||
<Divider type='vertical' style={{ borderColor: '#C4C4C4' }} className="ml-5px" /> |
|
||||
<div className='ml-5px fz-wb-550' style={{ color: '#F96900' }}>00:02:30</div> |
|
||||
</div> |
|
||||
<div className='ml-4 row-center delete'> |
|
||||
<i className='iconfont icon-delete'></i> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
)) |
|
||||
} |
|
||||
</div> |
|
||||
</div> |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
export default Cart |
|
@ -1,59 +1,44 @@ |
|||||
import { Tabs } from 'react-vant' |
|
||||
import '~/styles/personal.scss' |
import '~/styles/personal.scss' |
||||
import ProductItem from '~/components/ProductItem' |
|
||||
|
|
||||
const Person = () => { |
|
||||
|
const Personal = () => { |
||||
|
|
||||
return ( |
return ( |
||||
<div className="personal"> |
|
||||
<div className='box'> |
|
||||
<img src={require('~/assets/personal.png')} alt="" className='bg-cover'></img> |
|
||||
<div className='row-center avatar'> |
|
||||
<img src={require('~/assets/avatar.png')} className="img" alt="" /> |
|
||||
</div> |
|
||||
</div> |
|
||||
<div className='box-block'></div> |
|
||||
<div className='tac fz-wb-550 mt-1'>IamjackRider</div> |
|
||||
<div className='row-center'> |
|
||||
<div className='tag fz-wb-550 mt-5px'>开放我的资产</div> |
|
||||
</div> |
|
||||
<div className='row-between plr-5 mt-3'> |
|
||||
<div className='tac'> |
|
||||
<div className='fz-20 fz-wb-550'>120K</div> |
|
||||
<div className='mt-8px'>ArtWorks</div> |
|
||||
</div> |
|
||||
<div className='tac'> |
|
||||
<div className='fz-20 fz-wb-550'>120K</div> |
|
||||
<div className='mt-8px'>Auctions</div> |
|
||||
</div> |
|
||||
<div className='tac'> |
|
||||
<div className='fz-20 fz-wb-550'>255 ETH</div> |
|
||||
<div className='mt-8px'>Earning</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
<div className='mt-3'> |
|
||||
<Tabs |
|
||||
lineWidth={100} |
|
||||
background="none" |
|
||||
titleInactiveColor='#000' |
|
||||
titleActiveColor='#11C0CB' |
|
||||
color='#11C0CB' |
|
||||
animated |
|
||||
swipeable |
|
||||
> |
|
||||
<Tabs.TabPane title="我的NFT" titleClass='fz-wb-550'> |
|
||||
<div className='row-between flex-wrap plr-3'> |
|
||||
{Array.from({ length: 10 }).map((_, index) => <ProductItem key={index} />)} |
|
||||
</div> |
|
||||
</Tabs.TabPane> |
|
||||
<Tabs.TabPane title="我喜欢的NFT" titleClass='fz-wb-550'> |
|
||||
<div className='row-between flex-wrap plr-3'> |
|
||||
{Array.from({ length: 10 }).map((_, index) => <ProductItem key={index} />)} |
|
||||
|
<div className='plr-2 cart'> |
||||
|
|
||||
|
{/* <div className='fz-wb-550 mtb-2'>购物车</div> |
||||
|
<div> |
||||
|
{ |
||||
|
Array.from({ length: 5 }).map((_, index) => ( |
||||
|
<div className='row-items mt-3' key={index}> |
||||
|
<img src={require('~/assets/cover.png')} className="cover" alt="" /> |
||||
|
<div className='box p-2'> |
||||
|
<div className='fz-14'>The Unkown</div> |
||||
|
<div className='row-items mt-5px'> |
||||
|
<div className='price-tag'>ETH 2.25</div> |
||||
|
<div className='user-tag ml-1'> |
||||
|
<img src={require('~/assets/user.png')} className="img" alt="" /> |
||||
|
<div className='ml-3px'>iamjackrider</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div className='fz-12 sub-text mt-1'>Top Bid is By You</div> |
||||
|
<div className='fz-12 sub-text mt-1'>Time Remaining</div> |
||||
|
<div className='row-items mt-5px'> |
||||
|
<div className='timing-box fz-12'> |
||||
|
<i className='iconfont icon-clock fz-12 fz-wb-550' style={{ color: '#F96900' }} /> |
||||
|
<Divider type='vertical' style={{ borderColor: '#C4C4C4' }} className="ml-5px" /> |
||||
|
<div className='ml-5px fz-wb-550' style={{ color: '#F96900' }}>00:02:30</div> |
||||
|
</div> |
||||
|
<div className='ml-4 row-center delete'> |
||||
|
<i className='iconfont icon-delete'></i> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
</div> |
</div> |
||||
</Tabs.TabPane> |
|
||||
</Tabs> |
|
||||
</div> |
|
||||
|
)) |
||||
|
} */} |
||||
|
{/* </div> */} |
||||
</div> |
</div> |
||||
) |
) |
||||
} |
} |
||||
|
|
||||
export default Person |
|
||||
|
export default Personal |
@ -0,0 +1,71 @@ |
|||||
|
import { Tabs } from 'react-vant' |
||||
|
import '~/styles/share.scss' |
||||
|
import ProductItem from '~/components/ProductItem' |
||||
|
|
||||
|
const Share = () => { |
||||
|
return ( |
||||
|
<div className="personal"> |
||||
|
<div className='box'> |
||||
|
<img src={require('~/assets/personal.png')} alt="" className='bg-cover'></img> |
||||
|
<div className='row-center avatar'> |
||||
|
<img src={require('~/assets/avatar.png')} className="img" alt="" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div className='box-block'></div> |
||||
|
<div className='tac fz-wb-550 mt-1'>IamjackRider</div> |
||||
|
<div className='row-center'> |
||||
|
<div className='tag fz-wb-550 mt-5px'>开放我的资产</div> |
||||
|
</div> |
||||
|
<div className='row-between plr-5 mt-3'> |
||||
|
<div className='tac'> |
||||
|
<div className='fz-20 fz-wb-550'>5</div> |
||||
|
<div className='mt-8px'>售卖作品</div> |
||||
|
</div> |
||||
|
<div className='tac'> |
||||
|
<div className='fz-20 fz-wb-550'>15</div> |
||||
|
<div className='mt-8px'>拍卖作品</div> |
||||
|
</div> |
||||
|
<div className='tac'> |
||||
|
<div className='fz-20 fz-wb-550'>255 FIL</div> |
||||
|
<div className='mt-8px'>收入</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div className='mt-3'> |
||||
|
<Tabs |
||||
|
lineWidth={100} |
||||
|
background="none" |
||||
|
titleInactiveColor='#000' |
||||
|
titleActiveColor='#000' |
||||
|
color='#11C0CB' |
||||
|
animated |
||||
|
swipeable |
||||
|
> |
||||
|
<Tabs.TabPane |
||||
|
title={<div> |
||||
|
<span>我的NFT</span> |
||||
|
<span className='ml-5px' style={{ color: '#11C0CB' }}>25</span> |
||||
|
</div>} |
||||
|
titleClass='fz-wb-550' |
||||
|
> |
||||
|
<div className='row-between flex-wrap plr-3'> |
||||
|
{Array.from({ length: 10 }).map((_, index) => <ProductItem key={index} />)} |
||||
|
</div> |
||||
|
</Tabs.TabPane> |
||||
|
<Tabs.TabPane |
||||
|
title={<div> |
||||
|
<span>我喜欢的NFT</span> |
||||
|
<span className='ml-5px' style={{ color: '#11C0CB' }}>999+ </span> |
||||
|
</div>} |
||||
|
titleClass='fz-wb-550' |
||||
|
> |
||||
|
<div className='row-between flex-wrap plr-3'> |
||||
|
{Array.from({ length: 10 }).map((_, index) => <ProductItem key={index} />)} |
||||
|
</div> |
||||
|
</Tabs.TabPane> |
||||
|
</Tabs> |
||||
|
</div> |
||||
|
</div> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default Share |
@ -1,56 +0,0 @@ |
|||||
import { useState } from 'react'; |
|
||||
import '~/styles/layout.scss' |
|
||||
import { useRouter } from '~/hooks/useRouter'; |
|
||||
import Notify from './Notify'; |
|
||||
import Router from './router'; |
|
||||
import { tabbarData } from './routes'; |
|
||||
|
|
||||
const LayoutRouter = () => { |
|
||||
|
|
||||
const { location, push } = useRouter() |
|
||||
const [visible, setVisible] = useState(false) |
|
||||
|
|
||||
return ( |
|
||||
<div className='layout'> |
|
||||
<div className={`header plr-3 ${location.pathname === '/personal' && 'header-bg-color'}`}> |
|
||||
<div className='fz-wb-550'>9527</div> |
|
||||
<div className='row' onClick={() => setVisible(true)}> |
|
||||
<div className='notify-circle'></div> |
|
||||
<i className='iconfont icon-messages fz-24 fz-wb-1000' /> |
|
||||
</div> |
|
||||
</div> |
|
||||
<div className='pages'> |
|
||||
<Router /> |
|
||||
{ |
|
||||
tabbarData.includes(location.pathname) && <div className='tabbar-block'></div> |
|
||||
} |
|
||||
</div> |
|
||||
{ |
|
||||
tabbarData.includes(location.pathname) && ( |
|
||||
<div className='tabbar'> |
|
||||
{ |
|
||||
tabbarData.map((item, index) => ( |
|
||||
<div key={index} onClick={() => push(item)}> |
|
||||
<img |
|
||||
src={require(`~/assets/tabbar/tabbar-${index + 1}${item === location.pathname ? '-o' : ''}.png`)} |
|
||||
alt="" |
|
||||
className='img' |
|
||||
/> |
|
||||
{ |
|
||||
item === location.pathname && |
|
||||
<div className='row-center'> |
|
||||
<div className='circle'></div> |
|
||||
</div> |
|
||||
} |
|
||||
</div> |
|
||||
)) |
|
||||
} |
|
||||
</div> |
|
||||
) |
|
||||
} |
|
||||
<Notify visible={visible} setVisible={setVisible} /> |
|
||||
</div> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
export default LayoutRouter |
|
@ -0,0 +1,78 @@ |
|||||
|
import '~/styles/layout.scss' |
||||
|
import { useEffect, useRef, useState } from "react" |
||||
|
import { Button, Popover, PopoverInstance, Toast } from "react-vant" |
||||
|
import { observer } from 'mobx-react' |
||||
|
import useConnectWallet from '~/hooks/useConnectWallet' |
||||
|
import store from '~/store' |
||||
|
import { splitAddress } from '~/utils' |
||||
|
|
||||
|
const ConnectButton = () => { |
||||
|
|
||||
|
const { token, walletAddress } = store.state |
||||
|
const [loading, setLoading] = useState(false) |
||||
|
const { login } = useConnectWallet(); |
||||
|
const popover = useRef<PopoverInstance>(null); |
||||
|
|
||||
|
|
||||
|
const connectWallet = async () => { |
||||
|
setLoading(true); |
||||
|
await login(); |
||||
|
setLoading(false); |
||||
|
} |
||||
|
|
||||
|
const logout = () => { |
||||
|
popover.current && popover.current.hide(); |
||||
|
store.removeAddr(); |
||||
|
store.removeToken(); |
||||
|
Toast.success('退出成功'); |
||||
|
} |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (!(window as any).ethereum) { |
||||
|
return; |
||||
|
}; |
||||
|
(window as any).ethereum.on('accountsChanged', () => { |
||||
|
store.removeAddr(); |
||||
|
store.removeToken(); |
||||
|
}); |
||||
|
}, []); |
||||
|
|
||||
|
return ( |
||||
|
<div className="connect-button"> |
||||
|
{ |
||||
|
token ? ( |
||||
|
<Popover |
||||
|
ref={popover} |
||||
|
className='popover' |
||||
|
reference={<Button |
||||
|
className="button" |
||||
|
> |
||||
|
{splitAddress(walletAddress)} |
||||
|
</Button> |
||||
|
} |
||||
|
> |
||||
|
<Button className="button" onClick={logout}> |
||||
|
<div className='row-items'> |
||||
|
<div className='iconfont icon-tuichu'></div> |
||||
|
<div className='ml-5px mt-3px'>退出登錄</div> |
||||
|
</div> |
||||
|
</Button> |
||||
|
</Popover> |
||||
|
) : ( |
||||
|
<Button |
||||
|
className="button" |
||||
|
loading={loading} |
||||
|
loadingType='ball' |
||||
|
onClick={connectWallet} |
||||
|
> |
||||
|
连接钱包 |
||||
|
</Button> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
</div > |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default observer(ConnectButton) |
@ -0,0 +1,29 @@ |
|||||
|
|
||||
|
import '~/styles/layout.scss' |
||||
|
import ConnectButton from './ConnectButton' |
||||
|
|
||||
|
interface NavbarProps { |
||||
|
pathname: string, |
||||
|
setVisible: Function |
||||
|
} |
||||
|
|
||||
|
const Navbar = (props: NavbarProps) => { |
||||
|
|
||||
|
const { pathname, setVisible } = props |
||||
|
|
||||
|
return ( |
||||
|
<div className={`header plr-3 ${pathname === '/share' && 'header-bg-color'}`}> |
||||
|
<div className='fz-wb-550' style={{ flex: 1 }}>9527</div> |
||||
|
{/* <div style={{ flex: 1 }} className='tac'>首页</div> */} |
||||
|
<div className='row-justify-end' style={{ flex: 1 }}> |
||||
|
<ConnectButton /> |
||||
|
<div className='row ml-2' onClick={() => setVisible(true)}> |
||||
|
<div className='notify-circle'></div> |
||||
|
<i className='iconfont icon-messages fz-24 fz-wb-1000' /> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default Navbar |
@ -0,0 +1,36 @@ |
|||||
|
import '~/styles/layout.scss' |
||||
|
|
||||
|
interface TabbarProps { |
||||
|
tabbarData: string[], |
||||
|
push: Function, |
||||
|
pathname: string |
||||
|
} |
||||
|
|
||||
|
const Tabbar = (props: TabbarProps) => { |
||||
|
|
||||
|
const { tabbarData, push, pathname } = props |
||||
|
|
||||
|
return ( |
||||
|
<div className='tabbar'> |
||||
|
{ |
||||
|
tabbarData.map((item, index) => ( |
||||
|
<div key={index} onClick={() => push(item)}> |
||||
|
<img |
||||
|
src={require(`~/assets/tabbar/tabbar-${index + 1}${item === pathname ? '-o' : ''}.png`)} |
||||
|
alt="" |
||||
|
className='img' |
||||
|
/> |
||||
|
{ |
||||
|
item === pathname && |
||||
|
<div className='row-center'> |
||||
|
<div className='circle'></div> |
||||
|
</div> |
||||
|
} |
||||
|
</div> |
||||
|
)) |
||||
|
} |
||||
|
</div> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default Tabbar |
@ -0,0 +1,47 @@ |
|||||
|
import '~/styles/layout.scss' |
||||
|
import { LegacyRef, useEffect, useRef, useState } from 'react'; |
||||
|
import { useRouter } from '~/hooks/useRouter'; |
||||
|
import Notify from './Notify'; |
||||
|
import RenderRouter from './RenderRouter'; |
||||
|
import { tabbarData } from '../routes'; |
||||
|
import Navbar from './Navbar'; |
||||
|
import Tabbar from './Tabbar'; |
||||
|
|
||||
|
const LayoutRouter = () => { |
||||
|
|
||||
|
const { location, push } = useRouter() |
||||
|
const [visible, setVisible] = useState(false) |
||||
|
const pagesRef = useRef<HTMLDivElement>(null); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
// 在路由变化时将自定义滚动条滚动到顶部
|
||||
|
|
||||
|
}, [location.pathname]) |
||||
|
|
||||
|
return ( |
||||
|
<div className='layout'> |
||||
|
<Navbar |
||||
|
pathname={location.pathname} |
||||
|
setVisible={setVisible} |
||||
|
/> |
||||
|
<div className='pages' ref={pagesRef}> |
||||
|
<RenderRouter /> |
||||
|
{ |
||||
|
tabbarData.includes(location.pathname) && <div className='tabbar-block'></div> |
||||
|
} |
||||
|
</div> |
||||
|
{ |
||||
|
tabbarData.includes(location.pathname) && ( |
||||
|
<Tabbar |
||||
|
tabbarData={tabbarData} |
||||
|
push={push} |
||||
|
pathname={location.pathname} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
<Notify visible={visible} setVisible={setVisible} /> |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
export default LayoutRouter |
@ -0,0 +1,64 @@ |
|||||
|
import { makeAutoObservable } from "mobx"; |
||||
|
import { StoreLocalStorageKey } from "~/types"; |
||||
|
|
||||
|
interface Store { |
||||
|
state: object; |
||||
|
} |
||||
|
|
||||
|
class AppStore implements Store { |
||||
|
state = { |
||||
|
token: "", |
||||
|
walletAddress: "", |
||||
|
}; |
||||
|
|
||||
|
constructor() { |
||||
|
makeAutoObservable(this); |
||||
|
this.initState() |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @description 初始化数据 |
||||
|
*/ |
||||
|
initState() { |
||||
|
let addr = window.localStorage.getItem(StoreLocalStorageKey.ADDRESS) || ""; |
||||
|
let token = window.localStorage.getItem(StoreLocalStorageKey.TOKEN) || ""; |
||||
|
this.state.walletAddress = addr; |
||||
|
this.state.token = token; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @description 设置token |
||||
|
*/ |
||||
|
setToken(token: string): void { |
||||
|
this.state.token = token; |
||||
|
window.localStorage.setItem(StoreLocalStorageKey.TOKEN, token); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @description 移除token |
||||
|
*/ |
||||
|
removeToken(): void { |
||||
|
this.state.token = ""; |
||||
|
window.localStorage.removeItem(StoreLocalStorageKey.TOKEN); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @description 设置地址 |
||||
|
*/ |
||||
|
setAddress(addr: string): void { |
||||
|
this.state.walletAddress = addr; |
||||
|
window.localStorage.setItem(StoreLocalStorageKey.ADDRESS, addr); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @description 移除地址 |
||||
|
*/ |
||||
|
removeAddr(): void { |
||||
|
this.state.walletAddress = ""; |
||||
|
window.localStorage.removeItem(StoreLocalStorageKey.ADDRESS); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const store = new AppStore(); |
||||
|
|
||||
|
export default store; |
@ -1,60 +0,0 @@ |
|||||
.cart{ |
|
||||
|
|
||||
.cover{ |
|
||||
@include img-size(170px,170px) |
|
||||
} |
|
||||
|
|
||||
.box{ |
|
||||
height: 151px; |
|
||||
width: 100%; |
|
||||
background-color: $white; |
|
||||
box-shadow: 8px 8px 20px 0px rgba(0, 0, 0, 0.1); |
|
||||
border-top-right-radius: 10px; |
|
||||
border-bottom-right-radius: 10px; |
|
||||
|
|
||||
.price-tag{ |
|
||||
padding: 0px 8px; |
|
||||
height: 18px; |
|
||||
display: flex; |
|
||||
align-items: center; |
|
||||
background: linear-gradient(114deg, #320D6D 0%, #8A4CED 108%); |
|
||||
color: $white; |
|
||||
border-radius: 50px; |
|
||||
font-size: 12px; |
|
||||
} |
|
||||
|
|
||||
.user-tag{ |
|
||||
padding: 0px 8px 0px 0px; |
|
||||
height: 18px; |
|
||||
display: flex; |
|
||||
align-items: center; |
|
||||
background-color: #F1F1F1; |
|
||||
border-radius: 50px; |
|
||||
font-size: 12px; |
|
||||
.img{ |
|
||||
@include img-size(15px,15px); |
|
||||
border-radius:8px |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.timing-box{ |
|
||||
width: 85px; |
|
||||
height: 20px; |
|
||||
border-radius: 20px; |
|
||||
background-color: #f1f1f1; |
|
||||
display: flex; |
|
||||
justify-content: center; |
|
||||
align-items: center; |
|
||||
} |
|
||||
|
|
||||
.delete{ |
|
||||
width: 22px; |
|
||||
height: 22px; |
|
||||
border-radius: 12px; |
|
||||
background-color: #f1f1f1; |
|
||||
color:#F96900; |
|
||||
} |
|
||||
|
|
||||
} |
|
||||
|
|
||||
} |
|
@ -1,44 +1,60 @@ |
|||||
.personal{ |
|
||||
|
.cart{ |
||||
|
|
||||
.box{ |
|
||||
|
.cover{ |
||||
|
@include img-size(170px,170px) |
||||
|
} |
||||
|
|
||||
|
.box{ |
||||
|
height: 151px; |
||||
width: 100%; |
width: 100%; |
||||
height: 200px; |
|
||||
position: relative; |
|
||||
.bg-cover{ |
|
||||
width: 100%; |
|
||||
height: 200px; |
|
||||
top: 0; |
|
||||
left: 0; |
|
||||
object-fit:cover; |
|
||||
position: absolute; |
|
||||
border-bottom-left-radius: 50px; |
|
||||
border-bottom-right-radius: 50px; |
|
||||
|
background-color: $white; |
||||
|
box-shadow: 8px 8px 20px 0px rgba(0, 0, 0, 0.1); |
||||
|
border-top-right-radius: 10px; |
||||
|
border-bottom-right-radius: 10px; |
||||
|
|
||||
|
.price-tag{ |
||||
|
padding: 0px 8px; |
||||
|
height: 18px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
background: linear-gradient(114deg, #320D6D 0%, #8A4CED 108%); |
||||
|
color: $white; |
||||
|
border-radius: 50px; |
||||
|
font-size: 12px; |
||||
} |
} |
||||
|
|
||||
.avatar{ |
|
||||
position: absolute; |
|
||||
left: calc(50% - 71px); |
|
||||
bottom: -71px; |
|
||||
|
|
||||
|
.user-tag{ |
||||
|
padding: 0px 8px 0px 0px; |
||||
|
height: 18px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
background-color: #F1F1F1; |
||||
|
border-radius: 50px; |
||||
|
font-size: 12px; |
||||
.img{ |
.img{ |
||||
@include img-size(142px,142px); |
|
||||
border-radius: 71px; |
|
||||
|
@include img-size(15px,15px); |
||||
|
border-radius:8px |
||||
} |
} |
||||
} |
} |
||||
} |
|
||||
|
|
||||
.box-block{ |
|
||||
display: block; |
|
||||
height: 71px; |
|
||||
width: 100%; |
|
||||
} |
|
||||
|
.timing-box{ |
||||
|
width: 85px; |
||||
|
height: 20px; |
||||
|
border-radius: 20px; |
||||
|
background-color: #f1f1f1; |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.delete{ |
||||
|
width: 22px; |
||||
|
height: 22px; |
||||
|
border-radius: 12px; |
||||
|
background-color: #f1f1f1; |
||||
|
color:#F96900; |
||||
|
} |
||||
|
|
||||
.tag{ |
|
||||
background: linear-gradient(104deg, #1DD0DF -2%, #1DD0DF -2%, #1BEDFF -2%, #14BDEB 108%); |
|
||||
padding: 5px 15px; |
|
||||
font-size: 12px; |
|
||||
border-radius: 20px; |
|
||||
} |
} |
||||
|
|
||||
} |
|
||||
|
} |
@ -0,0 +1,44 @@ |
|||||
|
.personal{ |
||||
|
|
||||
|
.box{ |
||||
|
|
||||
|
width: 100%; |
||||
|
height: 200px; |
||||
|
position: relative; |
||||
|
.bg-cover{ |
||||
|
width: 100%; |
||||
|
height: 200px; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
object-fit:cover; |
||||
|
position: absolute; |
||||
|
border-bottom-left-radius: 50px; |
||||
|
border-bottom-right-radius: 50px; |
||||
|
} |
||||
|
|
||||
|
.avatar{ |
||||
|
position: absolute; |
||||
|
left: calc(50% - 71px); |
||||
|
bottom: -71px; |
||||
|
|
||||
|
.img{ |
||||
|
@include img-size(142px,142px); |
||||
|
border-radius: 71px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.box-block{ |
||||
|
display: block; |
||||
|
height: 71px; |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
.tag{ |
||||
|
background: linear-gradient(104deg, #1DD0DF -2%, #1DD0DF -2%, #1BEDFF -2%, #14BDEB 108%); |
||||
|
padding: 5px 15px; |
||||
|
font-size: 12px; |
||||
|
border-radius: 20px; |
||||
|
} |
||||
|
|
||||
|
} |
@ -0,0 +1,12 @@ |
|||||
|
interface PerformSNonce { |
||||
|
address: string; |
||||
|
chainId: number; |
||||
|
} |
||||
|
|
||||
|
interface PerformSignin { |
||||
|
address: string; |
||||
|
nonce: string; |
||||
|
signature: string; |
||||
|
} |
||||
|
|
||||
|
export { PerformSNonce, PerformSignin } |
@ -0,0 +1,2 @@ |
|||||
|
export { StoreLocalStorageKey } from "./store.d"; |
||||
|
export type { PerformSignin, PerformSNonce } from "./api"; |
@ -0,0 +1,6 @@ |
|||||
|
enum StoreLocalStorageKey { |
||||
|
TOKEN = "MARKET_NFT_TOKEN", |
||||
|
ADDRESS = "MARKET_NFT_ADDRESS", |
||||
|
} |
||||
|
|
||||
|
export { StoreLocalStorageKey }; |
@ -0,0 +1,14 @@ |
|||||
|
const splitAddress = (address: string, index?: number) => { |
||||
|
try { |
||||
|
let idx = index ? index : 5; |
||||
|
return ( |
||||
|
address.substring(0, idx) + |
||||
|
"..." + |
||||
|
address.substring(address.length - idx, address.length) |
||||
|
); |
||||
|
} catch (error) { |
||||
|
return ""; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export { splitAddress }; |
@ -0,0 +1,21 @@ |
|||||
|
const md5 = require('js-md5') |
||||
|
var signkey = process.env.REACT_APP_SIGN_KEY |
||||
|
|
||||
|
var signGenerator = (data: any) => { |
||||
|
|
||||
|
var keys = []; |
||||
|
for (var key in data) { |
||||
|
keys.push(key); |
||||
|
} |
||||
|
keys.sort(); |
||||
|
|
||||
|
var ptxt = ""; |
||||
|
for (var i = 0; i < keys.length; i++) { |
||||
|
ptxt += keys[i] + data[keys[i]]; |
||||
|
} |
||||
|
ptxt = signkey + ptxt + signkey; |
||||
|
var signval = md5(ptxt).toLowerCase() |
||||
|
return signval; |
||||
|
} |
||||
|
|
||||
|
export default signGenerator; |
@ -0,0 +1,35 @@ |
|||||
|
/* eslint-disable no-loop-func */ |
||||
|
var sortParam = (data: any) => { |
||||
|
var keys: any = []; |
||||
|
for (var key in data) { |
||||
|
keys.push(key); |
||||
|
} |
||||
|
|
||||
|
var ptxt = ""; |
||||
|
for (var i = 0; i < keys.length; i++) { |
||||
|
if (data[keys[i]] instanceof Array) { |
||||
|
if (i === 0) { |
||||
|
data[keys[i]].forEach((v: string, index: number) => { |
||||
|
if (index === 1) { |
||||
|
ptxt += keys[i] + "=" + v; |
||||
|
} else { |
||||
|
ptxt += "&" + keys[i] + "=" + v; |
||||
|
} |
||||
|
}); |
||||
|
} else { |
||||
|
data[keys[i]].forEach((v: any) => { |
||||
|
ptxt += "&" + keys[i] + "=" + v; |
||||
|
}); |
||||
|
} |
||||
|
} else { |
||||
|
if (i === 0) { |
||||
|
ptxt += keys[i] + "=" + data[keys[i]]; |
||||
|
} else { |
||||
|
ptxt += "&" + keys[i] + "=" + data[keys[i]]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
// console.log(ptxt);
|
||||
|
return ptxt; |
||||
|
}; |
||||
|
export default sortParam; |