-
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 ProductItem from '~/components/ProductItem' |
|||
|
|||
const Person = () => { |
|||
const Personal = () => { |
|||
|
|||
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> |
|||
</Tabs.TabPane> |
|||
</Tabs> |
|||
</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%; |
|||
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{ |
|||
@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; |