增加了pay的attrUpdateMatrix

This commit is contained in:
Xu Chang 2024-05-09 12:11:51 +08:00
parent a9161e7210
commit 7082a521cb
29 changed files with 433 additions and 47 deletions

View File

@ -0,0 +1 @@
<view>account detail</view>

View File

@ -0,0 +1,5 @@
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
import { EntityDict } from '../../../oak-app-domain';
export default function Render(props: WebComponentProps<EntityDict, 'accountOper', false, {
accountOpers: RowWithActions<EntityDict, 'accountOper'>[];
}>): string;

View File

@ -0,0 +1,5 @@
export default function Render(props) {
const { accountOpers } = props.data;
const { t } = props.methods;
return '还没有实现todo';
}

View File

@ -50,7 +50,14 @@ export default OakComponent({
},
methods: {
setUseAccount(v) {
this.setState({ useAccount: v }, () => this.tryCreatePay());
const { accountAvailMax } = this.props;
const { order } = this.state;
const accountMaxPrice = Math.min(accountAvailMax, order.price);
this.setState({
useAccount: v,
accountPrice: accountMaxPrice,
rest: order.price - accountMaxPrice,
}, () => this.tryCreatePay());
},
setAccountPrice(price) {
const { order } = this.state;

5
es/components/order/pay/info.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
/// <reference types="react" />
export default function Info(props: {
price: number;
t: (k: string) => string;
}): import("react").JSX.Element;

View File

@ -0,0 +1,11 @@
import Styles from './info.module.less';
export default function Info(props) {
const { price, t } = props;
return (<div className={Styles.info}>
<div className={Styles.should}>{t('price')}</div>
<div className={Styles.price}>
<div>{t('common::pay.symbol')}</div>
<div>{price}</div>
</div>
</div>);
}

View File

@ -0,0 +1,25 @@
.info {
margin: 8px;
height: 140px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: var(--oak-color-info);
.should {
font-size: large;
font-weight: bold;
}
.price {
display: flex;
flex-direction: row;
padding: 3px;
font-size: xx-large;
font-weight: bolder;
color: var(--oak-color-primary);
align-items: baseline;
}
}

23
es/components/order/pay/web.d.ts vendored Normal file
View File

@ -0,0 +1,23 @@
import React from 'react';
import { WebComponentProps } from 'oak-frontend-base';
import { EntityDict } from '../../../oak-app-domain';
import { AccountPayConfig, PayConfig } from '../../../types/PayConfig';
export default function Render(props: WebComponentProps<EntityDict, 'order', false, {
accountId?: string;
accountAvailMax: number;
order: EntityDict['order']['OpSchema'];
activePay?: EntityDict['pay']['OpSchema'];
accountConfig?: AccountPayConfig;
payConfig?: PayConfig;
accountPrice: number;
channel?: string;
meta?: object;
useAccount: boolean;
rest: number;
legal: false;
}, {
setAccountPrice: (price: number) => void;
onPickChannel: (channel: string) => void;
onSetChannelMeta: (meta?: object) => void;
setUseAccount: (v: boolean) => void;
}>): React.JSX.Element | null;

View File

@ -0,0 +1,67 @@
import React from 'react';
import { ToYuan, ToCent } from 'oak-domain/lib/utils/money';
import Styles from './web.mobile.module.less';
import PayChannelPicker from '../../pay/channelPicker';
import { InputNumber } from 'antd';
import { Checkbox, Divider, ErrorBlock } from 'antd-mobile';
import Info from './info';
function RenderPayChannel(props) {
const { price, payConfig, t, channel, meta, onPick, onSetMeta } = props;
return (<div className={Styles.pc1}>
<div className={Styles.content}>
<div>
{t('choose', { price: ToYuan(price) })}
</div>
<Divider />
<PayChannelPicker payConfig={payConfig} channel={channel} meta={meta} onPick={onPick} onSetMeta={onSetMeta}/>
</div>
</div>);
}
function RenderAccountPay(props) {
const { max, t, accountPrice, setAccountPrice, useAccount, setUseAccount, accountAvail } = props;
return (<div className={Styles.pc1}>
<div className={Styles.content}>
<Checkbox checked={useAccount} onChange={() => {
setUseAccount(!useAccount);
if (useAccount) {
setAccountPrice(0);
}
}}>
{t('useAccount')}
</Checkbox>
{useAccount && (<>
<Divider />
<div>
<InputNumber max={ToYuan(max)} addonAfter="¥" value={typeof accountPrice === 'number' ? ToYuan(accountPrice) : null} onChange={(v) => {
if (typeof v === 'number') {
setAccountPrice(Math.floor(ToCent(v)));
}
}}/>
<div className={Styles.tips}>{t('accountMax', { max: ToYuan(accountAvail) })}</div>
</div>
</>)}
</div>
</div>);
}
export default function Render(props) {
const { accountId, accountAvailMax, legal, accountPrice, useAccount, order, activePay, payConfig, channel, meta, rest } = props.data;
const { t, setAccountPrice, onPickChannel, onSetChannelMeta, setUseAccount } = props.methods;
if (order) {
if (activePay) {
return (<ErrorBlock status="default" title={t('paying')}/>);
}
if (!legal) {
return (<ErrorBlock status="default" title={t('illegalState', { state: t(`order:v.iState.${order.iState}`) })}/>);
}
return (<div className={Styles.container}>
<Info t={t} price={ToYuan(order.price)}/>
{accountId && accountAvailMax && <div className={Styles.ctrl}>
<RenderAccountPay max={Math.min(accountAvailMax, rest + accountPrice)} t={t} setAccountPrice={setAccountPrice} useAccount={useAccount} setUseAccount={setUseAccount} accountPrice={accountPrice} accountAvail={accountAvailMax}/>
</div>}
{!!(rest && rest > 0) && <div className={Styles.ctrl}>
<RenderPayChannel payConfig={payConfig} price={rest} t={t} channel={channel} meta={meta} onPick={onPickChannel} onSetMeta={onSetChannelMeta}/>
</div>}
</div>);
}
return null;
}

View File

@ -0,0 +1,36 @@
.container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
background-color: var(--oak-bg-color-page);
// min-height: 500px;
.ctrl {
margin: 8px;
margin-top: 10px;
flex: 1;
.pc1 {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
margin-top: 4px;
.content {
background-color: var(--oak-bg-color-container);
padding: 4px;
border-radius: 2px;
padding: 16px;
.tips {
color: var(--oak-color-warning);
font-size: small;
margin-top: 3px;
}
}
}
}
}

View File

@ -3,6 +3,7 @@ import { ToYuan, ToCent } from 'oak-domain/lib/utils/money';
import Styles from './web.pc.module.less';
import PayChannelPicker from '../../pay/channelPicker';
import { Divider, Checkbox, InputNumber, Flex, Result } from 'antd';
import Info from './info';
function RenderPayChannel(props) {
const { price, payConfig, t, channel, meta, onPick, onSetMeta } = props;
return (<div className={Styles.pc1}>
@ -52,13 +53,7 @@ export default function Render(props) {
return (<Result status="warning" title={t('illegalState', { state: t(`order:v.iState.${order.iState}`) })}/>);
}
return (<div className={Styles.container}>
<div className={Styles.info}>
<div className={Styles.should}>{t('price')}</div>
<div className={Styles.price}>
<div>{t('common::pay.symbol')}</div>
<div>{ToYuan(order.price)}</div>
</div>
</div>
<Info t={t} price={ToYuan(order.price)}/>
{accountId && accountAvailMax && <div className={Styles.ctrl}>
<RenderAccountPay max={Math.min(accountAvailMax, rest + accountPrice)} t={t} setAccountPrice={setAccountPrice} useAccount={useAccount} setUseAccount={setUseAccount} accountPrice={accountPrice} accountAvail={accountAvailMax}/>
</div>}

View File

@ -7,30 +7,6 @@
background-color: var(--oak-bg-color-page);
// min-height: 500px;
.info {
margin: 8px;
height: 140px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: var(--oak-color-info);
.should {
font-size: large;
font-weight: bold;
}
.price {
display: flex;
flex-direction: row;
padding: 3px;
font-size: xx-large;
font-weight: bolder;
color: var(--oak-color-primary)
}
}
.ctrl {
margin: 8px;
margin-top: 10px;

View File

@ -43,7 +43,7 @@ export default function Render(props) {
}
}} value={channel}>
<Space direction="vertical">
{payConfig.map((v) => (<Radio value={v.channel} key={v.channel}>
{payConfig.map((v) => (<Radio className={Styles.radio} value={v.channel} key={v.channel}>
<Space direction="horizontal" align='center'>
<span className={Styles.span}>
{t(`payChannel::${v.channel}`)}

View File

@ -1,3 +1,8 @@
.span {
margin-right: 10px;
}
.radio {
height: 40px;
}

View File

@ -38,6 +38,7 @@ export default OakComponent({
application,
iStateColor,
payConfig,
closable: !!(data?.["#oakLegalActions"]?.includes('close')),
};
},
features: ['application'],

View File

@ -1,5 +1,19 @@
import React from 'react';
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
import { EntityDict } from '../../../oak-app-domain';
import { OfflinePayConfig, PayConfig } from '../../../types/PayConfig';
export declare function RenderOffline(props: {
pay: RowWithActions<EntityDict, 'pay'>;
t: (key: string) => string;
offline: OfflinePayConfig;
updateMeta: (meta: any) => void;
metaUpdatable: boolean;
}): React.JSX.Element;
export default function Render(props: WebComponentProps<EntityDict, 'pay', false, {
pay: RowWithActions<EntityDict, 'pay'>;
}>): null;
application?: EntityDict['application']['Schema'];
iStateColor?: string;
payConfig?: PayConfig;
onClose: () => undefined;
closable: boolean;
}>): React.JSX.Element | null;

View File

@ -1,5 +1,156 @@
export default function Render(props) {
const { pay } = props.data;
const { t } = props.methods;
import React, { useEffect, useState } from 'react';
import { Card, Tag, List, Button, Modal, Form, Selector, TextArea } from 'antd-mobile';
import { QRCode, Alert } from 'antd';
import Styles from './web.mobile.module.less';
import * as dayJs from 'dayjs';
import { CentToString } from 'oak-domain/lib/utils/money';
import { PAY_CHANNEL_OFFLINE_NAME, PAY_CHANNEL_WECHAT_APP_NAME, PAY_CHANNEL_WECHAT_H5_NAME, PAY_CHANNEL_WECHAT_JS_NAME, PAY_CHANNEL_WECHAT_MP_NAME, PAY_CHANNEL_WECHAT_NATIVE_NAME } from '../../../types/PayConfig';
import { PayCircleOutline, GlobalOutline } from 'antd-mobile-icons';
export function RenderOffline(props) {
const { pay, t, offline, updateMeta, metaUpdatable } = props;
const { meta, iState } = pay;
return (<>
<Form layout="horizontal" style={{ width: '100%', marginTop: 12 }}>
<Form.Item label={t("offline.label.tips")}>
<span style={{ wordBreak: 'break-all', textDecoration: 'underline' }}>{offline.tips}</span>
</Form.Item>
{!!(offline.options?.length) && <Form.Item label={t("offline.label.option")}>
<Selector value={meta?.option ? [meta?.option] : undefined} options={offline.options.map(ele => ({
label: ele,
value: ele,
}))} disabled={!metaUpdatable} onChange={(v) => updateMeta({
...meta,
option: v[0],
})}/>
</Form.Item>}
<Form.Item label={t("offline.label.serial")}>
<TextArea autoSize={{ minRows: 3 }} value={meta?.serial} disabled={!metaUpdatable} placeholder={metaUpdatable ? t('offline.placeholder.serial') : t('offline.placeholder.none')} onChange={(value) => updateMeta({
...meta,
serial: value,
})}/>
</Form.Item>
</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 (<div className={Styles.counter}>{counter}</div>);
}
function RenderWechatPay(props) {
const { pay, t } = props;
const { externalId, channel, timeoutAt, iState } = pay;
switch (channel) {
case PAY_CHANNEL_WECHAT_NATIVE_NAME: {
if (iState === 'paying') {
return (<>
<Counter deadline={timeoutAt}/>
<QRCode value={externalId} size={280}/>
<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>
</>);
}
break;
}
}
return null;
}
function RenderPayMeta(props) {
const { pay, application, t, payConfig, updateMeta } = props;
const { iState, channel } = pay;
if (['unpaid', 'paying'].includes(iState) && pay.applicationId !== application.id && channel !== PAY_CHANNEL_OFFLINE_NAME) {
return <Alert type='warning' message={t('notSameApp')}/>;
}
switch (channel) {
case PAY_CHANNEL_OFFLINE_NAME: {
const { '#oakLegalActions': legalActions } = pay;
const metaUpdatable = !!legalActions?.find(ele => typeof ele === 'object'
&& ele.action === 'update'
&& ele.attrs?.includes('meta'));
return (<>
{iState === 'paying' && <Alert type='info' message={t('offline.tips')}/>}
{RenderOffline({
pay,
t,
offline: payConfig.find(ele => ele.channel === PAY_CHANNEL_OFFLINE_NAME),
updateMeta,
metaUpdatable
})}
</>);
}
case PAY_CHANNEL_WECHAT_APP_NAME:
case PAY_CHANNEL_WECHAT_H5_NAME:
case PAY_CHANNEL_WECHAT_JS_NAME:
case PAY_CHANNEL_WECHAT_MP_NAME:
case PAY_CHANNEL_WECHAT_NATIVE_NAME: {
return <RenderWechatPay pay={pay} t={t}/>;
}
}
return null;
}
export default function Render(props) {
const { pay, application, iStateColor, payConfig, oakExecutable, onClose, closable } = props.data;
const { t, update, execute, clean } = props.methods;
if (pay && application) {
const { iState, channel, price, '#oakLegalActions': legalActions } = pay;
return (<div className={Styles.container}>
<Card title={t('title')} extra={<Tag color={iStateColor}>{t(`pay:v.iState.${iState}`)}</Tag>}>
<div>
<List>
<List.Item prefix={<PayCircleOutline />} extra={CentToString(price, 2)}>
{t('pay:attr.price')}
</List.Item>
<List.Item prefix={<GlobalOutline />} extra={t(`payChannel::${channel}`)}>
{t('pay:attr.channel')}
</List.Item>
</List>
</div>
</Card>
<div className={Styles.meta}>
<RenderPayMeta pay={pay} t={t} application={application} payConfig={payConfig} updateMeta={(meta) => update({ meta })}/>
</div>
<div className={Styles.padding}/>
<div className={Styles.btn}>
{oakExecutable === true && (<>
<div className={Styles.btnItem}>
<Button block color='primary' onClick={() => execute()}>
{t('common::action.update')}
</Button>
</div>
<div className={Styles.btnItem}>
<Button block onClick={() => clean()}>
{t('common::reset')}
</Button>
</div>
</>)}
{closable && !(oakExecutable === true) && (<Button block color="primary" onClick={() => {
Modal.confirm({
title: t('cc.title'),
content: t('cc.content'),
onConfirm: async () => {
await execute('close');
onClose();
}
});
}}>
{t('pay:action.close')}
</Button>)}
</div>
</div>);
}
return null;
}

View File

@ -0,0 +1,35 @@
.container {
display: flex;
align-items: stretch;
flex-direction: column;
height: 100%;
}
.meta {
margin-top: 40px;
display: flex;
flex-direction: column;
align-items: center;
.qrCodeTips {
margin-top: 28px;
}
.counter {
font-size: var(--oak-font-size-headline-medium);
font-weight: bolder;
}
}
.padding {
flex: 1;
}
.btn {
display: flex;
flex-direction: row;
.btnItem {
flex: 1;
}
}

View File

@ -15,4 +15,5 @@ export default function Render(props: WebComponentProps<EntityDict, 'pay', false
iStateColor?: string;
payConfig?: PayConfig;
onClose: () => undefined;
closable: boolean;
}>): React.JSX.Element | null;

View File

@ -12,7 +12,7 @@ export function RenderOffline(props) {
return (<>
<Form labelCol={{ span: 4 }} wrapperCol={{ span: 14 }} layout="horizontal" style={{ width: '100%', marginTop: 12 }}>
<Form.Item label={t("offline.label.tips")}>
<span style={{ wordBreak: 'break-all' }}>{offline.tips}</span>
<span style={{ wordBreak: 'break-all', textDecoration: 'underline' }}>{offline.tips}</span>
</Form.Item>
<Form.Item label={t("offline.label.option")}>
<Select value={meta?.option} options={offline.options?.map(ele => ({
@ -58,7 +58,7 @@ function RenderWechatPay(props) {
<Counter deadline={timeoutAt}/>
<QRCode value={externalId} size={280}/>
<div className={Styles.qrCodeTips}>
{process.env.NODE_ENV === 'production' ?
{process.env.NODE_ENV === 'production' ?
<Alert type="info" message={t('wechat.native.tips')}/> :
<Alert type="warning" message={t('wechat.native.tips2')}/>}
</div>
@ -103,11 +103,10 @@ function RenderPayMeta(props) {
return null;
}
export default function Render(props) {
const { pay, application, iStateColor, payConfig, oakExecutable, onClose } = props.data;
const { t, update, execute } = props.methods;
const { pay, application, iStateColor, payConfig, oakExecutable, onClose, closable } = props.data;
const { t, update, execute, clean } = props.methods;
if (pay && application) {
const { iState, channel, price, '#oakLegalActions': legalActions } = pay;
const closable = !!legalActions?.find(ele => ele === 'close');
return (<Card title={t('title')} extra={<Tag color={iStateColor}>{t(`pay:v.iState.${iState}`)}</Tag>}>
<div className={Styles.container}>
<div className={Styles.detail}>
@ -128,9 +127,14 @@ export default function Render(props) {
<RenderPayMeta pay={pay} t={t} application={application} payConfig={payConfig} updateMeta={(meta) => update({ meta })}/>
</div>
<div className={Styles.btn}>
{oakExecutable === true && (<Button onClick={() => execute()}>
{t('common::action.update')}
</Button>)}
{oakExecutable === true && (<>
<Button type="primary" onClick={() => execute()}>
{t('common::action.update')}
</Button>
<Button onClick={() => clean()}>
{t('common::reset')}
</Button>
</>)}
{closable && !(oakExecutable === true) && (<Button type="primary" onClick={() => {
Modal.confirm({
title: t('cc.title'),

View File

@ -22,6 +22,9 @@ const attrUpdateMatrix = {
},
channel: PAY_CHANNEL_ACCOUNT_NAME,
}
},
paid: {
actions: ['succeedPaying'],
}
}
};

View File

@ -40,7 +40,7 @@ export const entityDesc = {
zh_CN: {
name: '订单',
attr: {
price: '订单金额',
price: '应支付金额',
paid: '已支付金额',
refunded: '已退款金额',
iState: '支付状态',

View File

@ -68,6 +68,9 @@ const watchers = [
results.push(result);
}
}
if (results.length === 0) {
return {};
}
return results.reduce((prev, cur) => mergeOperationResult(prev, cur));
}
}

View File

@ -24,6 +24,9 @@ const attrUpdateMatrix = {
},
channel: PayConfig_1.PAY_CHANNEL_ACCOUNT_NAME,
}
},
paid: {
actions: ['succeedPaying'],
}
}
};

View File

@ -43,7 +43,7 @@ exports.entityDesc = {
zh_CN: {
name: '订单',
attr: {
price: '订单金额',
price: '应支付金额',
paid: '已支付金额',
refunded: '已退款金额',
iState: '支付状态',

View File

@ -71,6 +71,9 @@ const watchers = [
results.push(result);
}
}
if (results.length === 0) {
return {};
}
return results.reduce((prev, cur) => (0, operationResult_1.mergeOperationResult)(prev, cur));
}
}

View File

@ -0,0 +1 @@
<view>account detail</view>

View File

@ -26,6 +26,9 @@ const attrUpdateMatrix: AttrUpdateMatrix<EntityDict> = {
},
channel: PAY_CHANNEL_ACCOUNT_NAME,
}
},
paid: {
actions: ['succeedPaying'],
}
}
};

View File

@ -76,6 +76,9 @@ const watchers: Watcher<EntityDict, 'pay', BRC>[] = [
}
}
if (results.length === 0) {
return {};
}
return results.reduce(
(prev, cur) => mergeOperationResult(prev, cur)
);