273 lines
12 KiB
JavaScript
273 lines
12 KiB
JavaScript
import React, { useEffect, useState } from 'react';
|
|
import { Input, Space, Tag, Card, QRCode, Form, Descriptions, Typography, Alert, Button, Modal, Radio, Flex, DatePicker } from 'antd';
|
|
import { CheckCircleOutlined } from '@ant-design/icons';
|
|
import { CentToString } from 'oak-domain/lib/utils/money';
|
|
import Styles from './web.pc.module.less';
|
|
import dayJs from 'dayjs';
|
|
import duration from 'dayjs/plugin/duration';
|
|
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
|
|
dayJs.extend(duration);
|
|
export function RenderOffline(props) {
|
|
const { pay, t, offline, offlines, updateOfflineId, updateExternalId, mode } = props;
|
|
const { iState, phantom3, externalId } = pay;
|
|
const { type, channel, name, qrCode, color } = offline;
|
|
const [show, setShow] = useState(false);
|
|
const [showQrCode, setShowQrCode] = useState(false);
|
|
const items2 = [
|
|
<Form.Item key="type" label={<span className={Styles.bold}>{`${t('channel.prefix')}`}</span>}>
|
|
{iState === 'paying' && offlines.length > 1 ?
|
|
<Button onClick={() => setShow(true)}>{t(`offlineAccount:v.type.${type}`)}</Button> :
|
|
<span className={Styles.value}>{t(`offlineAccount:v.type.${type}`)}</span>}
|
|
</Form.Item>
|
|
];
|
|
if (type === 'bank') {
|
|
items2.push(<Form.Item key="channel" label={t('offlineAccount::label.channel.bank')}>
|
|
<span className={Styles.value}>{channel}</span>
|
|
</Form.Item>, <Form.Item key="name" label={t('offlineAccount::label.name.bank')}>
|
|
<span className={Styles.value}>{name}</span>
|
|
</Form.Item>, <Form.Item key="qrCode" label={t('offlineAccount::label.qrCode.bank')}>
|
|
<span className={Styles.value}>{qrCode}</span>
|
|
</Form.Item>);
|
|
}
|
|
else {
|
|
if (type === 'others') {
|
|
items2.push(<Form.Item key="channel" label={t('offlineAccount::label.channel.others')}>
|
|
<span className={Styles.value}>{channel}</span>
|
|
</Form.Item>);
|
|
}
|
|
if (name) {
|
|
items2.push(<Form.Item key="name" label={t(`offlineAccount::label.name.${type}`)}>
|
|
<span className={Styles.value}>{name}</span>
|
|
</Form.Item>);
|
|
}
|
|
if (qrCode) {
|
|
items2.push(<Form.Item key="qrCode" label={t(`offlineAccount::label.qrCode.${type}`)}>
|
|
<span className={Styles.qrCode} onClick={() => setShowQrCode(true)}>
|
|
<QRCode value={qrCode} size={180} color={color}/>
|
|
</span>
|
|
</Form.Item>);
|
|
}
|
|
}
|
|
items2.push(<Form.Item key="code" label={<span className={Styles.bold}>{t('code.label')}</span>}>
|
|
<Tag color="red">
|
|
<span style={{ fontSize: 'large' }}>{phantom3}</span>
|
|
</Tag>
|
|
</Form.Item>, <Form.Item key="externalId" label={<span className={Styles.bold}>{t('externalId.label')}</span>} help={mode === 'frontend' && iState === 'paying' && t('externalId.help')}>
|
|
<Input value={externalId || ''} onChange={({ currentTarget }) => {
|
|
const { value } = currentTarget;
|
|
updateExternalId(value);
|
|
}} placeholder={t('externalId.placeholder')} disabled={iState !== 'paying' && mode === 'frontend'}/>
|
|
</Form.Item>);
|
|
return (<>
|
|
<Modal open={show} onCancel={() => setShow(false)} closeIcon={null} footer={null}>
|
|
<Radio.Group onChange={({ target }) => {
|
|
updateOfflineId(target.value);
|
|
setShow(false);
|
|
}} value={offline.id}>
|
|
{offlines.map((ele, idx) => (<Radio key={idx} value={ele.id}>
|
|
{t(`offlineAccount:v.type.${ele.type}`)}
|
|
</Radio>))}
|
|
</Radio.Group>
|
|
</Modal>
|
|
<Modal open={showQrCode} closeIcon={null} footer={null} onCancel={() => setShowQrCode(false)}>
|
|
<QRCode value={qrCode} size={400} color={color}/>
|
|
</Modal>
|
|
<Form labelCol={{ span: 8 }} wrapperCol={{ span: 12 }} layout="horizontal" style={{ width: '100%', marginTop: 12 }}>
|
|
{items2}
|
|
</Form>
|
|
</>);
|
|
}
|
|
function Counter(props) {
|
|
const { deadline } = props;
|
|
const [counter, setCounter] = useState('');
|
|
const timerFn = () => {
|
|
const now = Date.now();
|
|
if (now < deadline) {
|
|
const duration = dayJs.duration(deadline - now);
|
|
setCounter(duration.format('HH:mm:ss'));
|
|
setTimeout(timerFn, 1000);
|
|
}
|
|
};
|
|
useEffect(() => {
|
|
timerFn();
|
|
}, []);
|
|
return (<Typography.Title level={3}>{counter}</Typography.Title>);
|
|
}
|
|
function RenderWechatPay(props) {
|
|
const { pay, t } = props;
|
|
const { externalId, wpProduct, timeoutAt, iState } = pay;
|
|
if (iState === 'paid') {
|
|
return (<div className={Styles.paid}>
|
|
<CheckCircleOutlined style={{
|
|
fontSize: 72,
|
|
color: 'green',
|
|
fontWeight: 'bold',
|
|
}}/>
|
|
<div className={Styles.text}>
|
|
{t('success')}
|
|
</div>
|
|
</div>);
|
|
}
|
|
switch (wpProduct.type) {
|
|
case 'native': {
|
|
if (iState === 'paying') {
|
|
return (<div className={Styles.paid}>
|
|
<Counter deadline={timeoutAt}/>
|
|
<QRCode value={externalId} size={280} color="#04BE02"/>
|
|
<div className={Styles.qrCodeTips}>
|
|
{process.env.NODE_ENV === 'production' ?
|
|
<Alert type="info" message={t('wechat.native.tips')}/> :
|
|
<Alert type="warning" message={t('wechat.native.tips2')}/>}
|
|
</div>
|
|
</div>);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
function RenderPayMeta(props) {
|
|
const { pay, notSameApp, t, offlines, offline, updateOfflineId, updateExternalId, mode } = props;
|
|
const { iState, entity } = pay;
|
|
if (entity !== 'offlineAccount' && notSameApp) {
|
|
return <Alert type='warning' message={t('notSameApp')}/>;
|
|
}
|
|
switch (entity) {
|
|
case 'offlineAccount': {
|
|
if (offline && offlines) {
|
|
return (<>
|
|
{iState === 'paying' && (mode === 'frontend'
|
|
? <Alert type='info' message={t('code.help.frontend')}/>
|
|
: <Alert type='info' message={t('code.help.backend')}/>)}
|
|
<RenderOffline t={t} pay={pay} mode={mode} offline={offline} offlines={offlines} updateOfflineId={updateOfflineId} updateExternalId={updateExternalId}/>
|
|
</>);
|
|
}
|
|
return null;
|
|
}
|
|
case 'wpProduct': {
|
|
return <RenderWechatPay pay={pay} t={t}/>;
|
|
}
|
|
}
|
|
// todo 要支持注入第三方支付的渲染组件
|
|
return null;
|
|
}
|
|
export default function Render(props) {
|
|
const { pay, iStateColor, notSameApp, type, startPayable, goBackable, succeedable, oakExecutable, onClose, closable, offline, offlines, autoSuccessAt, mode } = props.data;
|
|
const { t, update, execute, clean, goBack, startPay, succeed } = props.methods;
|
|
const [showSa, setShowSa] = useState(false);
|
|
const [sa, setSa] = useState(Date.now());
|
|
if (pay) {
|
|
const { iState, price, entity } = pay;
|
|
const BtnPart2 = [];
|
|
if (startPayable) {
|
|
BtnPart2.push(<Button style={{ backgroundColor: '#04BE02' }} className={Styles.btnWechatPay} onClick={() => startPay()}>
|
|
{t('pay')}
|
|
</Button>);
|
|
}
|
|
else if (oakExecutable === true) {
|
|
BtnPart2.push(<Button type="primary" onClick={() => execute()}>
|
|
{t('common::action.update')}
|
|
</Button>, <Button onClick={() => clean()}>
|
|
{t('common::reset')}
|
|
</Button>);
|
|
}
|
|
else {
|
|
if (closable) {
|
|
BtnPart2.push(<Button danger onClick={() => {
|
|
Modal.confirm({
|
|
title: t('cc.title'),
|
|
content: t('cc.content'),
|
|
onOk: async () => {
|
|
await execute('close');
|
|
onClose();
|
|
},
|
|
okText: t('common::confirm'),
|
|
cancelText: t('common::action.cancel'),
|
|
});
|
|
}}>
|
|
{t('pay:action.close')}
|
|
</Button>);
|
|
}
|
|
if (goBackable) {
|
|
BtnPart2.push(<Button type="link" onClick={goBack}>
|
|
{t('common::back')}
|
|
</Button>);
|
|
}
|
|
}
|
|
return (<>
|
|
<Card className={Styles.card} title={t('title')} extra={<Space>
|
|
{succeedable ? <Button type="primary" onClick={() => {
|
|
if (autoSuccessAt) {
|
|
succeed(Date.now());
|
|
}
|
|
else {
|
|
setShowSa(true);
|
|
}
|
|
}}>
|
|
{t('pay:action.succeedPaying')}
|
|
</Button> : <Tag color={iStateColor}>
|
|
{t(`pay:v.iState.${iState}`)}
|
|
</Tag>}
|
|
</Space>}>
|
|
<div className={Styles.container}>
|
|
<div className={Styles.detail}>
|
|
<Descriptions column={1} bordered items={[
|
|
{
|
|
key: '0',
|
|
label: t('type.label'),
|
|
children: <span className={Styles.value}>{t(`type.${type}`)}</span>,
|
|
},
|
|
{
|
|
key: '1',
|
|
label: t('pay:attr.price'),
|
|
children: <span className={Styles.value}>{`${t('common::pay.symbol')} ${CentToString(price, 2)}`}</span>,
|
|
},
|
|
{
|
|
key: '2',
|
|
label: t('pay:attr.entity'),
|
|
children: <span className={Styles.value}>{t(`payChannel::${entity}`)}</span>,
|
|
}
|
|
]}/>
|
|
</div>
|
|
<div className={Styles.oper}>
|
|
<RenderPayMeta pay={pay} t={t} mode={mode} notSameApp={notSameApp} offline={offline} offlines={offlines} updateOfflineId={async (entityId) => {
|
|
execute(undefined, undefined, undefined, [
|
|
{
|
|
entity: 'pay',
|
|
operation: {
|
|
id: await generateNewIdAsync(),
|
|
action: 'update',
|
|
data: {
|
|
entity: 'offlineAccount',
|
|
entityId,
|
|
},
|
|
filter: {
|
|
id: props.data.oakId,
|
|
},
|
|
}
|
|
}
|
|
]);
|
|
}} updateExternalId={(externalId) => {
|
|
update({
|
|
externalId,
|
|
});
|
|
}}/>
|
|
</div>
|
|
<div className={Styles.btn}>
|
|
{BtnPart2}
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
<Modal open={showSa} closeIcon={null} title={t('successAt.title')} okText={t('common::confirm')} cancelText={t('common::action.cancel')} onCancel={() => setShowSa(false)} onOk={() => succeed(sa)} width={580}>
|
|
<Flex gap="middle" vertical>
|
|
<Alert type="warning" message={t('successAt.tips')}/>
|
|
<DatePicker maxDate={dayJs()} onChange={(date) => {
|
|
setSa(date.valueOf());
|
|
}} value={dayJs(sa)}/>
|
|
</Flex>
|
|
</Modal>
|
|
</>);
|
|
}
|
|
return null;
|
|
}
|