提取
This commit is contained in:
166
src/pages/draw/index.tsx
Normal file
166
src/pages/draw/index.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* Title: 获取矿晶
|
||||
* Routes:
|
||||
* - ./src/pages/routes
|
||||
*/
|
||||
import React, { useEffect } from 'react';
|
||||
import { connect, DispatchProp } from 'dva';
|
||||
import { Spin, Input, Select, Icon, Button, message, Modal } from 'antd';
|
||||
import { formatMessage } from 'umi-plugin-locale';
|
||||
import { DrawModelState } from './model';
|
||||
import { PageState } from '@/types/common';
|
||||
import Header from '@/components/Header';
|
||||
|
||||
import styles from './style.less';
|
||||
|
||||
const Option = Select.Option;
|
||||
|
||||
const DrawPage: React.FC<DispatchProp & DrawModelState> = ({
|
||||
dispatch,
|
||||
pageState,
|
||||
coin,
|
||||
coins,
|
||||
balance,
|
||||
address,
|
||||
amount,
|
||||
fee,
|
||||
actual,
|
||||
confirmVisible,
|
||||
confirmLoading,
|
||||
password,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
dispatch({ type: 'draw/load' });
|
||||
}, []);
|
||||
|
||||
let body;
|
||||
if (pageState === PageState.Pending) {
|
||||
body = (
|
||||
<Spin className={styles.none} />
|
||||
);
|
||||
} else if (pageState === PageState.Failure) {
|
||||
body = (
|
||||
<div className={styles.none}>{formatMessage({ id: 'common.loadError' })}</div>
|
||||
);
|
||||
} else if (pageState === PageState.Success) {
|
||||
const coinLabel = coin && coin.label.toUpperCase();
|
||||
const onScan = () => {
|
||||
message.warn(formatMessage({ id: 'draw.qrPrompt' }));
|
||||
}
|
||||
const confirmContent = (
|
||||
<Modal
|
||||
visible={confirmVisible}
|
||||
onCancel={() => dispatch({ type: 'draw/onUpdateState', confirmVisible: false })}
|
||||
onOk={() => dispatch({ type: 'draw/draw' })}
|
||||
okText={formatMessage({ id: 'common.ok' })}
|
||||
cancelText={formatMessage({ id: 'common.cancel' })}
|
||||
confirmLoading={confirmLoading}
|
||||
title={formatMessage({ id: 'common.prompt' })}
|
||||
maskClosable={false}
|
||||
>
|
||||
<span style={{ wordBreak: 'break-all' }}>
|
||||
{formatMessage(
|
||||
{ id: 'draw.summary' },
|
||||
{ num: amount, coin: coinLabel, address },
|
||||
)}
|
||||
</span>
|
||||
<div style={{ marginTop: 24 }}>
|
||||
<span>{formatMessage({ id: 'draw.pwdPrompt' })}</span>
|
||||
<Input
|
||||
style={{ marginTop: 8 }}
|
||||
value={password}
|
||||
onChange={e => dispatch({ type: 'draw/onUpdateState', password: e.target.value })}
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
body = (
|
||||
<>
|
||||
<div className={styles.section}>
|
||||
<div className="coin-selection">
|
||||
<span>{formatMessage({ id: 'draw.selection' })}</span>
|
||||
<Select
|
||||
size="small"
|
||||
value={coin && coin.id}
|
||||
onChange={(payload: any) => dispatch({ type: 'draw/selectCoin', payload })}
|
||||
>
|
||||
{coins.map(it => (
|
||||
<Option
|
||||
key={it.id}
|
||||
value={it.id}
|
||||
>{it.label.toUpperCase()}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`${styles.section}`}
|
||||
>
|
||||
<div>{formatMessage({ id: 'draw.target' }, { label: coinLabel })}</div>
|
||||
<Input
|
||||
value={address}
|
||||
className="app-input"
|
||||
placeholder={formatMessage({ id: 'draw.placeholder' })}
|
||||
suffix={<Icon onClick={onScan} type="scan" />}
|
||||
onChange={e => dispatch({ type: 'draw/changeAddress', payload: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.section}>
|
||||
<div className="with-postfix">
|
||||
<span>{formatMessage({ id: 'draw.amount' })}</span>
|
||||
<span className="small-info">
|
||||
{formatMessage(
|
||||
{ id: 'draw.avaliable' },
|
||||
{ num: balance || 0, coin: coinLabel },
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<Input
|
||||
className="app-input"
|
||||
placeholder={formatMessage({ id: 'draw.drawHint' })}
|
||||
addonAfter={coinLabel}
|
||||
type="number"
|
||||
value={amount}
|
||||
onChange={e => dispatch({ type: 'draw/changeAmount', payload: e.target.value })}
|
||||
/>
|
||||
<div className="small-info right">
|
||||
{formatMessage({ id: 'draw.fee' }, { num: fee || 0, coin: coinLabel })}
|
||||
</div>
|
||||
<div className="small-info right">
|
||||
{formatMessage({ id: 'draw.actual' }, { num: actual || 0, coin: coinLabel })}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
className={styles.btn}
|
||||
onClick={() => dispatch({ type: 'draw/performDraw' })}
|
||||
>
|
||||
{formatMessage({ id: 'draw.draw' })}
|
||||
</Button>
|
||||
|
||||
{confirmContent}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Header
|
||||
className="header"
|
||||
title={formatMessage({ id: 'draw.title' })}
|
||||
/>
|
||||
|
||||
<div className={styles.body}>
|
||||
<div className={styles.card}>{body}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(
|
||||
({ draw }: any) => ({ ...draw }),
|
||||
)(DrawPage);
|
||||
169
src/pages/draw/model.ts
Normal file
169
src/pages/draw/model.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import { Model, routerRedux } from "dva";
|
||||
import _ from 'lodash';
|
||||
import { PageState, CoinDrawn, FundBasic } from '@/types/common';
|
||||
import { getCoins, retrieveBalance, calcFee, withdraw } from './service';
|
||||
import { getFundBasic } from '@/services/common';
|
||||
import { message, Modal } from 'antd';
|
||||
import { formatMessage } from 'umi-plugin-locale';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { dispatch } from '@/utils/utils';
|
||||
|
||||
export interface DrawModelState {
|
||||
coins: CoinDrawn[];
|
||||
pageState: PageState;
|
||||
coin: CoinDrawn | null;
|
||||
|
||||
basic: FundBasic | null,
|
||||
balance: string;
|
||||
address: string;
|
||||
amount: string;
|
||||
fee: string; // 转账费用
|
||||
actual: string; // 实际到账
|
||||
|
||||
confirmVisible: boolean;
|
||||
password: string;
|
||||
confirmLoading: boolean;
|
||||
}
|
||||
|
||||
const initState: DrawModelState = {
|
||||
coins: [],
|
||||
coin: null,
|
||||
pageState: PageState.Pending,
|
||||
|
||||
basic: null,
|
||||
balance: '',
|
||||
address: '',
|
||||
amount: '',
|
||||
fee: '',
|
||||
actual: '',
|
||||
|
||||
confirmVisible: false,
|
||||
password: '',
|
||||
confirmLoading: false,
|
||||
};
|
||||
|
||||
const model: Model = {
|
||||
namespace: 'draw',
|
||||
|
||||
state: initState,
|
||||
|
||||
effects: {
|
||||
*load(_, { put, call, all }) {
|
||||
yield put({ type: 'onUpdateState', pageState: PageState.Pending });
|
||||
try {
|
||||
const [coins, basic]: [CoinDrawn[], FundBasic] = yield all([
|
||||
call(getCoins),
|
||||
call(getFundBasic),
|
||||
]);
|
||||
const coin = coins[0];
|
||||
yield put({
|
||||
type: 'onUpdateState',
|
||||
pageState: PageState.Success,
|
||||
coins,
|
||||
coin,
|
||||
basic,
|
||||
balance: retrieveBalance(basic, coin),
|
||||
});
|
||||
} catch (e) {
|
||||
yield put({
|
||||
type: 'onUpdateState',
|
||||
pageState: PageState.Failure,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
*selectCoin({ payload: id }, { put, select }) {
|
||||
const { coins, basic } = yield select((state: any) => state.draw);
|
||||
const coin = _.find(coins, ['id', id]);
|
||||
yield put({
|
||||
type: 'onUpdateState',
|
||||
coin,
|
||||
balance: retrieveBalance(basic, coin),
|
||||
});
|
||||
},
|
||||
|
||||
*changeAddress({ payload }, { put }) {
|
||||
if (!/^[\da-zA-Z]*$/.test(payload)) return;
|
||||
yield put({
|
||||
type: 'onUpdateState',
|
||||
address: payload,
|
||||
})
|
||||
},
|
||||
|
||||
*changeAmount({ payload }, { put, select }) {
|
||||
if (isNaN(payload)) return;
|
||||
const { coin } = (yield select((state: any) => state.draw)) as DrawModelState;
|
||||
if (!coin) return;
|
||||
const { fee, actual } = calcFee(coin, payload);
|
||||
yield put({
|
||||
type: 'onUpdateState',
|
||||
amount: payload,
|
||||
fee,
|
||||
actual,
|
||||
});
|
||||
},
|
||||
|
||||
*performDraw(_, { put, select }) {
|
||||
const {
|
||||
coin,
|
||||
amount,
|
||||
address,
|
||||
balance,
|
||||
} = (yield select((state: any) => state.draw)) as DrawModelState;
|
||||
if (!coin || !balance) return;
|
||||
if (!address) return message.warn(formatMessage({ id: 'draw.targetPrompt' }));
|
||||
const inputAmount = new BigNumber(amount || 0);
|
||||
if (inputAmount.lt(1000)) return message.warn(formatMessage({ id: 'draw.amountLt' }));
|
||||
if (inputAmount.gt(balance)) return message.warn(formatMessage({ id: 'draw.amountGt' }));
|
||||
yield put({
|
||||
type: 'onUpdateState',
|
||||
confirmVisible: true,
|
||||
confirmLoading: false,
|
||||
password: '',
|
||||
})
|
||||
},
|
||||
|
||||
*draw(_, { put, select, call }) {
|
||||
const {
|
||||
coin,
|
||||
amount,
|
||||
address,
|
||||
password,
|
||||
} = (yield select((state: any) => state.draw)) as DrawModelState;
|
||||
if (!coin) return;
|
||||
if (!address) return message.warn(formatMessage({ id: 'draw.targetPrompt' }));
|
||||
if (!password) return;
|
||||
yield put({ type: 'onUpdateState', confirmLoading: true });
|
||||
try {
|
||||
yield call(withdraw, coin, address, amount, password);
|
||||
yield put({
|
||||
type: 'onUpdateState',
|
||||
confirmVisible: false,
|
||||
});
|
||||
Modal.info({
|
||||
title: formatMessage({ id: 'common.prompt' }),
|
||||
content: formatMessage({ id: 'draw.success' }),
|
||||
okText: formatMessage({ id: 'common.ok' }),
|
||||
cancelText: formatMessage({ id: 'common.cancel' }),
|
||||
maskClosable: false,
|
||||
onOk: () => dispatch(routerRedux.goBack()),
|
||||
});
|
||||
} catch (e) {
|
||||
message.error(e.message);
|
||||
} finally {
|
||||
yield put({ type: 'onUpdateState', confirmLoading: false });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
reducers: {
|
||||
onUpdateState(state, payload) {
|
||||
return {
|
||||
...state,
|
||||
...payload,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default model;
|
||||
55
src/pages/draw/service.ts
Normal file
55
src/pages/draw/service.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import http from '@/utils/http';
|
||||
import _ from 'lodash';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { CoinDrawn, FundBasic } from '@/types/common';
|
||||
import { loadDrawCoins, storeDrawCoins } from '@/utils/storage';
|
||||
|
||||
export async function getCoins(): Promise<CoinDrawn[]> {
|
||||
const coinsCached = loadDrawCoins();
|
||||
if (coinsCached && coinsCached.length) {
|
||||
return coinsCached;
|
||||
}
|
||||
|
||||
const { ok, msg, data } = await http.get<CoinDrawn[]>('/fund/withdrawCoins');
|
||||
|
||||
if (!ok || !data) {
|
||||
throw Error(msg);
|
||||
}
|
||||
storeDrawCoins(data);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function withdraw(
|
||||
coin: CoinDrawn,
|
||||
targetAddress: string,
|
||||
amount: string,
|
||||
password: string,
|
||||
): Promise<void> {
|
||||
const { ok, msg } = await http.post('/fund/withdraw', {
|
||||
coinId: coin.id,
|
||||
targetAddress,
|
||||
amount,
|
||||
password,
|
||||
});
|
||||
if (ok) return
|
||||
throw Error(msg);
|
||||
}
|
||||
|
||||
export function retrieveBalance(basic?: FundBasic, coin?: CoinDrawn): string {
|
||||
if (!basic || !coin) return '';
|
||||
return (basic as any)[coin.label.toLocaleLowerCase()]
|
||||
}
|
||||
|
||||
export function calcFee(coin: CoinDrawn, input: string): { fee: string, actual: string } {
|
||||
const fail = { fee: '', actual: '' };
|
||||
if (!coin || !input) return fail;
|
||||
const i = new BigNumber(input);
|
||||
const f = new BigNumber(coin.fee);
|
||||
if (i.isNaN() || f.isNaN()) return fail;
|
||||
const fee = i.times(f);
|
||||
return {
|
||||
fee: fee.decimalPlaces(2).toString(),
|
||||
actual: i.minus(fee).decimalPlaces(2).toString(),
|
||||
};
|
||||
}
|
||||
117
src/pages/draw/style.less
Normal file
117
src/pages/draw/style.less
Normal file
@@ -0,0 +1,117 @@
|
||||
|
||||
@import '../../global.less';
|
||||
|
||||
.container {
|
||||
height: 100%;
|
||||
background: linear-gradient(to bottom, @blue-one, @blue-two);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
:global {
|
||||
.header {
|
||||
background-color: rgba(255,255,255,0);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.ant-btn-primary {
|
||||
border-width: 0;
|
||||
background-color: @blue-one;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.none {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
padding-top: 40%;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #fff;
|
||||
margin: 10px;
|
||||
padding-bottom: 20px;
|
||||
box-shadow: 1px 1px 8px rgba(0, 0, 0, .2);
|
||||
min-height: 60%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
@pLeft: 48px;
|
||||
@pRight: 30px;
|
||||
@pTop: 20px;
|
||||
|
||||
.btn {
|
||||
margin-left: @pLeft;
|
||||
margin-right: @pRight;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding-left: @pLeft;
|
||||
padding-right: @pRight;
|
||||
padding-top: @pTop;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
@size: 10px;
|
||||
width: @size;
|
||||
height: @size;
|
||||
background-color: @blue-two;
|
||||
border-radius: @size / 2;
|
||||
top: @pTop + @size / 2 + 2px;
|
||||
left: @pLeft / 2 - @size / 2;
|
||||
}
|
||||
|
||||
:global {
|
||||
.app-input {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.with-postfix {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.small-info {
|
||||
font-weight: lighter;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.coin-selection {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
.ant-select {
|
||||
flex-grow: 1;
|
||||
margin: 0 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
:global {
|
||||
canvas {
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user