This commit is contained in:
saplf
2019-09-05 23:24:49 +08:00
parent df25ff4244
commit c8f919ce5a
19 changed files with 658 additions and 37 deletions

166
src/pages/draw/index.tsx Normal file
View 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
View 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
View 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
View 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;
}
}
}