merge
This commit is contained in:
commit
8f66c1281f
|
|
@ -63,9 +63,15 @@ const checkers = [
|
|||
}
|
||||
break;
|
||||
}
|
||||
case 'loss': {
|
||||
if (totalPlus >= 0 || availPlus > 0 || totalPlus !== availPlus) {
|
||||
throw new OakInputIllegalException('accountOper', ['availPlus'], 'accountOper为consume时,其totalPlus/availPlus必须为0或负数,且totalPlus的绝对值要更大');
|
||||
case 'earn': {
|
||||
if (totalPlus <= 0 || availPlus < 0) {
|
||||
throw new OakInputIllegalException('accountOper', ['availPlus'], 'accountOper为earn时,其totalPlus必须为正数、availPlus必须为非负数');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'encash': {
|
||||
if (totalPlus !== 0 || availPlus <= 0) {
|
||||
throw new OakInputIllegalException('accountOper', ['availPlus'], 'accountOper为encash时,其totalPlus必须为0、availPlus必须为正数');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,2 @@
|
|||
import { 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 { OakInputIllegalException } from 'oak-domain/lib/types';
|
||||
const checkers = [
|
||||
{
|
||||
type: 'data',
|
||||
entity: 'application',
|
||||
action: 'update',
|
||||
checker: (data, context) => {
|
||||
const { payConfig } = (data || {});
|
||||
if (payConfig) {
|
||||
const wechatPayConfigs = payConfig.filter(ele => [
|
||||
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
|
||||
].includes(ele.channel));
|
||||
if (wechatPayConfigs) {
|
||||
wechatPayConfigs.forEach((config) => {
|
||||
const { apiV3Key } = config;
|
||||
if (apiV3Key?.length && apiV3Key.length !== 32) {
|
||||
throw new OakInputIllegalException('appliation', ['payConfig'], 'apiV3Key长度只能是32位');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
];
|
||||
const checkers = [];
|
||||
export default checkers;
|
||||
|
|
|
|||
|
|
@ -2,10 +2,14 @@ import aoCheckers from './accountOper';
|
|||
import payCheckers from './pay';
|
||||
import orderCheckers from './order';
|
||||
import applicationCheckers from './application';
|
||||
import offlineAccountCheckers from './offlineAccount';
|
||||
import wpProductCheckers from './wpProduct';
|
||||
const checkers = [
|
||||
...aoCheckers,
|
||||
...payCheckers,
|
||||
...orderCheckers,
|
||||
...applicationCheckers,
|
||||
...offlineAccountCheckers,
|
||||
...wpProductCheckers,
|
||||
];
|
||||
export default checkers;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
import { Checker } from 'oak-domain/lib/types/Auth';
|
||||
import { EntityDict } from '../oak-app-domain';
|
||||
import { RuntimeCxt } from '../types/RuntimeCxt';
|
||||
declare const checkers: Checker<EntityDict, 'offlineAccount', RuntimeCxt>[];
|
||||
export default checkers;
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import assert from 'assert';
|
||||
import { OakAttrNotNullException, OakInputIllegalException } from 'oak-domain/lib/types';
|
||||
import { pipeline } from 'oak-domain/lib/utils/executor';
|
||||
function checkAttributes(data) {
|
||||
const { type, channel, name, qrCode } = data;
|
||||
switch (type) {
|
||||
case 'bank': {
|
||||
if (!channel || !name || !qrCode) {
|
||||
throw new OakAttrNotNullException('offlineAccount', ['channel', 'name', 'qrCode'].filter(ele => !data[ele]));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'shouqianba':
|
||||
case 'wechat':
|
||||
case 'alipay': {
|
||||
if (!name && !qrCode) {
|
||||
throw new OakInputIllegalException('offlineAccount', ['name', 'qrCode'], 'offlineAccount::error.nameQrCodeBothNull');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'others': {
|
||||
if (!name && !qrCode) {
|
||||
throw new OakAttrNotNullException('offlineAccount', ['name', 'qrCode']);
|
||||
}
|
||||
if (!channel) {
|
||||
throw new OakAttrNotNullException('offlineAccount', ['channel']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const checkers = [
|
||||
{
|
||||
entity: 'offlineAccount',
|
||||
action: 'create',
|
||||
type: 'data',
|
||||
checker(data) {
|
||||
assert(!(data instanceof Array));
|
||||
checkAttributes(data);
|
||||
}
|
||||
},
|
||||
{
|
||||
entity: 'offlineAccount',
|
||||
action: 'update',
|
||||
type: 'logicalData',
|
||||
checker: (operation, context) => {
|
||||
const { data, filter } = operation;
|
||||
return pipeline(() => context.select('offlineAccount', {
|
||||
data: {
|
||||
id: 1,
|
||||
type: 1,
|
||||
channel: 1,
|
||||
name: 1,
|
||||
qrCode: 1,
|
||||
},
|
||||
filter
|
||||
}, { dontCollect: true }), (accounts) => {
|
||||
accounts.forEach((ele) => checkAttributes(Object.assign(ele, data)));
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
export default checkers;
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
import { OakInputIllegalException } from 'oak-domain/lib/types';
|
||||
import { CHECKER_MAX_PRIORITY } from 'oak-domain/lib/types/Trigger';
|
||||
import { pipeline } from 'oak-domain/lib/utils/executor';
|
||||
import assert from 'assert';
|
||||
import { PAY_CHANNEL_ACCOUNT_NAME, PAY_CHANNEL_OFFLINE_NAME } from '../types/PayConfig';
|
||||
const checkers = [
|
||||
{
|
||||
entity: 'pay',
|
||||
|
|
@ -24,26 +22,19 @@ const checkers = [
|
|||
type: 'data',
|
||||
action: 'create',
|
||||
checker(data) {
|
||||
const { channel, price, orderId, accountId } = data;
|
||||
const { entity, entityId, price, orderId, depositId } = data;
|
||||
if (price <= 0) {
|
||||
throw new OakInputIllegalException('pay', ['price'], '支付金额必须大于0');
|
||||
}
|
||||
if (!orderId) {
|
||||
// 充值类订单
|
||||
if (!accountId) {
|
||||
if (!depositId) {
|
||||
throw new OakInputIllegalException('pay', ['accountId'], '充值类支付必须指定accountId');
|
||||
}
|
||||
else if (channel === PAY_CHANNEL_ACCOUNT_NAME) {
|
||||
else if (entity === 'account') {
|
||||
throw new OakInputIllegalException('pay', ['channel'], '充值类支付不能使用帐户支付');
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (channel === PAY_CHANNEL_ACCOUNT_NAME) {
|
||||
if (!accountId) {
|
||||
throw new OakInputIllegalException('pay', ['accountId'], '使用account支付必须指向accountId');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -103,7 +94,7 @@ const checkers = [
|
|||
}
|
||||
// 非root用户只能手动成功offline类型的pay
|
||||
return {
|
||||
channel: PAY_CHANNEL_OFFLINE_NAME,
|
||||
entity: 'offlineAccount',
|
||||
};
|
||||
}
|
||||
},
|
||||
|
|
@ -112,11 +103,11 @@ const checkers = [
|
|||
entity: 'pay',
|
||||
type: 'logical',
|
||||
action: ['continuePaying', 'startPaying'],
|
||||
priority: CHECKER_MAX_PRIORITY - 1, // 要超过action矩阵定义的赋state值
|
||||
// priority: CHECKER_MAX_PRIORITY - 1, // 要超过action矩阵定义的赋state值
|
||||
checker: (operation, context) => {
|
||||
const { data, filter } = operation;
|
||||
assert(filter && typeof filter.id === 'string');
|
||||
const { paid } = data;
|
||||
const { paid } = data || {};
|
||||
if (paid) {
|
||||
return pipeline(() => context.select('pay', {
|
||||
data: {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
import { Checker } from 'oak-domain/lib/types/Auth';
|
||||
import { EntityDict } from '../oak-app-domain';
|
||||
import { RuntimeCxt } from '../types/RuntimeCxt';
|
||||
declare const checkers: Checker<EntityDict, 'wpProduct', RuntimeCxt>[];
|
||||
export default checkers;
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
import { pipeline } from 'oak-domain/lib/utils/executor';
|
||||
import assert from 'assert';
|
||||
import { getAppTypeFromProductType } from '../utils/wpProduct';
|
||||
import { OakInputIllegalException } from 'oak-domain/lib/types';
|
||||
const checkers = [
|
||||
{
|
||||
entity: 'wpProduct',
|
||||
action: 'create',
|
||||
type: 'row',
|
||||
filter(operation) {
|
||||
const { data } = operation;
|
||||
if (data) {
|
||||
const { type, enabled } = data;
|
||||
if (enabled) {
|
||||
return {
|
||||
application: {
|
||||
wpProduct$application: {
|
||||
"#sqp": 'not in',
|
||||
enabled: true,
|
||||
type,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
errMsg: '同一应用上不能存在同样类型的支付产品',
|
||||
},
|
||||
{
|
||||
entity: 'wpProduct',
|
||||
action: 'update',
|
||||
type: 'row',
|
||||
filter(operation, context) {
|
||||
const { data, filter } = operation;
|
||||
if (data && data.enabled) {
|
||||
assert(filter.id && typeof filter.id === 'string');
|
||||
return pipeline(() => context.select('wpProduct', {
|
||||
data: {
|
||||
id: 1,
|
||||
type: 1,
|
||||
applicationId: 1,
|
||||
},
|
||||
filter,
|
||||
}, { dontCollect: true }), (wpProducts) => {
|
||||
const [wpProduct] = wpProducts;
|
||||
const { type } = wpProduct;
|
||||
return {
|
||||
application: {
|
||||
wpProduct$application: {
|
||||
"#sqp": 'not in',
|
||||
enabled: true,
|
||||
type,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
errMsg: 'error::wpProduct.repeatProductsOnSameApp',
|
||||
},
|
||||
{
|
||||
entity: 'wpProduct',
|
||||
action: 'create',
|
||||
type: 'logicalData',
|
||||
checker(operation, context) {
|
||||
const { data } = operation;
|
||||
if (data) {
|
||||
const { type, applicationId } = data;
|
||||
if (type && applicationId) {
|
||||
return pipeline(() => context.select('application', {
|
||||
data: {
|
||||
id: 1,
|
||||
type: 1,
|
||||
config: 1,
|
||||
},
|
||||
filter: {
|
||||
id: applicationId,
|
||||
}
|
||||
}, { dontCollect: true }), (applications) => {
|
||||
const { type, config } = applications[0];
|
||||
if (!getAppTypeFromProductType(data.type).includes(type)) {
|
||||
throw new OakInputIllegalException('wpProduct', ['applicationId'], 'error::wpProduct.TypeConflict');
|
||||
}
|
||||
switch (type) {
|
||||
case 'web': {
|
||||
const { wechat } = config;
|
||||
if (!wechat?.appId) {
|
||||
throw new OakInputIllegalException('wpProduct', ['applicationId'], 'error::wpProduct.NoWechatInfoOnApp');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'wechatMp': {
|
||||
const { appId } = config;
|
||||
if (!appId) {
|
||||
throw new OakInputIllegalException('wpProduct', ['applicationId'], 'error::wpProduct.NoWechatInfoOnApp');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'wechatPublic': {
|
||||
const { appId } = config;
|
||||
if (!appId) {
|
||||
throw new OakInputIllegalException('wpProduct', ['applicationId'], 'error::wpProduct.NoWechatInfoOnApp');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
];
|
||||
export default checkers;
|
||||
|
|
@ -20,7 +20,7 @@ declare const List: <T extends keyof EntityDict>(props: ReactComponentProps<Enti
|
|||
rowSelection?: any;
|
||||
hideHeader?: boolean | undefined;
|
||||
disableSerialNumber?: boolean | undefined;
|
||||
size?: "small" | "large" | "middle" | undefined;
|
||||
size?: "small" | "middle" | "large" | undefined;
|
||||
scroll?: ({
|
||||
x?: string | number | true | undefined;
|
||||
y?: string | number | undefined;
|
||||
|
|
@ -46,7 +46,7 @@ declare const ListPro: <T extends keyof EntityDict>(props: {
|
|||
tablePagination?: any;
|
||||
rowSelection?: any;
|
||||
disableSerialNumber?: boolean | undefined;
|
||||
size?: "small" | "large" | "middle" | undefined;
|
||||
size?: "small" | "middle" | "large" | undefined;
|
||||
scroll?: any;
|
||||
locale?: any;
|
||||
opWidth?: number | undefined;
|
||||
|
|
@ -58,14 +58,14 @@ declare const Detail: <T extends keyof EntityDict>(props: ReactComponentProps<En
|
|||
data: Partial<EntityDict[T]["Schema"]>;
|
||||
title?: string | undefined;
|
||||
bordered?: boolean | undefined;
|
||||
layout?: "horizontal" | "vertical" | undefined;
|
||||
layout?: "vertical" | "horizontal" | undefined;
|
||||
}>) => React.ReactElement;
|
||||
declare const Upsert: <T extends keyof EntityDict>(props: ReactComponentProps<EntityDict, T, false, {
|
||||
helps: Record<string, string>;
|
||||
entity: T;
|
||||
attributes: OakAbsAttrUpsertDef<EntityDict, T, string | number>[];
|
||||
data: EntityDict[T]["Schema"];
|
||||
layout: "horizontal" | "vertical";
|
||||
layout: "vertical" | "horizontal";
|
||||
mode: "default" | "card";
|
||||
}>) => React.ReactElement;
|
||||
export { FilterPanel, List, ListPro, Detail, Upsert, ReactComponentProps, ColumnProps, RowWithActions, OakExtraActionProps, OakAbsAttrDef, onActionFnDef, };
|
||||
|
|
|
|||
|
|
@ -5,6 +5,6 @@
|
|||
"l-input": "@oak-frontend-base/miniprogram_npm/lin-ui/input/index",
|
||||
"l-button": "@oak-frontend-base/miniprogram_npm/lin-ui/button/index",
|
||||
"l-notice-bar": "@oak-frontend-base/miniprogram_npm/lin-ui/notice-bar/index",
|
||||
"pay-channel-picker": "../../pay/channelPicker/index"
|
||||
"pay-channel-picker": "../../pay/channelPicker2/index"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import React from 'react';
|
||||
import ChannelPicker from '../../pay/channelPicker';
|
||||
import { Form, Input, NoticeBar } from 'antd-mobile';
|
||||
export default function Render(props) {
|
||||
const { depositMax, payConfig, tips, price, channel, meta, priceStr, onSetChannel, onSetMeta } = props.data;
|
||||
|
|
@ -22,9 +21,15 @@ export default function Render(props) {
|
|||
{price > 0 && <Form.Item label={<span>
|
||||
{t('label.channel')}:
|
||||
</span>}>
|
||||
<ChannelPicker payConfig={payConfig} onPick={(channel) => {
|
||||
onSetChannel(channel);
|
||||
}} channel={channel} meta={meta} onSetMeta={(meta) => onSetMeta(meta)}/>
|
||||
{/* <ChannelPicker
|
||||
payConfig={payConfig}
|
||||
onPick={(channel) => {
|
||||
onSetChannel(channel);
|
||||
}}
|
||||
channel={channel}
|
||||
meta={meta}
|
||||
onSetMeta={(meta) => onSetMeta(meta)}
|
||||
/> */}
|
||||
</Form.Item>}
|
||||
</Form>);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Form, InputNumber, Alert } from 'antd';
|
||||
import ChannelPicker from '../../pay/channelPicker';
|
||||
import { ToYuan } from 'oak-domain/lib/utils/money';
|
||||
import Styles from './web.pc.module.less';
|
||||
export default function Render(props) {
|
||||
|
|
@ -17,9 +16,15 @@ export default function Render(props) {
|
|||
{price > 0 ? <Form.Item label={<span style={{ marginTop: 10 }}>
|
||||
{t('label.channel')}:
|
||||
</span>}>
|
||||
<ChannelPicker payConfig={payConfig} onPick={(channel) => {
|
||||
onSetChannel(channel);
|
||||
}} channel={channel} meta={meta} onSetMeta={(meta) => onSetMeta(meta)}/>
|
||||
{/* <ChannelPicker
|
||||
payConfig={payConfig}
|
||||
onPick={(channel) => {
|
||||
onSetChannel(channel);
|
||||
}}
|
||||
channel={channel}
|
||||
meta={meta}
|
||||
onSetMeta={(meta) => onSetMeta(meta)}
|
||||
/> */}
|
||||
</Form.Item> : <div style={{ height: 120 }}/>}
|
||||
</Form>);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "account", false, {
|
||||
depositMinCent: number;
|
||||
depositMaxCent: number;
|
||||
onDepositPayId: (payId: string) => void;
|
||||
onGoToUnfinishedPay: (payId: string) => void;
|
||||
onWithdraw: () => void;
|
||||
textColor: string;
|
||||
priceColor: string;
|
||||
bgColor: string;
|
||||
}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { CentToString, ToYuan } from "oak-domain/lib/utils/money";
|
||||
import { CentToString } from "oak-domain/lib/utils/money";
|
||||
import { generateNewIdAsync } from "oak-domain/lib/utils/uuid";
|
||||
import assert from 'assert';
|
||||
import { DATA_SUBSCRIBER_KEYS } from "../../../config/constants";
|
||||
|
|
@ -9,24 +9,26 @@ export default OakComponent({
|
|||
id: 1,
|
||||
total: 1,
|
||||
avail: 1,
|
||||
systemId: 1,
|
||||
ofSystemId: 1,
|
||||
entity: 1,
|
||||
entityId: 1,
|
||||
pay$account: {
|
||||
$entity: 'pay',
|
||||
deposit$account: {
|
||||
$entity: 'deposit',
|
||||
data: {
|
||||
id: 1,
|
||||
pay$deposit: {
|
||||
$entity: 'pay',
|
||||
data: {
|
||||
id: 1,
|
||||
},
|
||||
filter: {
|
||||
iState: 'paying',
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
iState: {
|
||||
$in: ['unpaid', 'paying'],
|
||||
},
|
||||
orderId: {
|
||||
$exists: false,
|
||||
},
|
||||
iState: 'depositing',
|
||||
},
|
||||
indexFrom: 0,
|
||||
count: 1,
|
||||
},
|
||||
accountOper$account: {
|
||||
$entity: 'accountOper',
|
||||
|
|
@ -51,38 +53,27 @@ export default OakComponent({
|
|||
properties: {
|
||||
depositMinCent: 0,
|
||||
depositMaxCent: 1000000,
|
||||
onDepositPayId: (payId) => undefined,
|
||||
onGoToUnfinishedPay: (payId) => undefined,
|
||||
onWithdraw: () => undefined,
|
||||
textColor: '#B7966E',
|
||||
priceColor: '#8A6321',
|
||||
bgColor: '#EAD8BC',
|
||||
},
|
||||
formData({ data }) {
|
||||
const unfinishedPayId = data?.pay$account?.[0]?.id;
|
||||
const unfinishedDepositId = data?.deposit$account?.[0]?.id;
|
||||
return {
|
||||
account: data,
|
||||
availStr: data?.avail && CentToString(data.avail, 2),
|
||||
totalStr: data?.total && CentToString(data.total, 2),
|
||||
unfinishedPayId,
|
||||
loanStr: (data?.avail !== undefined && data?.total !== undefined) && CentToString(data.total - data.avail, 2),
|
||||
unfinishedDepositId,
|
||||
};
|
||||
},
|
||||
actions: ['deposit', 'withdraw'],
|
||||
methods: {
|
||||
async createDepositPay() {
|
||||
const { depPrice, depositChannel, depositMeta } = this.state;
|
||||
async newDeposit() {
|
||||
const { depPrice, depositChannel, depositLoss } = this.state;
|
||||
const payId = await generateNewIdAsync();
|
||||
const { oakId, depositMaxCent, depositMinCent } = this.props;
|
||||
if (depPrice > depositMaxCent) {
|
||||
this.setMessage({
|
||||
type: 'error',
|
||||
content: this.t('error.maxOverflow', { value: ToYuan(depositMaxCent) }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
else if (depPrice < depositMinCent) {
|
||||
this.setMessage({
|
||||
type: 'error',
|
||||
content: this.t('error.minOverflow', { value: ToYuan(depositMinCent) }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.setDepositing(true);
|
||||
try {
|
||||
await this.execute(undefined, undefined, undefined, [
|
||||
|
|
@ -92,21 +83,35 @@ export default OakComponent({
|
|||
id: await generateNewIdAsync(),
|
||||
action: 'deposit',
|
||||
data: {
|
||||
pay$account: {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: payId,
|
||||
channel: depositChannel,
|
||||
price: depPrice,
|
||||
meta: depositMeta,
|
||||
},
|
||||
},
|
||||
deposit$account: [
|
||||
{
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: await generateNewIdAsync(),
|
||||
price: depPrice,
|
||||
loss: depositLoss[0] || 0,
|
||||
creatorId: this.features.token.getUserId(),
|
||||
pay$deposit: [
|
||||
{
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'create',
|
||||
data: {
|
||||
id: payId,
|
||||
price: depPrice,
|
||||
entity: depositChannel.entity,
|
||||
entityId: depositChannel.entityId,
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
filter: {
|
||||
id: oakId,
|
||||
id: this.props.oakId,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
]);
|
||||
this.setDepositing(false);
|
||||
|
|
@ -116,11 +121,8 @@ export default OakComponent({
|
|||
throw err;
|
||||
}
|
||||
this.onDepositModalClose();
|
||||
const { onDepositPayId } = this.props;
|
||||
onDepositPayId && onDepositPayId(payId);
|
||||
},
|
||||
setDepositMeta(depositMeta) {
|
||||
this.setState({ depositMeta });
|
||||
const { onGoToUnfinishedPay } = this.props;
|
||||
onGoToUnfinishedPay && onGoToUnfinishedPay(payId);
|
||||
},
|
||||
setDepositOpen(depositOpen) {
|
||||
this.setState({ depositOpen });
|
||||
|
|
@ -129,17 +131,27 @@ export default OakComponent({
|
|||
this.setState({ ufOpen });
|
||||
},
|
||||
setDepPrice(depPrice) {
|
||||
this.setState({ depPrice });
|
||||
this.setState({ depPrice }, () => this.setDepositLoss());
|
||||
},
|
||||
setDepositChannel(depositChannel) {
|
||||
this.setState({ depositChannel });
|
||||
this.setState({ depositChannel }, () => this.setDepositLoss());
|
||||
},
|
||||
setDepositLoss() {
|
||||
const { depPrice, depositChannel } = this.state;
|
||||
if (depPrice && depositChannel) {
|
||||
const depositLoss = this.features.pay.calcDepositLoss(depPrice, depositChannel);
|
||||
this.setState({ depositLoss });
|
||||
}
|
||||
else {
|
||||
this.setState({ depositLoss: [0, '', undefined] });
|
||||
}
|
||||
},
|
||||
setDepositing(depositing) {
|
||||
this.setState({ depositing });
|
||||
},
|
||||
onDepositClick() {
|
||||
const { unfinishedPayId } = this.state;
|
||||
if (unfinishedPayId) {
|
||||
const { unfinishedDepositId } = this.state;
|
||||
if (unfinishedDepositId) {
|
||||
this.setUfOpen(true);
|
||||
}
|
||||
else {
|
||||
|
|
@ -150,15 +162,23 @@ export default OakComponent({
|
|||
this.setDepositOpen(false);
|
||||
this.setDepPrice(null);
|
||||
this.setDepositChannel(undefined);
|
||||
this.setDepositMeta(undefined);
|
||||
},
|
||||
onUfModalClose() {
|
||||
this.setUfOpen(false);
|
||||
},
|
||||
onUnfinishedPayClickedMp() {
|
||||
const { onDepositPayId } = this.props;
|
||||
const { unfinishedPayId } = this.state;
|
||||
onDepositPayId && onDepositPayId(unfinishedPayId);
|
||||
onUnfinishedDepositClick() {
|
||||
const { onGoToUnfinishedPay } = this.props;
|
||||
const { unfinishedDepositId } = this.state;
|
||||
const [pay] = this.features.cache.get('pay', {
|
||||
data: {
|
||||
id: 1,
|
||||
},
|
||||
filter: {
|
||||
depositId: unfinishedDepositId,
|
||||
iState: 'paying',
|
||||
},
|
||||
});
|
||||
onGoToUnfinishedPay && onGoToUnfinishedPay(pay.id);
|
||||
},
|
||||
onWithdrawClick() {
|
||||
this.props.onWithdraw();
|
||||
|
|
@ -169,11 +189,10 @@ export default OakComponent({
|
|||
ufOpen: false,
|
||||
depPrice: null,
|
||||
depositChannel: undefined,
|
||||
depositMeta: undefined,
|
||||
depositLoss: [0, '', {}],
|
||||
depositing: false,
|
||||
setDepPriceMp(price) { this.setDepPrice(price); },
|
||||
setDepositChannelMp(depositChannel) { this.setDepositChannel(depositChannel); },
|
||||
setDepositMetaMp(depositMeta) { this.setDepositMeta(depositMeta); }
|
||||
},
|
||||
lifetimes: {
|
||||
ready() {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"l-button": "@oak-frontend-base/miniprogram_npm/lin-ui/button/index",
|
||||
"l-popup": "@oak-frontend-base/miniprogram_npm/lin-ui/popup/index",
|
||||
"l-dialog": "@oak-frontend-base/miniprogram_npm/lin-ui/dialog/index",
|
||||
"account-deposit": "../deposit/index"
|
||||
"deposit-new": "../../deposit/new/index"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,28 +10,47 @@
|
|||
|
||||
|
||||
.info {
|
||||
height: 600rpx;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
height: 480rpx;
|
||||
padding: 48rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.grid {
|
||||
padding: 20rpx;
|
||||
.top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.middle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 28rpx;
|
||||
color: @oak-color-primary;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
.bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 28rpx;
|
||||
color: @oak-color-primary;
|
||||
}
|
||||
.box {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
row-gap: 16rpx;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 28rpx;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
.fortify {
|
||||
|
|
@ -41,17 +60,31 @@
|
|||
|
||||
.value {
|
||||
font-size: x-large;
|
||||
font-weight: bolder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 40%;
|
||||
display: flex;
|
||||
align-self: stretch;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
|
||||
.item {
|
||||
flex: 1,
|
||||
width: 100%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.my-button {
|
||||
font-size: 32rpx;
|
||||
border-radius: 8rpx !important;
|
||||
}
|
||||
|
||||
.withdrawBtn {
|
||||
color: #333;
|
||||
border: 2rpx solid #eee;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,30 +1,41 @@
|
|||
<view class="container">
|
||||
<view class="info">
|
||||
<view class="grid fortify">
|
||||
<view class="label">{{t('avail')}}:</view>
|
||||
<view class="value">{{t('common::pay.symbol')}} {{availStr}}</view>
|
||||
<view class="info" style="background-color:{{bgColor}};color:{{textColor}};">
|
||||
<view class="top label">{{t('history')}}</view>
|
||||
<view class="middle">
|
||||
<view class="box fortify">
|
||||
<view class="label">{{t('avail')}}{{t('yuan')}}</view>
|
||||
<view class="value" style="color:{{priceColor}}">{{t('common::pay.symbol')}} {{availStr}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="grid">
|
||||
<view class="label">{{t('total')}}:</view>
|
||||
<view class="value">{{t('common::pay.symbol')}} {{totalStr}}</view>
|
||||
<view class="bottom">
|
||||
<view class="box">
|
||||
<view class="label">{{t('total')}}{{t('yuan')}}</view>
|
||||
<view class="value" style="color:{{priceColor}}">{{t('common::pay.symbol')}} {{totalStr}}</view>
|
||||
</view>
|
||||
<view class="box">
|
||||
<view class="label">{{t('loan')}}{{t('yuan')}}</view>
|
||||
<view class="value" style="color:{{priceColor}}">{{t('common::pay.symbol')}} {{loanStr}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view style="flex: 1" />
|
||||
<view style="height:30vh;" />
|
||||
<view class="btn">
|
||||
<view class="item">
|
||||
<l-button
|
||||
type="default"
|
||||
size="long"
|
||||
bind:lintap="onDepositClick"
|
||||
bgColor="{{textColor}}"
|
||||
l-class="my-button"
|
||||
>
|
||||
{{t('account:action.deposit')}}
|
||||
</l-button>
|
||||
</view>
|
||||
<view class="item">
|
||||
<l-button
|
||||
type="success"
|
||||
size="long"
|
||||
plain="{{true}}"
|
||||
bind:lintap="onWithdrawClick"
|
||||
l-class="withdrawBtn my-button"
|
||||
>
|
||||
{{t('account:action.withdraw')}}
|
||||
</l-button>
|
||||
|
|
@ -37,14 +48,15 @@
|
|||
bind:lintap="onDepositModalClose"
|
||||
>
|
||||
<view class="ad-container">
|
||||
<account-deposit
|
||||
<deposit-new
|
||||
depositMinCent="{{depositMinCent}}"
|
||||
depositMaxCent='{{depositMaxCent}}'
|
||||
price="{{depPrice}}"
|
||||
channel='{{depositChannel}}'
|
||||
meta='{{depositMeta}}'
|
||||
loss="{{depositLoss}}"
|
||||
onSetPrice="{{setDepPriceMp}}"
|
||||
onSetChannel="{{setDepositChannelMp}}"
|
||||
onSetMeta="{{setDepositMetaMp}}"
|
||||
onSetPrice="{{setDepPriceMp}}"
|
||||
/>
|
||||
<view style="margin-top: 12rpx">
|
||||
<l-button
|
||||
|
|
@ -52,7 +64,7 @@
|
|||
size="long"
|
||||
disabled="{{!depPrice || !depositChannel || depositing}}"
|
||||
loading="{{depositing}}"
|
||||
bind:lintap="createDepositPay"
|
||||
bind:lintap="newDeposit"
|
||||
>
|
||||
{{depositing ? t('depositing') : t('common::confirm')}}
|
||||
</l-button>
|
||||
|
|
@ -67,7 +79,7 @@
|
|||
show-title="{{false}}"
|
||||
content="{{t('uf.content')}}"
|
||||
confirm-text="{{t('uf.go')}}"
|
||||
bind:linconfirm="onUnfinishedPayClickedMp"
|
||||
bind:linconfirm="onUnfinishedDepositClick"
|
||||
bind:lintap="onUfModalClose"
|
||||
/>
|
||||
</block>
|
||||
|
|
|
|||
|
|
@ -12,9 +12,10 @@
|
|||
"content": "您有未支付的充值,请先完成支付",
|
||||
"go": "前往支付"
|
||||
},
|
||||
"history": "账单:",
|
||||
"history": "账单",
|
||||
"error": {
|
||||
"maxOverflow": "充值金额不得高于%{value}元",
|
||||
"minOverflow": "充值金额不得低于%{value}元"
|
||||
}
|
||||
},
|
||||
"yuan": "(元)"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,32 @@
|
|||
import React from 'react';
|
||||
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
|
||||
import { EntityDict } from '../../../oak-app-domain';
|
||||
import { PayChannel } from '../../../types/Pay';
|
||||
export default function Render(props: WebComponentProps<EntityDict, 'account', false, {
|
||||
account: RowWithActions<EntityDict, 'account'>;
|
||||
depositMaxCent: number;
|
||||
depositMinCent: number;
|
||||
unfinishedPayId?: string;
|
||||
onDepositPayId: (payId: string) => void;
|
||||
unfinishedDepositId?: string;
|
||||
onWithdraw: () => void;
|
||||
depositOpen: boolean;
|
||||
ufOpen: boolean;
|
||||
depPrice: number | null;
|
||||
depositChannel: string | undefined;
|
||||
depositChannel: PayChannel | undefined;
|
||||
depositLoss: [number, string, any];
|
||||
depositMeta: any;
|
||||
depositing: boolean;
|
||||
newDepositPath: string;
|
||||
textColor: string;
|
||||
priceColor: string;
|
||||
bgColor: string;
|
||||
}, {
|
||||
createDepositPay: () => Promise<void>;
|
||||
newDeposit: () => Promise<void>;
|
||||
setDepositOpen: (v: boolean) => void;
|
||||
setUfOpen: (v: boolean) => void;
|
||||
setDepPrice: (v: number | null) => void;
|
||||
setDepositChannel: (v: string | undefined) => void;
|
||||
setDepositMeta: (v: any) => void;
|
||||
setDepositChannel: (v: PayChannel | undefined) => void;
|
||||
setDepositing: (v: boolean) => void;
|
||||
onDepositClick: () => void;
|
||||
onDepositModalClose: () => void;
|
||||
onUfModalClose: () => void;
|
||||
onUnfinishedDepositClick: () => void;
|
||||
}>): React.JSX.Element | null;
|
||||
|
|
|
|||
|
|
@ -1,62 +1,69 @@
|
|||
import React from 'react';
|
||||
import { Button, Popup, Dialog } from 'antd-mobile';
|
||||
import { Button, Popup } from 'antd-mobile';
|
||||
import Styles from './web.mobile.module.less';
|
||||
import classNames from 'classnames';
|
||||
import { CentToString } from 'oak-domain/lib/utils/money';
|
||||
import AccountDeposit from '../deposit';
|
||||
import NewDeposit from '../../deposit/new';
|
||||
export default function Render(props) {
|
||||
const { account, depositMaxCent, unfinishedPayId, onDepositPayId, depositOpen, depositMinCent, ufOpen, depPrice, depositChannel, depositMeta, depositing, onWithdraw, } = props.data;
|
||||
const { t, createDepositPay, setMessage, setDepositOpen, setUfOpen, setDepPrice, setDepositChannel, setDepositMeta, setDepositing, onDepositClick, onDepositModalClose, onUfModalClose } = props.methods;
|
||||
const { account, depositMaxCent, newDepositPath, depositOpen, depositMinCent, ufOpen, depPrice, depositChannel, depositLoss, depositing, onWithdraw, textColor, priceColor, bgColor, } = props.data;
|
||||
const { t, newDeposit, setMessage, setDepositOpen, setUfOpen, setDepPrice, setDepositChannel, onUnfinishedDepositClick, setDepositing, onDepositClick, onDepositModalClose, } = props.methods;
|
||||
if (account) {
|
||||
const { total, avail, '#oakLegalActions': legalActions, accountOper$account: opers } = account;
|
||||
return (<div className={Styles.container}>
|
||||
<div className={Styles.info}>
|
||||
<div className={classNames(Styles.grid, Styles.fortify)}>
|
||||
<span className={Styles.label}>{t('avail')}:</span>
|
||||
<span className={Styles.value}>{t('common::pay.symbol')} {CentToString(avail, 2)}</span>
|
||||
<div className={Styles.info} style={{ backgroundColor: bgColor, color: textColor }}>
|
||||
<div className={classNames(Styles.top, Styles.label)}>{t('history')}</div>
|
||||
<div className={Styles.middle}>
|
||||
<div className={classNames(Styles.box, Styles.fortify)}>
|
||||
<div className={Styles.label}>{t('avail')}{t('yuan')}</div>
|
||||
<div className={Styles.value} style={{ color: priceColor }}>{t('common::pay.symbol')} {CentToString(avail, 2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={Styles.grid}>
|
||||
<span className={Styles.label}>{t('total')}:</span>
|
||||
<span className={Styles.value}>{t('common::pay.symbol')} {CentToString(total, 2)}</span>
|
||||
<div className={Styles.bottom}>
|
||||
<div className={Styles.box}>
|
||||
<div className={Styles.label}>{t('total')}{t('yuan')}</div>
|
||||
<div className={Styles.value} style={{ color: priceColor }}>{t('common::pay.symbol')} {CentToString(total, 2)}</div>
|
||||
</div>
|
||||
<div className={Styles.box}>
|
||||
<div className={Styles.label}>{t('loan')}{t('yuan')}</div>
|
||||
<div className={Styles.value} style={{ color: priceColor }}>{t('common::pay.symbol')} {CentToString(total - avail, 2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}/>
|
||||
<div style={{ height: '30vh' }}/>
|
||||
<div className={Styles.btn}>
|
||||
{legalActions?.includes('deposit') && <div className={Styles.item}>
|
||||
<Button block color="primary" disabled={ufOpen || depositOpen} onClick={() => {
|
||||
if (unfinishedPayId) {
|
||||
Dialog.alert({
|
||||
title: t('uf.title'),
|
||||
content: t('uf.content'),
|
||||
confirmText: t('uf.go'),
|
||||
onConfirm: () => onDepositPayId(unfinishedPayId)
|
||||
});
|
||||
}
|
||||
else {
|
||||
onDepositClick();
|
||||
}
|
||||
<Button block color="primary" disabled={ufOpen || !!depositOpen} onClick={() => {
|
||||
onDepositClick();
|
||||
}} style={{
|
||||
'--background-color': textColor,
|
||||
'--border-color': textColor,
|
||||
'--border-radius': '6px',
|
||||
fontWeight: 'bold',
|
||||
}}>
|
||||
{t('account:action.deposit')}
|
||||
</Button>
|
||||
</div>}
|
||||
{legalActions?.includes('withdraw') && <div className={Styles.item}>
|
||||
<Button block onClick={() => onWithdraw()}>
|
||||
<Button block onClick={() => onWithdraw()} style={{
|
||||
'--border-radius': '6px',
|
||||
fontWeight: 'bold',
|
||||
}}>
|
||||
{t('account:action.withdraw')}
|
||||
</Button>
|
||||
</div>}
|
||||
</div>
|
||||
<Popup visible={depositOpen} onMaskClick={() => onDepositModalClose()} onClose={() => onDepositModalClose()}>
|
||||
<Popup visible={!!depositOpen} onMaskClick={() => onDepositModalClose()} onClose={() => onDepositModalClose()} destroyOnClose>
|
||||
<div style={{ padding: 12, marginBottom: 6 }}>
|
||||
<AccountDeposit depositMinCent={depositMinCent} depositMaxCent={depositMaxCent} channel={depositChannel} meta={depositMeta} onSetPrice={(price) => setDepPrice(price)} onSetChannel={(channel) => setDepositChannel(channel)} onSetMeta={(meta) => setDepositMeta(meta)}/>
|
||||
<Button block loading={depositing} color="primary" disabled={!depPrice || !depositChannel || depositing} onClick={() => createDepositPay()}>
|
||||
<NewDeposit depositMinCent={depositMinCent} depositMaxCent={depositMaxCent} oakPath={newDepositPath} price={depPrice} channel={depositChannel} loss={depositLoss} onSetChannel={setDepositChannel} onSetPrice={setDepPrice}/>
|
||||
<Button block loading={depositing} color="primary" disabled={!depPrice || !depositChannel || depositing} onClick={() => newDeposit()}>
|
||||
{depositing ? t('depositing') : t('common::confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
</Popup>
|
||||
<Popup visible={ufOpen} onMaskClick={() => onUfModalClose()} onClose={() => onUfModalClose()} destroyOnClose={true}>
|
||||
<Popup visible={ufOpen} onMaskClick={() => setUfOpen(false)} onClose={() => setUfOpen(false)} destroyOnClose={true}>
|
||||
<div className={Styles.uf}>
|
||||
<div className={Styles.tips}>{t('uf.content')}</div>
|
||||
<Button color="warning" onClick={() => onDepositPayId(unfinishedPayId)}>
|
||||
<Button color="warning" onClick={() => onUnfinishedDepositClick()}>
|
||||
{t('uf.go')}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,28 +5,47 @@
|
|||
align-items: center;
|
||||
|
||||
.info {
|
||||
height: 300px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 24px;
|
||||
height: 240px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.grid {
|
||||
padding: 10px;
|
||||
.top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.middle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
color: var(--oak-color-primary);
|
||||
margin-right: 4px;
|
||||
}
|
||||
.bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 14px;
|
||||
color: var(--oak-color-primary);
|
||||
}
|
||||
.box {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
row-gap: 8px;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 14px;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
.fortify {
|
||||
|
|
@ -36,17 +55,20 @@
|
|||
|
||||
.value {
|
||||
font-size: x-large;
|
||||
font-weight: bolder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 40%;
|
||||
display: flex;
|
||||
align-self: stretch;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.item {
|
||||
flex: 1,
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +1,29 @@
|
|||
import React from 'react';
|
||||
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
|
||||
import { EntityDict } from '../../../oak-app-domain';
|
||||
import { PayChannel } from '../../../types/Pay';
|
||||
export default function Render(props: WebComponentProps<EntityDict, 'account', false, {
|
||||
account: RowWithActions<EntityDict, 'account'>;
|
||||
depositMaxCent: number;
|
||||
depositMinCent: number;
|
||||
unfinishedPayId?: string;
|
||||
onDepositPayId: (payId: string) => void;
|
||||
unfinishedDepositId?: string;
|
||||
onWithdraw: () => void;
|
||||
depositOpen: boolean;
|
||||
ufOpen: boolean;
|
||||
depPrice: number | null;
|
||||
depositChannel: string | undefined;
|
||||
depositChannel: PayChannel | undefined;
|
||||
depositLoss: [number, string, any];
|
||||
depositMeta: any;
|
||||
depositing: boolean;
|
||||
newDepositPath: string;
|
||||
}, {
|
||||
createDepositPay: () => Promise<string>;
|
||||
newDeposit: () => Promise<string>;
|
||||
setDepositOpen: (v: boolean) => void;
|
||||
setUfOpen: (v: boolean) => void;
|
||||
setDepPrice: (v: number | null) => void;
|
||||
setDepositChannel: (v: string | undefined) => void;
|
||||
setDepositMeta: (v: any) => void;
|
||||
setDepositChannel: (v: PayChannel | undefined) => void;
|
||||
setDepositing: (v: boolean) => void;
|
||||
onDepositClick: () => void;
|
||||
onDepositModalClose: () => void;
|
||||
onUnfinishedDepositClick: () => void;
|
||||
}>): React.JSX.Element | null;
|
||||
|
|
|
|||
|
|
@ -3,17 +3,17 @@ import { Button, Modal, Card, Flex, Divider, Typography } from 'antd';
|
|||
import Styles from './web.pc.module.less';
|
||||
import { CentToString } from 'oak-domain/lib/utils/money';
|
||||
import classNames from 'classnames';
|
||||
import AccountDeposit from '../deposit';
|
||||
import NewDeposit from '../../deposit/new';
|
||||
import AccountOperList from '../../accountOper/pure/List.pc';
|
||||
import { AccountBookOutlined, ScheduleOutlined } from '@ant-design/icons';
|
||||
export default function Render(props) {
|
||||
const { account, depositMaxCent, unfinishedPayId, onDepositPayId, depositOpen, depositMinCent, ufOpen, depPrice, depositChannel, depositMeta, depositing, onWithdraw, } = props.data;
|
||||
const { t, createDepositPay, setMessage, setDepositOpen, setUfOpen, setDepPrice, setDepositChannel, setDepositMeta, setDepositing, onDepositClick, onDepositModalClose, } = props.methods;
|
||||
const { account, depositMaxCent, newDepositPath, depositOpen, depositMinCent, ufOpen, depPrice, depositChannel, depositLoss, depositing, onWithdraw, } = props.data;
|
||||
const { t, newDeposit, setMessage, setDepositOpen, setUfOpen, setDepPrice, setDepositChannel, onUnfinishedDepositClick, setDepositing, onDepositClick, onDepositModalClose, } = props.methods;
|
||||
if (account) {
|
||||
const { total, avail, '#oakLegalActions': legalActions, accountOper$account: opers } = account;
|
||||
return (<>
|
||||
<Card className={Styles.card} title={<span><AccountBookOutlined /> {t('title')}</span>} extra={<Flex gap="middle">
|
||||
{legalActions?.includes('deposit') && <Button type="primary" disabled={ufOpen || depositOpen} onClick={() => onDepositClick()}>
|
||||
{legalActions?.includes('deposit') && <Button type="primary" disabled={ufOpen || !!depositOpen} onClick={() => onDepositClick()}>
|
||||
{t('account:action.deposit')}
|
||||
</Button>}
|
||||
{legalActions?.includes('withdraw') && <Button onClick={() => onWithdraw()}>
|
||||
|
|
@ -38,17 +38,17 @@ export default function Render(props) {
|
|||
{!!opers?.length && (<>
|
||||
<Divider />
|
||||
<div className={Styles.oper}>
|
||||
<span className={Styles.title}><ScheduleOutlined /> {t('history')}</span>
|
||||
<span className={Styles.title}><ScheduleOutlined /> {t('history')}:</span>
|
||||
<AccountOperList accountOpers={opers} t={t}/>
|
||||
</div>
|
||||
</>)}
|
||||
</div>
|
||||
</Card>
|
||||
<Modal title={t('deposit.title')} open={depositOpen} onCancel={() => onDepositModalClose()} destroyOnClose={true} footer={<Button loading={depositing} type="primary" disabled={!depPrice || !depositChannel || depositing} onClick={() => createDepositPay()}>
|
||||
<Modal title={t('deposit.title')} open={depositOpen} onCancel={() => onDepositModalClose()} destroyOnClose={true} footer={<Button loading={depositing} type="primary" disabled={!depPrice || !depositChannel || depositing} onClick={() => newDeposit()}>
|
||||
{depositing ? t('depositing') : t('common::confirm')}
|
||||
</Button>}>
|
||||
<div style={{ padding: 12 }}>
|
||||
<AccountDeposit depositMinCent={depositMinCent} depositMaxCent={depositMaxCent} channel={depositChannel} meta={depositMeta} onSetPrice={(price) => setDepPrice(price)} onSetChannel={(channel) => setDepositChannel(channel)} onSetMeta={(meta) => setDepositMeta(meta)}/>
|
||||
<NewDeposit depositMinCent={depositMinCent} depositMaxCent={depositMaxCent} oakPath={newDepositPath} price={depPrice} channel={depositChannel} loss={depositLoss} onSetChannel={setDepositChannel} onSetPrice={setDepPrice}/>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal title={t('uf.title')} open={ufOpen} onCancel={() => {
|
||||
|
|
@ -56,7 +56,7 @@ export default function Render(props) {
|
|||
}} destroyOnClose={true} footer={null}>
|
||||
<div className={Styles.uf}>
|
||||
<Typography.Paragraph>{t('uf.content')}</Typography.Paragraph>
|
||||
<Button type="link" onClick={() => onDepositPayId(unfinishedPayId)}>
|
||||
<Button type="link" onClick={() => onUnfinishedDepositClick()}>
|
||||
{t('uf.go')}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
import { PayChannel } from "../../../types/Pay";
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, keyof import("../../../oak-app-domain").EntityDict, boolean, {
|
||||
accountId: string;
|
||||
depositMinCent: number;
|
||||
depositMaxCent: number;
|
||||
price: number | null;
|
||||
channel: PayChannel | null;
|
||||
onSetPrice: (price: null | number) => void;
|
||||
onSetChannel: (channel: PayChannel) => void;
|
||||
loss: [number, string, any];
|
||||
}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import { ToCent, ToYuan } from "oak-domain/lib/utils/money";
|
||||
export default OakComponent({
|
||||
properties: {
|
||||
accountId: '',
|
||||
depositMinCent: 0,
|
||||
depositMaxCent: 1000000,
|
||||
price: 0,
|
||||
channel: {},
|
||||
onSetPrice: (price) => undefined,
|
||||
onSetChannel: (channel) => undefined,
|
||||
loss: [0, '', {}],
|
||||
},
|
||||
data: {
|
||||
onChooseChannelMp(channel) { this.onChooseChannel(channel); },
|
||||
},
|
||||
formData({ data, features }) {
|
||||
const payChannels = features.pay.getPayChannels('deposit');
|
||||
const { price, loss, depositMaxCent, depositMinCent } = this.props;
|
||||
return {
|
||||
depositMax: ToYuan(depositMaxCent),
|
||||
depositMin: ToYuan(depositMinCent),
|
||||
deposit: data,
|
||||
payChannels,
|
||||
priceStr: price ? ToYuan(price) : undefined,
|
||||
lossStr: loss?.[0] ? ToYuan(loss?.[0]) : undefined,
|
||||
lossReason: loss?.[1] ? this.t(loss[1], loss[2]) : '',
|
||||
};
|
||||
},
|
||||
lifetimes: {
|
||||
ready() {
|
||||
const { depositMinCent } = this.props;
|
||||
if (depositMinCent) {
|
||||
this.onPriceChange(ToYuan(depositMinCent), true);
|
||||
}
|
||||
}
|
||||
},
|
||||
listeners: {
|
||||
price() {
|
||||
this.reRender();
|
||||
},
|
||||
loss() {
|
||||
this.reRender();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onPriceChange(price2) {
|
||||
const price = price2 === null ? null : ToCent(price2);
|
||||
this.props.onSetPrice(price);
|
||||
},
|
||||
onDepPriceChangeMp(event) {
|
||||
const { value } = event.detail;
|
||||
if (value === null) {
|
||||
this.onPriceChange(value);
|
||||
}
|
||||
else {
|
||||
const price2 = parseInt(value);
|
||||
if (!isNaN(price2)) {
|
||||
this.onPriceChange(price2);
|
||||
}
|
||||
}
|
||||
},
|
||||
onChooseChannel(channel) {
|
||||
this.props.onSetChannel(channel);
|
||||
},
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"usingComponents": {
|
||||
"l-form": "@oak-frontend-base/miniprogram_npm/lin-ui/form/index",
|
||||
"l-form-item": "@oak-frontend-base/miniprogram_npm/lin-ui/form-item/index",
|
||||
"l-input": "@oak-frontend-base/miniprogram_npm/lin-ui/input/index",
|
||||
"l-button": "@oak-frontend-base/miniprogram_npm/lin-ui/button/index",
|
||||
"channel-picker": "../../pay/channelPicker2/index"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<block wx:if="{{payChannels && payChannels.length > 0}}">
|
||||
<l-form>
|
||||
<l-form-item label="{{t('deposit:attr.price')}}" label-placement="column">
|
||||
<l-input
|
||||
label="{{t('common::pay.symbol')}}"
|
||||
label-width="{{70}}"
|
||||
focus="{{true}}"
|
||||
show-row="{{false}}"
|
||||
value="{{priceStr || ''}}"
|
||||
type="number"
|
||||
placeholder="一次最大充值{{depositMax}}元"
|
||||
bind:lininput="onDepPriceChangeMp"
|
||||
/>
|
||||
</l-form-item>
|
||||
<block wx:if="{{price > 0}}">
|
||||
<l-form-item label="{{t('pay:attr.entity')}}" label-placement="column">
|
||||
<channel-picker
|
||||
payChannels="{{payChannels}}"
|
||||
payChannel="{{channel}}"
|
||||
onPick="{{onChooseChannelMp}}"
|
||||
/>
|
||||
</l-form-item>
|
||||
</block>
|
||||
<block wx:if="{{lossStr}}">
|
||||
<l-form-item label="{{t('deposit:attr.loss')}}" label-placement="column">
|
||||
<l-input
|
||||
hide-label
|
||||
show-row="{{false}}"
|
||||
value="{{lossStr || ''}}"
|
||||
disabled="{{true}}"
|
||||
/>
|
||||
</l-form-item>
|
||||
</block>
|
||||
</l-form>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<view class="container">{{t('error.noChannel')}}</view>
|
||||
</block>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"placeholder": "一次最大充值%{max}元",
|
||||
"label": {
|
||||
"depPrice": "充值金额",
|
||||
"channel": "充值渠道"
|
||||
},
|
||||
"tips": {
|
||||
"depositLoss": "线上充值将按照充值渠道的扣款比例扣除相应费用(一般是0.6%),敬请知晓",
|
||||
"depositLossRatio": "线上充值将按照%{ratio}%的比例扣除相应费用,敬请知晓"
|
||||
},
|
||||
"error": {
|
||||
"noChannel": "未配置交费渠道,请联系系统管理员"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import React from 'react';
|
||||
import { WebComponentProps } from 'oak-frontend-base';
|
||||
import { EntityDict } from '../../../oak-app-domain';
|
||||
import { PayChannel, PayChannels } from '../../../types/Pay';
|
||||
export default function render(props: WebComponentProps<EntityDict, 'deposit', false, {
|
||||
depositMax: number;
|
||||
price: number;
|
||||
payChannels?: PayChannels;
|
||||
channel?: PayChannel;
|
||||
depositMin: number;
|
||||
lossStr: string | undefined;
|
||||
lossReason: string;
|
||||
priceStr?: string;
|
||||
}, {
|
||||
onChooseChannel: (channel: PayChannel) => void;
|
||||
onPriceChange: (price: number | null) => void;
|
||||
}>): React.JSX.Element;
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import React from 'react';
|
||||
import { Form, Input } from 'antd-mobile';
|
||||
import ChannelPicker from '../../pay/channelPicker2';
|
||||
import Styles from './web.pc.module.less';
|
||||
export default function render(props) {
|
||||
const { depositMax, payChannels, price, channel, lossStr, lossReason, priceStr } = props.data;
|
||||
const { onChooseChannel, onPriceChange, t } = props.methods;
|
||||
if (payChannels) {
|
||||
if (payChannels.length > 0) {
|
||||
return (<Form layout="vertical">
|
||||
<Form.Item label={<span>{t("deposit:attr.price")}</span>} extra={t('common::pay.symbol')}>
|
||||
<Input autoFocus type='number' placeholder={t('placeholder', { max: depositMax })} value={priceStr} onChange={(value) => {
|
||||
if (value === '' || value === null) {
|
||||
onPriceChange(null);
|
||||
return;
|
||||
}
|
||||
const v = parseInt(value);
|
||||
if (!isNaN(v)) {
|
||||
onPriceChange(v);
|
||||
}
|
||||
}}/>
|
||||
</Form.Item>
|
||||
{price > 0 && <Form.Item label={<span>{t('pay:attr.entity')}</span>}>
|
||||
<ChannelPicker payChannels={payChannels} payChannel={channel} onPick={onChooseChannel}/>
|
||||
</Form.Item>}
|
||||
{!!lossStr && <Form.Item label={<span>{t("deposit:attr.loss")}:</span>} help={lossReason} extra={t('common::pay.symbol')}>
|
||||
<Input disabled value={lossStr}/>
|
||||
</Form.Item>}
|
||||
</Form>);
|
||||
}
|
||||
}
|
||||
return (<div className={Styles.container}>
|
||||
{t('error.noChannel')}
|
||||
</div>);
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
import { WebComponentProps } from 'oak-frontend-base';
|
||||
import { EntityDict } from '../../../oak-app-domain';
|
||||
import { PayChannel, PayChannels } from '../../../types/Pay';
|
||||
export default function render(props: WebComponentProps<EntityDict, 'deposit', false, {
|
||||
depositMax: number;
|
||||
price: number;
|
||||
payChannels?: PayChannels;
|
||||
channel?: PayChannel;
|
||||
depositMin: number;
|
||||
lossStr: string | undefined;
|
||||
lossReason: string;
|
||||
}, {
|
||||
onChooseChannel: (channel: PayChannel) => void;
|
||||
onPriceChange: (price: number | null) => void;
|
||||
}>): React.JSX.Element;
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import React from 'react';
|
||||
import { Form, InputNumber, Input } from 'antd';
|
||||
import ChannelPicker from '../../pay/channelPicker2';
|
||||
import { ToYuan } from 'oak-domain/lib/utils/money';
|
||||
import Styles from './web.pc.module.less';
|
||||
export default function render(props) {
|
||||
const { depositMax, payChannels, price, channel, lossStr, lossReason } = props.data;
|
||||
const { onChooseChannel, onPriceChange, t } = props.methods;
|
||||
if (payChannels) {
|
||||
if (payChannels.length > 0) {
|
||||
return (<Form labelCol={{ span: 4 }} wrapperCol={{ span: 14 }} layout="horizontal" style={{ width: '100%' }} colon={false}>
|
||||
<Form.Item label={<span>{t("deposit:attr.price")}:</span>}>
|
||||
<InputNumber autoFocus placeholder={t('placeholder', { max: depositMax })} value={typeof price == 'number' ? ToYuan(price) : null} addonAfter={t('common::pay.symbol')} onChange={(value) => {
|
||||
onPriceChange(value);
|
||||
}}/>
|
||||
</Form.Item>
|
||||
{price > 0 ? <Form.Item label={<span>
|
||||
{t('pay:attr.entity')}:
|
||||
</span>}>
|
||||
<ChannelPicker payChannels={payChannels} payChannel={channel} onPick={onChooseChannel}/>
|
||||
</Form.Item> : <div style={{ height: 56 }}/>}
|
||||
{!!lossStr && <Form.Item label={<span>{t("deposit:attr.loss")}:</span>} help={lossReason}>
|
||||
<Input disabled value={lossStr} addonAfter={t('common::pay.symbol')}/>
|
||||
</Form.Item>}
|
||||
</Form>);
|
||||
}
|
||||
}
|
||||
return (<div className={Styles.container}>
|
||||
{t('error.noChannel')}
|
||||
</div>);
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
.tips {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "offlineAccount", true, {
|
||||
systemId: string;
|
||||
}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
export default OakComponent({
|
||||
entity: 'offlineAccount',
|
||||
isList: true,
|
||||
projection: {
|
||||
id: 1,
|
||||
type: 1,
|
||||
channel: 1,
|
||||
name: 1,
|
||||
qrCode: 1,
|
||||
allowDeposit: 1,
|
||||
allowPay: 1,
|
||||
systemId: 1,
|
||||
price: 1,
|
||||
enabled: 1,
|
||||
},
|
||||
properties: {
|
||||
systemId: '',
|
||||
},
|
||||
formData({ data, legalActions }) {
|
||||
return {
|
||||
accounts: data.map((ele) => {
|
||||
const { type } = ele;
|
||||
const color = this.features.style.getColor('offlineAccount', 'type', type);
|
||||
return {
|
||||
color,
|
||||
...ele,
|
||||
};
|
||||
}),
|
||||
canCreate: legalActions?.includes('create'),
|
||||
};
|
||||
},
|
||||
actions: ['create', 'update', 'remove'],
|
||||
});
|
||||
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"tips": "线下账户是需要system的相关工作人员手动同步收款和打款结果的账户",
|
||||
"notnull": "属性\"%{value}\"不能为空",
|
||||
"noData": "没有数据",
|
||||
"confirmDelete": "确定删除",
|
||||
"areYouSure": "确认删除本数据吗?"
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react';
|
||||
import { EntityDict } from "../../../oak-app-domain";
|
||||
import { RowWithActions, WebComponentProps } from "oak-frontend-base";
|
||||
export default function render(props: WebComponentProps<EntityDict, 'offlineAccount', true, {
|
||||
accounts?: (RowWithActions<EntityDict, 'offlineAccount'> & {
|
||||
color: string;
|
||||
})[];
|
||||
systemId: string;
|
||||
canCreate?: boolean;
|
||||
}>): React.JSX.Element;
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Button, Modal, Alert, Descriptions, QRCode } from 'antd';
|
||||
import { PlusCircleOutlined } from '@ant-design/icons';
|
||||
import Styles from './web.pc.module.less';
|
||||
import Upsert from '../upsert';
|
||||
import { OakAttrNotNullException, OakException } from 'oak-domain/lib/types';
|
||||
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
|
||||
function OfflineAccount(props) {
|
||||
const { account, t, onUpdate, onRemove, onQrCodeClick } = props;
|
||||
const { type, channel, name, qrCode, allowDeposit, allowPay, color, enabled, price } = account;
|
||||
const legalActions = account['#oakLegalActions'];
|
||||
return (<Descriptions style={{ width: 340, height: 435 }} title={t(`offlineAccount:v.type.${type}`)} extra={<>
|
||||
{legalActions.includes('update') && <Button style={{ marginRight: 4 }} size="small" onClick={() => onUpdate()}>
|
||||
{t('common::action.update')}
|
||||
</Button>}
|
||||
{legalActions.includes('remove') && <Button danger size="small" onClick={() => onRemove()}>
|
||||
{t('common::action.remove')}
|
||||
</Button>}
|
||||
</>} column={1} bordered size="small" labelStyle={{ width: 98 }}>
|
||||
<Descriptions.Item label={t('offlineAccount:attr.type')}>{t(`offlineAccount:v.type.${type}`)}</Descriptions.Item>
|
||||
{channel && <Descriptions.Item label={t(`offlineAccount::label.channel.${type}`)}>{channel}</Descriptions.Item>}
|
||||
{name && <Descriptions.Item label={t(`offlineAccount::label.name.${type}`)}>{name}</Descriptions.Item>}
|
||||
{qrCode && <Descriptions.Item label={t(`offlineAccount::label.qrCode.${type}`)}>
|
||||
{type === 'bank' ? qrCode : <span className={Styles.qrCode} onClick={onQrCodeClick}><QRCode value={qrCode} size={name ? 80 : 128} color={color}/></span>}
|
||||
</Descriptions.Item>}
|
||||
<Descriptions.Item label={t('offlineAccount:attr.price')}>{price}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('offlineAccount:attr.allowDeposit')}>{t(`common::${allowDeposit}`)}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('offlineAccount:attr.allowPay')}>{t(`common::${allowPay}`)}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('offlineAccount:attr.enabled')}>{t(`common::${enabled}`)}</Descriptions.Item>
|
||||
</Descriptions>);
|
||||
}
|
||||
export default function render(props) {
|
||||
const { accounts, oakFullpath, oakExecutable, systemId, canCreate } = props.data;
|
||||
const { t, addItem, execute, clean } = props.methods;
|
||||
const [upsertId, setUpsertId] = useState('');
|
||||
const getNotNullMessage = (attr) => {
|
||||
if (['channel', 'name', 'qrCode'].includes(attr)) {
|
||||
const upsertRow = accounts?.find(ele => ele.id === upsertId);
|
||||
return t('notnull', { value: t(`offlineAccount::label.${attr}.${upsertRow.type}`) });
|
||||
}
|
||||
return t('notnull', { value: t(`offlineAccount:attr.${attr}`) });
|
||||
};
|
||||
const errMsg = oakExecutable instanceof OakException && (oakExecutable instanceof OakAttrNotNullException ? getNotNullMessage(oakExecutable.getAttributes()[0]) : t(oakExecutable.message));
|
||||
const U = (<Modal destroyOnClose width={680} title={`${t('offlineAccount:name')}${t('common::action.add')}`} open={!!upsertId} onCancel={() => {
|
||||
clean();
|
||||
setUpsertId('');
|
||||
}} closeIcon={null} onOk={async () => {
|
||||
await execute();
|
||||
setUpsertId('');
|
||||
}} okButtonProps={{
|
||||
disabled: oakExecutable !== true,
|
||||
}} okText={t('common::confirm')} cancelText={(t('common::action.cancel'))}>
|
||||
<div style={{ padding: 10 }}>
|
||||
{errMsg && <Alert type="error" message={errMsg} style={{ marginBottom: 20 }}/>}
|
||||
<Upsert oakPath={`${oakFullpath}.${upsertId}`} systemId={systemId}/>
|
||||
</div>
|
||||
</Modal>);
|
||||
const [showQrCodeId, setShowQrCodeId] = useState('');
|
||||
const showQrCodeRow = showQrCodeId ? accounts.find(ele => ele.id === showQrCodeId) : undefined;
|
||||
if (accounts && accounts.length > 0) {
|
||||
return (<div className={Styles.container}>
|
||||
<Alert type='info' message={t('tips')}/>
|
||||
{U}
|
||||
<div className={Styles.list}>
|
||||
{accounts.filter(ele => ele.$$createAt$$ > 1).map((ele, idx) => <div className={Styles.item} key={idx}>
|
||||
<OfflineAccount account={ele} t={t} onRemove={() => {
|
||||
Modal.confirm({
|
||||
title: t('confirmDelete'),
|
||||
content: t('areYouSure'),
|
||||
onOk: async () => execute(undefined, undefined, undefined, [
|
||||
{
|
||||
entity: 'offlineAccount',
|
||||
operation: {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'remove',
|
||||
data: {},
|
||||
filter: {
|
||||
id: ele.id,
|
||||
},
|
||||
}
|
||||
}
|
||||
]),
|
||||
});
|
||||
}} onUpdate={() => setUpsertId(ele.id)} onQrCodeClick={() => setShowQrCodeId(ele.id)}/>
|
||||
</div>)}
|
||||
</div>
|
||||
<div className={Styles.btnBar}>
|
||||
{canCreate && <Button type="primary" onClick={() => {
|
||||
const id = addItem({ systemId });
|
||||
setUpsertId(id);
|
||||
}}>
|
||||
{t('common::action.add')}
|
||||
</Button>}
|
||||
</div>
|
||||
{showQrCodeRow && <Modal open={!!showQrCodeId} closeIcon={null} footer={null} onCancel={() => setShowQrCodeId('')}>
|
||||
<QRCode value={showQrCodeRow.qrCode} size={480} color={showQrCodeRow.color}/>
|
||||
</Modal>}
|
||||
</div>);
|
||||
}
|
||||
return (<div className={Styles.container2}>
|
||||
<Alert type='info' message={t('tips')}/>
|
||||
{U}
|
||||
<div className={Styles.body}>
|
||||
{canCreate ? <PlusCircleOutlined className={Styles.add} shape="circle" style={{ fontSize: 50 }} onClick={() => {
|
||||
const id = addItem({ systemId });
|
||||
setUpsertId(id);
|
||||
}}/> : t('noData')}
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
.container {
|
||||
min-height: 80vh;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.list {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
|
||||
.item {
|
||||
border: 0.1px solid silver;
|
||||
border-radius: 8px;
|
||||
margin: 18px;
|
||||
padding: 18px;
|
||||
|
||||
.qrCode:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btnBar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.container2 {
|
||||
min-height: 80vh;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
.body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.add:hover {
|
||||
color: var(--oak-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
/// <reference types="wechat-miniprogram" />
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../oak-app-domain").EntityDict, "offlineAccount", false, WechatMiniprogram.Component.DataOption>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
export default OakComponent({
|
||||
entity: 'offlineAccount',
|
||||
projection: {
|
||||
id: 1,
|
||||
type: 1,
|
||||
channel: 1,
|
||||
name: 1,
|
||||
desc: 1,
|
||||
systemId: 1,
|
||||
},
|
||||
isList: false,
|
||||
formData({ data }) {
|
||||
return {
|
||||
offlineAccount: data,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"tips": "线下交易账户是需要人工同步收款和打款行为的账户"
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
/// <reference types="wechat-miniprogram" />
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "offlineAccount", false, WechatMiniprogram.Component.DataOption>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
export default OakComponent({
|
||||
entity: 'offlineAccount',
|
||||
projection: {
|
||||
id: 1,
|
||||
type: 1,
|
||||
channel: 1,
|
||||
name: 1,
|
||||
qrCode: 1,
|
||||
allowDeposit: 1,
|
||||
allowPay: 1,
|
||||
systemId: 1,
|
||||
enabled: 1,
|
||||
depositLossRatio: 1,
|
||||
taxlossRatio: 1,
|
||||
},
|
||||
isList: false,
|
||||
formData({ data }) {
|
||||
return {
|
||||
offlineAccount: data,
|
||||
};
|
||||
},
|
||||
lifetimes: {
|
||||
ready() {
|
||||
if (this.isCreation()) {
|
||||
this.update({
|
||||
allowDeposit: false,
|
||||
allowPay: false,
|
||||
price: 0,
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"placeholder": {
|
||||
"channel": {
|
||||
"bank": "银行/支行",
|
||||
"others": "请输入途径名称"
|
||||
},
|
||||
"name": {
|
||||
"bank": "输入户主姓名",
|
||||
"alipay": "输入支付宝帐号",
|
||||
"wechat": "输入微信号",
|
||||
"shouqianba": "输入收钱吧商户号",
|
||||
"others": "输入该收款途径的唯一账号"
|
||||
},
|
||||
"qrCode": {
|
||||
"bank": "请输入银行账号",
|
||||
"alipay": "请将二维码解析后的字符串填入",
|
||||
"wechat": "请将二维码解析后的字符串填入",
|
||||
"shouqianba": "请将二维码解析后的字符串填入",
|
||||
"others": "请将二维码解析后的字符串填入"
|
||||
},
|
||||
"taxlossRatio": "渠道收款时收取的手续费百分比,0.6代表千分之六",
|
||||
"depositLossRatio": "充值时收取的手续费百分比,0.6代表千分之六"
|
||||
},
|
||||
"help": {
|
||||
"allowDeposit": "是否允许用户在系统中主动向此账号发起充值",
|
||||
"allowPay": "是否允许用户在系统中主动向此账号发起支付"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { EntityDict } from "../../../oak-app-domain";
|
||||
import { RowWithActions, WebComponentProps } from "oak-frontend-base";
|
||||
import React from 'react';
|
||||
export default function render(props: WebComponentProps<EntityDict, 'offlineAccount', false, {
|
||||
offlineAccount: RowWithActions<EntityDict, 'offlineAccount'>;
|
||||
}>): React.JSX.Element | undefined;
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import React from 'react';
|
||||
import { Form, Switch, InputNumber, Input, Select } from 'antd';
|
||||
export default function render(props) {
|
||||
const { offlineAccount } = props.data;
|
||||
const { t, update } = props.methods;
|
||||
if (offlineAccount) {
|
||||
return (<Form labelCol={{ span: 6 }} wrapperCol={{ span: 16 }} layout="horizontal" style={{ minWidth: 600 }}>
|
||||
<Form.Item label={t('offlineAccount:attr.type')} required>
|
||||
<Select value={offlineAccount.type} options={['bank', 'alipay', 'wechat', 'shouqianba', 'others'].map(ele => ({
|
||||
label: t(`offlineAccount:v.type.${ele}`),
|
||||
value: ele,
|
||||
}))} onSelect={(value) => update({ type: value })}/>
|
||||
</Form.Item>
|
||||
{['bank', 'others'].includes(offlineAccount.type) && <Form.Item label={t(`offlineAccount::label.channel.${offlineAccount.type}`)} required>
|
||||
<Input value={offlineAccount.channel || ''} onChange={({ currentTarget }) => {
|
||||
const { value } = currentTarget;
|
||||
update({
|
||||
channel: value,
|
||||
});
|
||||
}} placeholder={t(`placeholder.channel.${offlineAccount.type}`)}/>
|
||||
</Form.Item>}
|
||||
{!!offlineAccount.type && <Form.Item label={t(`offlineAccount::label.name.${offlineAccount.type}`)} required={['bank'].includes(offlineAccount.type)}>
|
||||
<Input value={offlineAccount.name || ''} onChange={({ currentTarget }) => {
|
||||
const { value } = currentTarget;
|
||||
update({
|
||||
name: value,
|
||||
});
|
||||
}} placeholder={t(`placeholder.name.${offlineAccount.type}`)}/>
|
||||
</Form.Item>}
|
||||
{!!offlineAccount.type && <Form.Item label={t(`offlineAccount::label.qrCode.${offlineAccount.type}`)} required={offlineAccount.type === 'bank'}>
|
||||
{offlineAccount.type === 'bank' && <Input value={offlineAccount.qrCode || ''} onChange={({ currentTarget }) => {
|
||||
const { value } = currentTarget;
|
||||
update({
|
||||
qrCode: value,
|
||||
});
|
||||
}} placeholder={t(`placeholder.qrCode.${offlineAccount.type}`)}/>}
|
||||
{offlineAccount.type !== 'bank' && <Input.TextArea rows={8} value={offlineAccount.qrCode || ''} onChange={({ currentTarget }) => {
|
||||
const { value } = currentTarget;
|
||||
update({
|
||||
qrCode: value,
|
||||
});
|
||||
}} placeholder={t(`placeholder.qrCode.${offlineAccount.type}`)}/>}
|
||||
</Form.Item>}
|
||||
<Form.Item label={t('offlineAccount:attr.taxlossRatio')} help={t('placeholder.taxlossRatio')}>
|
||||
<InputNumber value={offlineAccount.taxlossRatio} max={5} min={0.01} addonAfter={"%"} step={0.01} precision={2} onChange={(value) => {
|
||||
const taxlossRatio = value;
|
||||
update({ taxlossRatio });
|
||||
}}/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t('offlineAccount:attr.depositLossRatio')} help={t('placeholder.depositLossRatio')}>
|
||||
<InputNumber value={offlineAccount.depositLossRatio} max={5} min={0.01} addonAfter={"%"} step={0.01} precision={2} onChange={(value) => {
|
||||
const depositLossRatio = value;
|
||||
update({ depositLossRatio });
|
||||
}}/>
|
||||
</Form.Item>
|
||||
{!!offlineAccount.type && <Form.Item label={t('offlineAccount:attr.allowDeposit')} required help={t('help.allowDeposit')}>
|
||||
<Switch value={offlineAccount.allowDeposit} onChange={(allowDeposit) => {
|
||||
update({ allowDeposit });
|
||||
}}/>
|
||||
</Form.Item>}
|
||||
{!!offlineAccount.type && <Form.Item label={t('offlineAccount:attr.allowPay')} required help={t('help.allowPay')}>
|
||||
<Switch value={offlineAccount.allowPay} onChange={(allowPay) => {
|
||||
update({ allowPay });
|
||||
}}/>
|
||||
</Form.Item>}
|
||||
<Form.Item label={t('offlineAccount:attr.enabled')} required>
|
||||
<Switch value={offlineAccount.enabled} onChange={(enabled) => {
|
||||
update({ enabled });
|
||||
}}/>
|
||||
</Form.Item>
|
||||
</Form>);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { EntityDict } from "../../oak-app-domain";
|
||||
import { RowWithActions, WebComponentProps } from "oak-frontend-base";
|
||||
import React from 'react';
|
||||
export default function render(props: WebComponentProps<EntityDict, 'offlineAccount', false, {
|
||||
offlineAccount: RowWithActions<EntityDict, 'offlineAccount'>;
|
||||
}>): React.JSX.Element | undefined;
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import React from 'react';
|
||||
import { Alert, Form, Switch, InputNumber } from 'antd';
|
||||
export default function render(props) {
|
||||
const { offlineAccount } = props.data;
|
||||
const { t } = props.methods;
|
||||
if (offlineAccount) {
|
||||
return (<Form labelCol={{ span: 6 }} wrapperCol={{ span: 12 }} layout="horizontal" style={{ minWidth: 600 }}>
|
||||
<Alert type='info' message={t('tips')}/>
|
||||
<Form.Item label={t('label.depositLoss')} help={t('placeholder.depositLoss')}>
|
||||
<Switch value={config.depositLoss} onChange={(value) => {
|
||||
config.depositLoss = value;
|
||||
if (value === false) {
|
||||
config.depositLossRatio = undefined;
|
||||
}
|
||||
update(config);
|
||||
}}/>
|
||||
</Form.Item>
|
||||
{config.depositLoss &&
|
||||
<Form.Item label={t('label.depositLossRatio')} help={t('placeholder.depositLossRatio')}>
|
||||
<InputNumber value={config.depositLossRatio} max={5} min={0.01} addonAfter={"%"} step={0.01} precision={2} onChange={(value) => {
|
||||
config.depositLossRatio = value;
|
||||
update(config);
|
||||
}}/>
|
||||
</Form.Item>}
|
||||
</Form>);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
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 PayChannelPicker from '../../pay/channelPicker';
|
||||
import { InputNumber } from 'antd';
|
||||
import { Checkbox, Divider, ErrorBlock } from 'antd-mobile';
|
||||
import Info from './info';
|
||||
|
|
@ -13,7 +13,13 @@ function RenderPayChannel(props) {
|
|||
{t('choose', { price: ToYuan(price) })}
|
||||
</div>
|
||||
<Divider />
|
||||
<PayChannelPicker payConfig={payConfig} channel={channel} meta={meta} onPick={onPick} onSetMeta={onSetMeta}/>
|
||||
{/* <PayChannelPicker
|
||||
payConfig={payConfig}
|
||||
channel={channel}
|
||||
meta={meta}
|
||||
onPick={onPick}
|
||||
onSetMeta={onSetMeta}
|
||||
/> */}
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { ToYuan, ToCent } from 'oak-domain/lib/utils/money';
|
||||
import Styles from './web.pc.module.less';
|
||||
import PayChannelPicker from '../../pay/channelPicker';
|
||||
// import PayChannelPicker from '../../pay/channelPicker';
|
||||
import { Divider, Checkbox, InputNumber, Flex, Result } from 'antd';
|
||||
import Info from './info';
|
||||
function RenderPayChannel(props) {
|
||||
|
|
@ -12,7 +12,13 @@ function RenderPayChannel(props) {
|
|||
{t('choose', { price: ToYuan(price) })}
|
||||
</div>
|
||||
<Divider />
|
||||
<PayChannelPicker payConfig={payConfig} channel={channel} meta={meta} onPick={onPick} onSetMeta={onSetMeta}/>
|
||||
{/* <PayChannelPicker
|
||||
payConfig={payConfig}
|
||||
channel={channel}
|
||||
meta={meta}
|
||||
onPick={onPick}
|
||||
onSetMeta={onSetMeta}
|
||||
/> */}
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
import { PayChannels, PayChannel } from "../../../types/Pay";
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, keyof import("../../../oak-app-domain").EntityDict, boolean, {
|
||||
payChannels: PayChannels;
|
||||
payChannel: PayChannel | undefined;
|
||||
onPick: (channel: PayChannel) => void;
|
||||
}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
export default OakComponent({
|
||||
properties: {
|
||||
payChannels: {},
|
||||
payChannel: undefined,
|
||||
onPick: (channel) => undefined,
|
||||
},
|
||||
formData() {
|
||||
const { payChannels, payChannel } = this.props;
|
||||
const channels = payChannels.map(ele => ({
|
||||
...ele,
|
||||
label: ele.label ? this.t(ele.label) : this.t(`payChannel::${ele.entity}`),
|
||||
value: ele.entityId,
|
||||
}));
|
||||
return {
|
||||
channels,
|
||||
channel: channels.find(ele => ele.entityId === payChannel?.entityId)?.entityId,
|
||||
};
|
||||
},
|
||||
listeners: {
|
||||
payChannel() {
|
||||
this.reRender();
|
||||
},
|
||||
payChannels() {
|
||||
this.reRender();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onPickChannel(id) {
|
||||
const { onPick, payChannels } = this.props;
|
||||
const channel = payChannels.find(ele => ele.entityId === id);
|
||||
onPick(channel);
|
||||
},
|
||||
onPickChannelMp(touch) {
|
||||
const { detail } = touch;
|
||||
const { currentKey } = detail;
|
||||
this.onPickChannel(currentKey);
|
||||
},
|
||||
}
|
||||
});
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
{
|
||||
"usingComponents": {
|
||||
"l-input": "@oak-frontend-base/miniprogram_npm/lin-ui/input/index",
|
||||
"l-radio": "@oak-frontend-base/miniprogram_npm/lin-ui/radio/index",
|
||||
"l-radio-group": "@oak-frontend-base/miniprogram_npm/lin-ui/radio-group/index",
|
||||
"l-tag": "@oak-frontend-base/miniprogram_npm/lin-ui/tag/index"
|
||||
"l-radio": "@oak-frontend-base/miniprogram_npm/lin-ui/radio/index"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<block wx:if="{{channels.length > 0}}">
|
||||
<l-radio-group
|
||||
wx:for="{{channels}}"
|
||||
wx:key="value"
|
||||
l-class="form-item"
|
||||
current="{{channel}}"
|
||||
bind:linchange="onPickChannelMp"
|
||||
>
|
||||
<l-radio wx:key="{{item.value}}" key="{{item.value}}">
|
||||
<view class="radio">
|
||||
<view style="white-space:nowrap">{{item.label}}</view>
|
||||
</view>
|
||||
</l-radio>
|
||||
</l-radio-group>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<l-input
|
||||
disabled
|
||||
value="{{t('noChannel')}}"
|
||||
/>
|
||||
</block>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"offline": "线下充值"
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
/// <reference types="react" />
|
||||
import { PayChannelOption } from "../../../types/Pay";
|
||||
export default function Render(props: {
|
||||
data: {
|
||||
channel?: string;
|
||||
channels: PayChannelOption[];
|
||||
};
|
||||
methods: {
|
||||
onPickChannel: (id: string) => void;
|
||||
};
|
||||
}): import("react").JSX.Element | null;
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { Selector } from 'antd-mobile';
|
||||
export default function Render(props) {
|
||||
const { channels, channel } = props.data;
|
||||
const { onPickChannel } = props.methods;
|
||||
if (channels) {
|
||||
return (<Selector options={channels} value={channel ? [channel] : []} onChange={(arr) => onPickChannel(arr[0])}/>);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
/// <reference types="react" />
|
||||
import { PayChannelOption } from "../../../types/Pay";
|
||||
export default function Render(props: {
|
||||
data: {
|
||||
channel?: string;
|
||||
channels: PayChannelOption[];
|
||||
};
|
||||
methods: {
|
||||
onPickChannel: (id: string) => void;
|
||||
};
|
||||
}): import("react").JSX.Element;
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { Select } from "antd";
|
||||
export default function Render(props) {
|
||||
const { channels, channel } = props.data;
|
||||
const { onPickChannel } = props.methods;
|
||||
return (<Select options={channels} value={channel || null} onSelect={(v) => onPickChannel(v)}/>);
|
||||
}
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
import { EntityDict } from "../../../oak-app-domain";
|
||||
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/types/Entity';
|
||||
import { StartPayRoutine, JudgeCanPay } from "../../../types/Pay";
|
||||
export declare function registerFrontendPayRoutine<ED extends EntityDict & BaseEntityDict>(entity: keyof ED, routine: StartPayRoutine, projection: ED['pay']['Selection']['data'], judgeCanPay: JudgeCanPay): void;
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<EntityDict, "pay", false, {
|
||||
onClose: () => void;
|
||||
disableAutoPay: boolean;
|
||||
|
|
|
|||
|
|
@ -1,99 +1,187 @@
|
|||
import { CentToString } from "oak-domain/lib/utils/money";
|
||||
import { PAY_CHANNEL_OFFLINE_NAME, PAY_CHANNEL_ACCOUNT_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, PAY_ORG_CHANNELS } from "../../../types/PayConfig";
|
||||
import { DATA_SUBSCRIBER_KEYS } from "../../../config/constants";
|
||||
import assert from "assert";
|
||||
import { isWeiXin } from 'oak-frontend-base/es/utils/utils';
|
||||
import { merge } from "oak-domain/lib/utils/lodash";
|
||||
import { canStartPay } from "../../../utils/wpProductFrontend";
|
||||
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
|
||||
const PayRoutineDict = {
|
||||
wpProduct: {
|
||||
projection: {
|
||||
wpProduct: {
|
||||
id: 1,
|
||||
type: 1,
|
||||
},
|
||||
},
|
||||
routine: async (pay, features) => {
|
||||
const { iState, wpProduct, meta } = pay;
|
||||
switch (wpProduct.type) {
|
||||
case 'mp': {
|
||||
if (process.env.OAK_PLATFORM === 'wechatMp') {
|
||||
const { prepayMeta } = meta;
|
||||
if (prepayMeta) {
|
||||
const result = await wx.requestPayment(prepayMeta);
|
||||
process.env.NODE_ENV === 'development' && console.log(result);
|
||||
}
|
||||
else {
|
||||
features.message.setMessage({
|
||||
type: 'error',
|
||||
content: features.locales.t('startPayError.illegaPayData'),
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
features.message.setMessage({
|
||||
type: 'error',
|
||||
content: features.locales.t('startPayError.falseEnv', { env: 'wechatMp' }),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 'jsapi': {
|
||||
if (process.env.OAK_PLATFORM === 'web' && isWeiXin) {
|
||||
const { prepayMeta } = meta;
|
||||
if (prepayMeta) {
|
||||
const { timeStamp, ...rest } = prepayMeta;
|
||||
// chooseWXPay文档都找不到了,网上查出来这里timestamp的s要小写,吐血了
|
||||
await features.wechatSdk.loadWxAPi('chooseWXPay', {
|
||||
timestamp: timeStamp,
|
||||
...rest,
|
||||
});
|
||||
}
|
||||
else {
|
||||
features.message.setMessage({
|
||||
type: 'error',
|
||||
content: features.locales.t('startPayError.illegaPayData'),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
default: {
|
||||
assert('尚未实现');
|
||||
}
|
||||
}
|
||||
},
|
||||
judgeCanPay: canStartPay,
|
||||
}
|
||||
};
|
||||
export function registerFrontendPayRoutine(entity, routine, projection, judgeCanPay) {
|
||||
PayRoutineDict[entity] = {
|
||||
routine,
|
||||
projection,
|
||||
judgeCanPay,
|
||||
};
|
||||
}
|
||||
export default OakComponent({
|
||||
entity: 'pay',
|
||||
isList: false,
|
||||
projection: {
|
||||
id: 1,
|
||||
applicationId: 1,
|
||||
price: 1,
|
||||
meta: 1,
|
||||
iState: 1,
|
||||
channel: 1,
|
||||
paid: 1,
|
||||
refunded: 1,
|
||||
timeoutAt: 1,
|
||||
forbidRefundAt: 1,
|
||||
externalId: 1,
|
||||
orderId: 1,
|
||||
accountId: 1,
|
||||
account: {
|
||||
projection: () => {
|
||||
const baseProjection = {
|
||||
id: 1,
|
||||
entityId: 1,
|
||||
applicationId: 1,
|
||||
price: 1,
|
||||
meta: 1,
|
||||
iState: 1,
|
||||
paid: 1,
|
||||
refunded: 1,
|
||||
timeoutAt: 1,
|
||||
forbidRefundAt: 1,
|
||||
externalId: 1,
|
||||
orderId: 1,
|
||||
depositId: 1,
|
||||
deposit: {
|
||||
id: 1,
|
||||
accountId: 1,
|
||||
price: 1,
|
||||
loss: 1,
|
||||
},
|
||||
order: {
|
||||
id: 1,
|
||||
creatorId: 1,
|
||||
},
|
||||
entity: 1,
|
||||
},
|
||||
order: {
|
||||
id: 1,
|
||||
entityId: 1,
|
||||
creatorId: 1,
|
||||
},
|
||||
creatorId: 1,
|
||||
phantom3: 1,
|
||||
};
|
||||
for (const k in PayRoutineDict) {
|
||||
merge(baseProjection, PayRoutineDict[k].projection);
|
||||
}
|
||||
return baseProjection;
|
||||
},
|
||||
properties: {
|
||||
onClose: () => undefined,
|
||||
disableAutoPay: false,
|
||||
},
|
||||
data: {
|
||||
PAY_CHANNEL_OFFLINE_NAME,
|
||||
PAY_CHANNEL_ACCOUNT_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,
|
||||
showCloseConfirmMp: false,
|
||||
showChannelSelectMp: false,
|
||||
},
|
||||
formData({ data }) {
|
||||
const application = this.features.application.getApplication();
|
||||
const iState = data?.iState;
|
||||
const iStateColor = iState && this.features.style.getColor('pay', 'iState', iState);
|
||||
const payConfig = this.features.pay.getPayConfigs();
|
||||
const { meta, channel } = data || {};
|
||||
const userId = this.features.token.getUserId();
|
||||
const startPayable = iState === 'paying' && meta && PAY_ORG_CHANNELS.includes(channel) && ([PAY_CHANNEL_WECHAT_JS_NAME, PAY_CHANNEL_WECHAT_NATIVE_NAME].includes(channel) && meta.payId === userId);
|
||||
const startPayable = iState === 'paying' && !['account', 'offlineAccount'].includes(data.entity) && (PayRoutineDict[data.entity] && PayRoutineDict[data.entity].judgeCanPay(data, this.features));
|
||||
const payChannels = this.features.pay.getPayChannels();
|
||||
const offlines = this.features.cache.get('offlineAccount', {
|
||||
data: {
|
||||
id: 1,
|
||||
type: 1,
|
||||
channel: 1,
|
||||
name: 1,
|
||||
qrCode: 1,
|
||||
},
|
||||
filter: {
|
||||
systemId: this.features.application.getApplication().systemId,
|
||||
}
|
||||
}).map(ele => {
|
||||
const color = this.features.style.getColor('offlineAccount', 'type', ele.type);
|
||||
return {
|
||||
color,
|
||||
...ele,
|
||||
};
|
||||
});
|
||||
const offline = offlines?.find(ele => ele.id === data.entityId);
|
||||
return {
|
||||
type: data?.orderId ? 'order' : 'account',
|
||||
type: data?.orderId ? 'order' : 'deposit',
|
||||
pay: data,
|
||||
application,
|
||||
iStateColor,
|
||||
payConfig,
|
||||
closable: !!(data?.["#oakLegalActions"]?.includes('close')),
|
||||
startPayable,
|
||||
metaUpdatable: !!(data?.["#oakLegalActions"]?.find(ele => typeof ele === 'object'
|
||||
&& ele.action === 'update'
|
||||
&& ele.attrs?.includes('meta'))),
|
||||
offline: payConfig && payConfig.find(ele => ele.channel === PAY_CHANNEL_OFFLINE_NAME),
|
||||
notSameApp: data && data.applicationId !== application.id && data.channel !== PAY_CHANNEL_OFFLINE_NAME,
|
||||
offline,
|
||||
offlines,
|
||||
notSameApp: data && data.applicationId !== application.id && data.entity !== 'offlineAccount',
|
||||
priceStr: data?.price && CentToString(data.price, 2),
|
||||
};
|
||||
},
|
||||
features: ['application'],
|
||||
features: [{
|
||||
feature: 'application',
|
||||
callback() {
|
||||
this.refreshOfflineAccounts();
|
||||
}
|
||||
}],
|
||||
actions: ['close', 'startPaying', {
|
||||
action: 'update',
|
||||
attrs: ['meta'],
|
||||
attrs: ['entityId'],
|
||||
}],
|
||||
methods: {
|
||||
onSetOfflineSerialMp(e) {
|
||||
const { pay } = this.state;
|
||||
const { value } = e.detail;
|
||||
this.update({
|
||||
meta: {
|
||||
...pay?.meta,
|
||||
serial: value,
|
||||
}
|
||||
});
|
||||
},
|
||||
onSelectOfflineOptionMp(e) {
|
||||
const { option } = e.currentTarget.dataset;
|
||||
const { pay } = this.state;
|
||||
this.update({
|
||||
meta: {
|
||||
...pay?.meta,
|
||||
option,
|
||||
}
|
||||
});
|
||||
refreshOfflineAccounts() {
|
||||
const { entity } = this.state.pay || {};
|
||||
if (entity === 'offlineAccount') {
|
||||
this.features.cache.refresh('offlineAccount', {
|
||||
data: {
|
||||
id: 1,
|
||||
channel: 1,
|
||||
name: 1,
|
||||
type: 1,
|
||||
qrCode: 1,
|
||||
},
|
||||
filter: {
|
||||
systemId: this.features.application.getApplication().systemId,
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
executeMp() {
|
||||
return this.execute();
|
||||
|
|
@ -120,68 +208,62 @@ export default OakComponent({
|
|||
},
|
||||
async startPay() {
|
||||
const { pay } = this.state;
|
||||
const { iState, channel, meta } = pay;
|
||||
switch (channel) {
|
||||
case PAY_CHANNEL_WECHAT_MP_NAME: {
|
||||
if (process.env.OAK_PLATFORM === 'wechatMp') {
|
||||
const { prepayMeta } = meta;
|
||||
if (prepayMeta) {
|
||||
const result = await wx.requestPayment(prepayMeta);
|
||||
process.env.NODE_ENV === 'development' && console.log(result);
|
||||
}
|
||||
else {
|
||||
this.setMessage({
|
||||
type: 'warning',
|
||||
content: this.t('startPayError.illegaPayData'),
|
||||
});
|
||||
await PayRoutineDict[pay.entity].routine(pay, this.features);
|
||||
},
|
||||
/* async onWxBridgeReady(payMetaData: any): Promise<void> {
|
||||
return new Promise(
|
||||
(resolve, reject) => {
|
||||
WeixinJSBridge.invoke('getBrandWCPayRequest', payMetaData,
|
||||
function (res: { err_msg: string }) {
|
||||
if (res.err_msg == "get_brand_wcpay_request:ok") {
|
||||
// 使用以上方式判断前端返回,微信团队郑重提示:
|
||||
//res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。
|
||||
resolve(undefined as void);
|
||||
}
|
||||
else {
|
||||
reject(res.err_msg);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
} */
|
||||
openChannelSelectMp() {
|
||||
this.setState({
|
||||
showChannelSelectMp: true,
|
||||
});
|
||||
},
|
||||
closeChannelSelectMp() {
|
||||
this.setState({
|
||||
showChannelSelectMp: false,
|
||||
});
|
||||
},
|
||||
async updateOfflineIdMp(touch) {
|
||||
const { detail } = touch;
|
||||
const { currentKey } = detail;
|
||||
const { oakId } = this.props;
|
||||
if (currentKey) {
|
||||
await this.execute(undefined, undefined, undefined, [
|
||||
{
|
||||
entity: 'pay',
|
||||
operation: {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'update',
|
||||
data: {
|
||||
entity: 'offlineAccount',
|
||||
entityId: currentKey,
|
||||
},
|
||||
filter: {
|
||||
id: oakId,
|
||||
},
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.setMessage({
|
||||
type: 'warning',
|
||||
content: this.t('startPayError.falseEnv', { env: 'wechatMp' }),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
case PAY_CHANNEL_WECHAT_JS_NAME: {
|
||||
if (process.env.OAK_PLATFORM === 'web' && isWeiXin) {
|
||||
const { prepayMeta } = meta;
|
||||
if (prepayMeta) {
|
||||
const { timeStamp, ...rest } = prepayMeta;
|
||||
// chooseWXPay文档都找不到了,网上查出来这里timestamp的s要小写,吐血了
|
||||
await this.features.wechatSdk.loadWxAPi('chooseWXPay', {
|
||||
timestamp: timeStamp,
|
||||
...rest,
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.setMessage({
|
||||
type: 'warning',
|
||||
content: this.t('startPayError.illegaPayData'),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
default: {
|
||||
assert('尚未实现');
|
||||
}
|
||||
]);
|
||||
this.setState({
|
||||
showChannelSelectMp: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
async onWxBridgeReady(payMetaData) {
|
||||
return new Promise((resolve, reject) => {
|
||||
WeixinJSBridge.invoke('getBrandWCPayRequest', payMetaData, function (res) {
|
||||
if (res.err_msg == "get_brand_wcpay_request:ok") {
|
||||
// 使用以上方式判断前端返回,微信团队郑重提示:
|
||||
//res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。
|
||||
resolve(undefined);
|
||||
}
|
||||
else {
|
||||
reject(res.err_msg);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
lifetimes: {
|
||||
ready() {
|
||||
|
|
@ -200,6 +282,11 @@ export default OakComponent({
|
|||
if (next.startPayable && !this.props.disableAutoPay) {
|
||||
this.startPay();
|
||||
}
|
||||
},
|
||||
pay(prev, next) {
|
||||
if (!prev.pay && next.pay) {
|
||||
this.refreshOfflineAccounts();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@
|
|||
"l-icon": "@oak-frontend-base/miniprogram_npm/lin-ui/icon/index",
|
||||
"l-textarea": "@oak-frontend-base/miniprogram_npm/lin-ui/textarea/index",
|
||||
"l-button": "@oak-frontend-base/miniprogram_npm/lin-ui/button/index",
|
||||
"l-dialog": "@oak-frontend-base/miniprogram_npm/lin-ui/dialog/index"
|
||||
"l-dialog": "@oak-frontend-base/miniprogram_npm/lin-ui/dialog/index",
|
||||
"l-countdown": "@oak-frontend-base/miniprogram_npm/lin-ui/countdown/index",
|
||||
"l-form": "@oak-frontend-base/miniprogram_npm/lin-ui/form/index",
|
||||
"l-form-item": "@oak-frontend-base/miniprogram_npm/lin-ui/form-item/index",
|
||||
"l-popup": "@oak-frontend-base/miniprogram_npm/lin-ui/popup/index"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
@import '../../../config/styles/mp/mixins.less';
|
||||
@import '../../../config/styles/mp/index.less';
|
||||
|
||||
.page {
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 24rpx;
|
||||
box-sizing: border-box;
|
||||
.safe-area-inset-bottom();
|
||||
|
||||
.info {
|
||||
background-color: @oak-bg-color-container;
|
||||
|
|
@ -14,20 +17,21 @@
|
|||
|
||||
.title-bar-wrapper {
|
||||
width: 100%;
|
||||
|
||||
.title-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 28rpx 0;
|
||||
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
font-size: larger;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.title-bar-wrapper::after {
|
||||
content: '';
|
||||
height: 1px;
|
||||
|
|
@ -39,100 +43,157 @@
|
|||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.notSameApp {
|
||||
margin: 40rpx 0;
|
||||
border-radius: 20rpx;
|
||||
padding: 22rpx;
|
||||
background-color: @oak-color-warning;
|
||||
color: @oak-color-primary;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
.meta {
|
||||
margin-top: 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.tips {
|
||||
margin-left: 16rpx;
|
||||
text-wrap: wrap;
|
||||
}
|
||||
.qrCodeTips {
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
.offline {
|
||||
.tips {
|
||||
margin: 40rpx 0;
|
||||
border-radius: 20rpx;
|
||||
padding: 22rpx;
|
||||
background-color: @oak-color-info
|
||||
}
|
||||
|
||||
.info {
|
||||
margin: 40rpx 0;
|
||||
border-radius: 20rpx;
|
||||
padding: 22rpx;
|
||||
|
||||
.list {
|
||||
.left {
|
||||
width: 200rpx;
|
||||
}
|
||||
|
||||
.right {
|
||||
margin-left: 24rpx;
|
||||
flex: 1;
|
||||
|
||||
.tips2 {
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.offline-option {
|
||||
margin-left: 12rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
|
||||
l-tag {
|
||||
margin-left: 6rpx;
|
||||
}
|
||||
|
||||
.selected {
|
||||
background:#333 !important;
|
||||
color:#fff !important;
|
||||
}
|
||||
}
|
||||
|
||||
.textarea {
|
||||
min-width: 400rpx;
|
||||
margin: 18rpx 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.counter {
|
||||
font-size: var(--oak-font-size-headline-medium);
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
.success {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 24rpx;
|
||||
margin-top: 90rpx;
|
||||
|
||||
text {
|
||||
margin-top: 40rpx;
|
||||
font-weight: bold;
|
||||
color: green;
|
||||
font-size: xx-large;
|
||||
}
|
||||
.qrCode:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.padding {
|
||||
flex: 1;
|
||||
.wariningAlert {
|
||||
background: #fff2f0;
|
||||
border: 2rpx solid #ffccc7;
|
||||
padding: 12rpx 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
word-wrap: break-word;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
.infoAlert {
|
||||
background: #e6f4ff;
|
||||
border: 2rpx solid #91caff;
|
||||
padding: 12rpx 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
word-wrap: break-word;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
.notSameApp {
|
||||
margin: 40rpx 0;
|
||||
border-radius: 20rpx;
|
||||
padding: 22rpx;
|
||||
background-color: @oak-color-warning;
|
||||
color: @oak-color-primary;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.tips {
|
||||
margin-left: 16rpx;
|
||||
text-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
.defaultBtn {
|
||||
color: #333;
|
||||
border: 2rpx solid #eee;
|
||||
padding: 12rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
}
|
||||
// .offline {
|
||||
// .tips {
|
||||
// margin: 40rpx 0;
|
||||
// border-radius: 20rpx;
|
||||
// padding: 22rpx;
|
||||
// background-color: @oak-color-info
|
||||
// }
|
||||
|
||||
// .info {
|
||||
// margin: 40rpx 0;
|
||||
// border-radius: 20rpx;
|
||||
// padding: 22rpx;
|
||||
|
||||
// .list {
|
||||
// .left {
|
||||
// width: 200rpx;
|
||||
// }
|
||||
|
||||
// .right {
|
||||
// margin-left: 24rpx;
|
||||
// flex: 1;
|
||||
|
||||
// .tips2 {
|
||||
// flex: 1;
|
||||
// word-break: break-all;
|
||||
// text-decoration: underline;
|
||||
// }
|
||||
|
||||
// .offline-option {
|
||||
// margin-left: 12rpx;
|
||||
// display: flex;
|
||||
// flex-direction: row;
|
||||
// flex-wrap: wrap;
|
||||
|
||||
// l-tag {
|
||||
// margin-left: 6rpx;
|
||||
// }
|
||||
|
||||
// .selected {
|
||||
// background: #333 !important;
|
||||
// color: #fff !important;
|
||||
// }
|
||||
// }
|
||||
|
||||
// .textarea {
|
||||
// min-width: 400rpx;
|
||||
// margin: 18rpx 0;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
.paid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 24rpx;
|
||||
margin-top: 90rpx;
|
||||
|
||||
text {
|
||||
margin-top: 40rpx;
|
||||
font-weight: bold;
|
||||
color: green;
|
||||
font-size: xx-large;
|
||||
}
|
||||
}
|
||||
|
||||
.my-panel-class {
|
||||
padding: 40rpx;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
background-color: #fff;
|
||||
overflow-y: auto;
|
||||
.safe-area-inset-bottom();
|
||||
}
|
||||
|
||||
.padding {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 16rpx;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
|
@ -1,152 +1,152 @@
|
|||
<view class="page">
|
||||
<view class="container">
|
||||
<view class="info">
|
||||
<view class="title-bar-wrapper">
|
||||
<view class="title-bar">
|
||||
<view class="title">{{t('title')}}</view>
|
||||
<l-tag color="{{iStateColor}}">{{t('pay:v.iState.' + pay.iState)}}</l-tag>
|
||||
<l-tag bg-color="{{iStateColor}}" type="reading">{{t('pay:v.iState.' + pay.iState)}}</l-tag>
|
||||
</view>
|
||||
</view>
|
||||
<view class="list">
|
||||
<l-list title="{{t('type.label')}}" icon="order" right-desc="{{t('type.' + type)}}" is-link="{{false}}" />
|
||||
<l-list title="{{t('type.label')}}" icon="warning" right-desc="{{t('type.' + type)}}" is-link="{{false}}" />
|
||||
<l-list title="{{t('pay:attr.price')}}" icon="cart" right-desc="{{priceStr}}" is-link="{{false}}" />
|
||||
<l-list title="{{t('pay:attr.channel')}}" icon="research" right-desc="{{t('payChannel::' + pay.channel)}}" is-link="{{false}}" />
|
||||
<l-list title="{{t('pay:attr.channel')}}" icon="research" right-desc="{{t('payChannel::' + pay.entity)}}" is-link="{{false}}" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="notSameApp" wx:if="{{metaUpdatable && notSameApp}}">
|
||||
<l-icon name="warning" />
|
||||
<view class="tips">
|
||||
{{t('notSameApp')}}
|
||||
</view>
|
||||
</view>
|
||||
<view class="offline" wx:elif="{{pay.channel === PAY_CHANNEL_OFFLINE_NAME}}">
|
||||
<view wx:if="{{pay.iState === 'paying'}}" class="tips">
|
||||
{{t('offline.tips')}}
|
||||
</view>
|
||||
<view class="info">
|
||||
<l-list is-link="{{false}}" l-class="list">
|
||||
<view slot="left-section" class="left">
|
||||
{{t('offline.label.tips')}}
|
||||
<view class="meta">
|
||||
<block wx:if="{{pay.entity !== 'offlineAccount' && notSameApp}}">
|
||||
<view class="wariningAlert">{{t('notSameApp')}}</view>
|
||||
</block>
|
||||
<block wx:elif="{{pay.entity === 'offlineAccount'}}">
|
||||
<view wx:if="{{pay.iState === 'paying'}}" class="infoAlert" style="margin-bottom:20rpx;">{{t('code.help')}}</view>
|
||||
<l-popup wx:if="{{showChannelSelectMp}}" show="{{showChannelSelectMp}}" content-align="bottom" bind:lintap="closeChannelSelectMp">
|
||||
<view class="my-panel-class">
|
||||
<l-radio-group
|
||||
wx:for="{{offlines}}"
|
||||
wx:key="value"
|
||||
current="{{offline.id}}"
|
||||
bind:linchange="updateOfflineIdMp"
|
||||
>
|
||||
<l-radio wx:key="{{item.id}}" key="{{item.id}}">
|
||||
<view class="radio">
|
||||
<view style="white-space:nowrap">{{t('offlineAccount:v.type.'+ item.type)}}</view>
|
||||
</view>
|
||||
</l-radio>
|
||||
</l-radio-group>
|
||||
</view>
|
||||
<view slot="right-section" class="right">
|
||||
<view class="tips2">
|
||||
{{offline.tips}}
|
||||
</view>
|
||||
</view>
|
||||
</l-list>
|
||||
<l-list is-link="{{false}}" l-class="list">
|
||||
<view slot="left-section" class="left">
|
||||
{{t('offline.label.option')}}
|
||||
</view>
|
||||
<view slot="right-section" class="right">
|
||||
<view wx:if={{!metaUpdatable}}>{{pay.meta.option}}</view>
|
||||
<view wx:else>
|
||||
<view
|
||||
class="offline-option"
|
||||
>
|
||||
<l-tag
|
||||
wx:for="{{offline.options}}"
|
||||
plain="{{true}}"
|
||||
select="{{item===pay.meta.option}}"
|
||||
l-select-class="selected"
|
||||
catch:lintap="onSelectOfflineOptionMp"
|
||||
data-option="{{item}}"
|
||||
>
|
||||
{{item}}
|
||||
</l-tag>
|
||||
</l-popup>
|
||||
<l-form style="margin-top:24rpx;width:100%;background-color:#fff;">
|
||||
<l-form-item label-width="240rpx" label="{{t('channel.prefix')}}">
|
||||
<block wx:if="{{pay.iState === 'paying' && offlines.length > 1}}">
|
||||
<view style="display: flex; align-items: center; justify-content: flex-start;">
|
||||
<view class="defaultBtn" bindtap="openChannelSelectMp">{{t('offlineAccount:v.type.' + offline.type)}}</view>
|
||||
</view>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<view>{{t('offlineAccount:v.type.' + offline.type)}}</view>
|
||||
</block>
|
||||
</l-form-item>
|
||||
<l-form-item label-width="240rpx" label="{{t('code.label')}}">
|
||||
<l-tag bg-color="#d9363e">{{pay.phantom3}}</l-tag>
|
||||
</l-form-item>
|
||||
<block wx:if="{{offline.type === 'bank'}}">
|
||||
<l-form-item label-width="240rpx" label="{{t('offlineAccount::label.channel.bank')}}">
|
||||
<view>{{offline.channel}}</view>
|
||||
</l-form-item>
|
||||
<l-form-item label-width="240rpx" label="{{t('offlineAccount::label.name.bank')}}">
|
||||
<view>{{offline.name}}</view>
|
||||
</l-form-item>
|
||||
<l-form-item label-width="240rpx" label="{{t('offlineAccount::label.qrCode.bank')}}">
|
||||
<view>{{offline.qrCode}}</view>
|
||||
</l-form-item>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<block wx:if="{{offline.type === 'others'}}">
|
||||
<l-form-item label-width="240rpx" label="{{t('offlineAccount::label.channel.others')}}">
|
||||
<view>{{offline.channel}}</view>
|
||||
</l-form-item>
|
||||
</block>
|
||||
<block wx:if="{{offline.name}}">
|
||||
<l-form-item label-width="240rpx" label="{{t('offlineAccount::label.name.' + offline.type)}}">
|
||||
<view>{{offline.name}}</view>
|
||||
</l-form-item>
|
||||
</block>
|
||||
<!-- <block wx:if="{{offline.qrCode}}">
|
||||
<l-form-item label-width="240rpx" label="{{t('offlineAccount::label.qrCode.' + offline.type)}}">
|
||||
<view class="qrCode"></view>
|
||||
</l-form-item>
|
||||
</block> -->
|
||||
</block>
|
||||
</l-form>
|
||||
</block>
|
||||
<block wx:elif="{{pay.entity === 'wpProduct'}}">
|
||||
<view wx:if="{{pay.iState === 'paid'}}" class="paid">
|
||||
<l-icon name="success" color="green" size="140" />
|
||||
<view class="text">{{t('success')}}</view>
|
||||
</view>
|
||||
<block wx:if="{{pay.wpProduct.type === 'native'}}">
|
||||
<view wx:if="{{pay.iState === 'paying'}}">
|
||||
<l-countdown time-type="second" time="{{duration}}" format="{%h}:{%m}:{%s}"/>
|
||||
<view>QRCode</view>
|
||||
<view class="qrCodeTips">
|
||||
<view wx:if="{{process.env.NODE_ENV === 'production'}}" class="infoAlert">{{t('wechat.native.tips')}}</view>
|
||||
<view wx:else class="warningAlert">{{t('wechat.native.tips2')}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</l-list>
|
||||
|
||||
<l-list is-link="{{false}}" l-class="list">
|
||||
<view slot="left-section" class="left">
|
||||
{{t("offline.label.serial")}}
|
||||
</view>
|
||||
<view slot="right-section" class="right">
|
||||
<l-textarea
|
||||
l-class="textarea"
|
||||
maxlength="{{60}}"
|
||||
placeholder="{{metaUpdatable ? t('offline.placeholder.serial') : t('offline.placeholder.none')}}"
|
||||
disabled="{{!metaUpdatable}}"
|
||||
value="{{ (meta && meta.serial) ? meta.serial : '' }}"
|
||||
bind:lininput="onSetOfflineSerialMp"
|
||||
/>
|
||||
</view>
|
||||
</l-list>
|
||||
</block>
|
||||
</block>
|
||||
</view>
|
||||
<view class="padding"></view>
|
||||
<block wx:if="{{startPayable}}">
|
||||
<l-button
|
||||
size="long"
|
||||
bind:lintap="startPay"
|
||||
bg-color="#04BE02"
|
||||
>
|
||||
{{t('pay')}}
|
||||
</l-button>
|
||||
</block>
|
||||
<block wx:elif="{{oakExecutable === true}}">
|
||||
<view class="btn">
|
||||
<l-button
|
||||
size="long"
|
||||
bind:lintap="executeMp"
|
||||
>
|
||||
{{t('common::action.update')}}
|
||||
</l-button>
|
||||
<l-button
|
||||
plain="true"
|
||||
size="long"
|
||||
bind:lintap="resetMp"
|
||||
>
|
||||
{{t('common::reset')}}
|
||||
</l-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mp" wx:elif="{{pay.channel === PAY_CHANNEL_WECHAT_MP_NAME}}">
|
||||
<view class="success" wx:if="{{pay.iState === 'paid'}}">
|
||||
<l-icon name="success" color="green" size="140" />
|
||||
<text>{{t('success')}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="else" wx:else>
|
||||
</view>
|
||||
<view class="padding" />
|
||||
<view class="btn-container">
|
||||
<block wx:if="{{startPayable}}">
|
||||
<view class="btn">
|
||||
<l-button
|
||||
type="default"
|
||||
size="long"
|
||||
bind:lintap="startPay"
|
||||
bg-color="#04BE02"
|
||||
>
|
||||
{{t('wechatPay')}}
|
||||
</l-button>
|
||||
</view>
|
||||
</block>
|
||||
<block wx:elif="{{oakExecutable === true}}">
|
||||
<view class="btn">
|
||||
<l-button
|
||||
type="default"
|
||||
size="long"
|
||||
bind:lintap="executeMp"
|
||||
>
|
||||
{{t('common::action.update')}}
|
||||
</l-button>
|
||||
</view>
|
||||
<view class="btn">
|
||||
<l-button
|
||||
type="warning"
|
||||
size="long"
|
||||
bind:lintap="resetMp"
|
||||
>
|
||||
{{t('common::reset')}}
|
||||
</l-button>
|
||||
</view>
|
||||
</block>
|
||||
<block wx:elif="{{closable}}">
|
||||
<view class="btn">
|
||||
<l-button
|
||||
type="default"
|
||||
size="long"
|
||||
bind:lintap="closeMp"
|
||||
>
|
||||
{{t('pay:action.close')}}
|
||||
</l-button>
|
||||
</view>
|
||||
<l-dialog
|
||||
show="{{showCloseConfirmMp}}"
|
||||
type="confirm"
|
||||
title="{{t('cc.title')}}"
|
||||
content="{{t('cc.content')}}"
|
||||
bind:linconfirm="confirmCloseMp"
|
||||
bind:lincancel="cancelCloseMp"
|
||||
bind:lintap="cancelCloseMp"
|
||||
/>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<view class="btn">
|
||||
<l-button
|
||||
type="default"
|
||||
size="long"
|
||||
bind:lintap="goBack"
|
||||
>
|
||||
{{t('common::back')}}
|
||||
</l-button>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
<block wx:elif="{{closable}}">
|
||||
<l-button
|
||||
type="error"
|
||||
size="long"
|
||||
bind:lintap="closeMp"
|
||||
>
|
||||
{{t('pay:action.close')}}
|
||||
</l-button>
|
||||
<l-dialog
|
||||
show="{{showCloseConfirmMp}}"
|
||||
type="confirm"
|
||||
title="{{t('cc.title')}}"
|
||||
content="{{t('cc.content')}}"
|
||||
bind:linconfirm="confirmCloseMp"
|
||||
bind:lincancel="cancelCloseMp"
|
||||
bind:lintap="cancelCloseMp"
|
||||
/>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<l-button
|
||||
type="default"
|
||||
size="long"
|
||||
bind:lintap="goBack"
|
||||
>
|
||||
{{t('common::back')}}
|
||||
</l-button>
|
||||
</block>
|
||||
</view>
|
||||
|
|
@ -1,17 +1,5 @@
|
|||
{
|
||||
"title": "支付详情",
|
||||
"offline": {
|
||||
"tips": "请按照支付说明完成线下转帐,并填写凭证号,等待后台人工确认",
|
||||
"label": {
|
||||
"tips": "支付说明",
|
||||
"option": "渠道",
|
||||
"serial": "凭证号"
|
||||
},
|
||||
"placeholder": {
|
||||
"serial": "凭证号是转账流水、备注等可以用于查询的依据",
|
||||
"none": "未填写"
|
||||
}
|
||||
},
|
||||
"notSameApp": "不是在本应用端创建的支付,请到创建此支付的应用上完成支付",
|
||||
"cc": {
|
||||
"title": "关闭支付",
|
||||
|
|
@ -23,16 +11,24 @@
|
|||
"tips2": "虚拟的支付二维码,不用扫~"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"label": "类型",
|
||||
"order": "订单支付",
|
||||
"account": "帐户充值"
|
||||
},
|
||||
"success": "支付成功",
|
||||
"pay": "支付",
|
||||
"wechatPay": "微信支付",
|
||||
"startPayError": {
|
||||
"illegalPrepayData": "没有有效的预支付数据,可能后台是运行在开发环境,请检查",
|
||||
"falseEnv": "该订单渠道【%{env}】不匹配当前环境"
|
||||
},
|
||||
"type": {
|
||||
"label": "支付类型",
|
||||
"order": "订单支付",
|
||||
"deposit": "账户充值"
|
||||
},
|
||||
"code": {
|
||||
"label": "转帐凭证号",
|
||||
"help": "请在转帐留言中附上转帐凭证号,便于人工核对"
|
||||
},
|
||||
"channel": {
|
||||
"change": "更换",
|
||||
"prefix": "渠道"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,28 @@
|
|||
import React from 'react';
|
||||
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
|
||||
import { EntityDict } from '../../../oak-app-domain';
|
||||
import { OfflinePayConfig, PayConfig } from '../../../types/PayConfig';
|
||||
type ColoredOffline = EntityDict['offlineAccount']['OpSchema'] & {
|
||||
color: string;
|
||||
};
|
||||
export declare function RenderOffline(props: {
|
||||
pay: RowWithActions<EntityDict, 'pay'>;
|
||||
t: (key: string) => string;
|
||||
offline: OfflinePayConfig;
|
||||
updateMeta: (meta: any) => void;
|
||||
metaUpdatable: boolean;
|
||||
offlines: ColoredOffline[];
|
||||
offline: ColoredOffline;
|
||||
updateOfflineId: (entityId: string) => void;
|
||||
}): React.JSX.Element;
|
||||
export default function Render(props: WebComponentProps<EntityDict, 'pay', false, {
|
||||
pay: RowWithActions<EntityDict, 'pay'>;
|
||||
pay?: RowWithActions<EntityDict, 'pay'>;
|
||||
iStateColor?: string;
|
||||
payConfig?: PayConfig;
|
||||
onClose: () => undefined;
|
||||
closable: boolean;
|
||||
startPayable: boolean;
|
||||
metaUpdatable: boolean;
|
||||
notSameApp?: boolean;
|
||||
type: string;
|
||||
offlines: ColoredOffline[];
|
||||
offline: ColoredOffline;
|
||||
}, {
|
||||
goBack: () => void;
|
||||
startPay: () => Promise<void>;
|
||||
}>): React.JSX.Element | null;
|
||||
export {};
|
||||
|
|
|
|||
|
|
@ -1,36 +1,77 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Card, Tag, List, Button, Modal, Form, Selector, TextArea } from 'antd-mobile';
|
||||
import { Card, Tag, List, Button, Modal, Form, Selector, Popup } from 'antd-mobile';
|
||||
import { QRCode, Alert } from 'antd';
|
||||
import Styles from './web.mobile.module.less';
|
||||
import * as dayJs from 'dayjs';
|
||||
import duration from 'dayjs/plugin/duration';
|
||||
dayJs.extend(duration);
|
||||
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, InformationCircleOutline, CheckCircleOutline } from 'antd-mobile-icons';
|
||||
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
|
||||
export function RenderOffline(props) {
|
||||
const { pay, t, offline, updateMeta, metaUpdatable } = props;
|
||||
const { meta, iState } = pay;
|
||||
const { pay, t, offline, offlines, updateOfflineId } = props;
|
||||
const { meta, iState, phantom3 } = pay;
|
||||
const { type, channel, name, qrCode, color } = offline || {};
|
||||
const [show, setShow] = 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>,
|
||||
<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>
|
||||
];
|
||||
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={() => Modal.show({
|
||||
content: <QRCode style={{ width: '70vw', height: '70vw' }} value={qrCode} color={color}/>,
|
||||
closeOnMaskClick: true,
|
||||
})}>
|
||||
<QRCode value={qrCode} size={110} color={color}/>
|
||||
</span>
|
||||
</Form.Item>);
|
||||
}
|
||||
}
|
||||
return (<>
|
||||
<Popup visible={show} onMaskClick={() => {
|
||||
setShow(false);
|
||||
}} onClose={() => {
|
||||
setShow(false);
|
||||
}} position='bottom' bodyStyle={{ padding: 18 }}>
|
||||
<Selector options={offlines.map(ele => ({
|
||||
label: t(`offlineAccount:v.type.${ele.type}`),
|
||||
value: ele.id,
|
||||
}))} value={offline ? [offline.id] : []} onChange={(arr) => {
|
||||
updateOfflineId(arr[0]);
|
||||
setShow(false);
|
||||
}}/>
|
||||
</Popup>
|
||||
<Form layout="horizontal" style={{ width: '100%', marginTop: 12 }}>
|
||||
{metaUpdatable && <Form.Item label={t("offline.label.tips")}>
|
||||
<span style={{ wordBreak: 'break-all', textDecoration: 'underline' }}>{offline.tips}</span>
|
||||
</Form.Item>}
|
||||
<Form.Item label={t("offline.label.option")}>
|
||||
{(metaUpdatable && !!(offline.options?.length)) ? <Selector value={meta?.option ? [meta?.option] : undefined} options={offline.options.map(ele => ({
|
||||
label: ele,
|
||||
value: ele,
|
||||
}))} onChange={(v) => updateMeta({
|
||||
...meta,
|
||||
option: v[0],
|
||||
})}/> : <span>{meta?.option}</span>}
|
||||
</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>
|
||||
{items2}
|
||||
</Form>
|
||||
</>);
|
||||
}
|
||||
|
|
@ -52,7 +93,7 @@ function Counter(props) {
|
|||
}
|
||||
function RenderWechatPay(props) {
|
||||
const { pay, t } = props;
|
||||
const { externalId, channel, timeoutAt, iState } = pay;
|
||||
const { externalId, wpProduct, timeoutAt, iState } = pay;
|
||||
if (iState === 'paid') {
|
||||
return (<div className={Styles.paid}>
|
||||
<CheckCircleOutline fontSize={72} fontWeight="bold" color="green"/>
|
||||
|
|
@ -61,12 +102,12 @@ function RenderWechatPay(props) {
|
|||
</div>
|
||||
</div>);
|
||||
}
|
||||
switch (channel) {
|
||||
case PAY_CHANNEL_WECHAT_NATIVE_NAME: {
|
||||
switch (wpProduct.type) {
|
||||
case 'native': {
|
||||
if (iState === 'paying') {
|
||||
return (<>
|
||||
<Counter deadline={timeoutAt}/>
|
||||
<QRCode value={externalId} size={280}/>
|
||||
<QRCode value={externalId} size={280} color="#04BE02"/>
|
||||
<div className={Styles.qrCodeTips}>
|
||||
{process.env.NODE_ENV === 'production' ?
|
||||
<Alert type="info" message={t('wechat.native.tips')}/> :
|
||||
|
|
@ -80,41 +121,31 @@ function RenderWechatPay(props) {
|
|||
return null;
|
||||
}
|
||||
function RenderPayMeta(props) {
|
||||
const { pay, t, payConfig, updateMeta, metaUpdatable, notSameApp } = props;
|
||||
const { iState, channel } = pay;
|
||||
if (metaUpdatable && notSameApp) {
|
||||
const { pay, notSameApp, t, offlines, offline, updateOfflineId } = props;
|
||||
const { iState, entity } = pay;
|
||||
if (entity !== 'offlineAccount' && notSameApp) {
|
||||
return <Alert type='warning' message={t('notSameApp')}/>;
|
||||
}
|
||||
switch (channel) {
|
||||
case PAY_CHANNEL_OFFLINE_NAME: {
|
||||
switch (entity) {
|
||||
case 'offlineAccount': {
|
||||
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
|
||||
})}
|
||||
{iState === 'paying' && <Alert type='info' message={t('code.help')} style={{ marginBottom: 10 }}/>}
|
||||
<RenderOffline t={t} pay={pay} offline={offline} offlines={offlines} updateOfflineId={updateOfflineId}/>
|
||||
</>);
|
||||
}
|
||||
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: {
|
||||
case 'wpProduct': {
|
||||
return <RenderWechatPay pay={pay} t={t}/>;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
export default function Render(props) {
|
||||
const { pay, iStateColor, payConfig, notSameApp, type, startPayable, oakExecutable, onClose, closable, metaUpdatable } = props.data;
|
||||
const { pay, iStateColor, notSameApp, type, startPayable, oakExecutable, onClose, closable, offline, offlines } = props.data;
|
||||
const { t, update, execute, clean, goBack, startPay } = props.methods;
|
||||
if (pay) {
|
||||
const { iState, channel, price } = pay;
|
||||
const { iState, price, entity } = pay;
|
||||
let BtnPart = startPayable ? (<Button block style={{ backgroundColor: '#04BE02' }} className={Styles.btnWechatPay} onClick={() => startPay()}>
|
||||
{t('wechatPay')}
|
||||
{t('pay')}
|
||||
</Button>) : oakExecutable === true ? (<>
|
||||
<div className={Styles.btnItem}>
|
||||
<Button block color='primary' onClick={() => execute()}>
|
||||
|
|
@ -150,14 +181,31 @@ export default function Render(props) {
|
|||
<List.Item prefix={<PayCircleOutline />} extra={CentToString(price, 2)}>
|
||||
{t('pay:attr.price')}
|
||||
</List.Item>
|
||||
<List.Item prefix={<GlobalOutline />} extra={t(`payChannel::${channel}`)}>
|
||||
<List.Item prefix={<GlobalOutline />} extra={t(`payChannel::${entity}`)}>
|
||||
{t('pay:attr.channel')}
|
||||
</List.Item>
|
||||
</List>
|
||||
</div>
|
||||
</Card>
|
||||
<div className={Styles.meta}>
|
||||
<RenderPayMeta pay={pay} t={t} payConfig={payConfig} updateMeta={(meta) => update({ meta })} metaUpdatable={metaUpdatable} notSameApp={notSameApp}/>
|
||||
<RenderPayMeta pay={pay} t={t} 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
]);
|
||||
}}/>
|
||||
</div>
|
||||
<div className={Styles.padding}/>
|
||||
<div className={Styles.btn}>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@
|
|||
font-size: var(--oak-font-size-headline-medium);
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
.qrCode:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.paid {
|
||||
|
|
|
|||
|
|
@ -1,25 +1,28 @@
|
|||
import React from 'react';
|
||||
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
|
||||
import { EntityDict } from '../../../oak-app-domain';
|
||||
import { OfflinePayConfig, PayConfig } from '../../../types/PayConfig';
|
||||
type ColoredOffline = EntityDict['offlineAccount']['OpSchema'] & {
|
||||
color: string;
|
||||
};
|
||||
export declare function RenderOffline(props: {
|
||||
pay: RowWithActions<EntityDict, 'pay'>;
|
||||
t: (key: string) => string;
|
||||
offline: OfflinePayConfig;
|
||||
updateMeta: (meta: any) => void;
|
||||
metaUpdatable: boolean;
|
||||
}): React.JSX.Element;
|
||||
offlines: ColoredOffline[];
|
||||
offline: ColoredOffline;
|
||||
updateOfflineId: (entityId: string) => void;
|
||||
}): React.JSX.Element | null;
|
||||
export default function Render(props: WebComponentProps<EntityDict, 'pay', false, {
|
||||
pay?: RowWithActions<EntityDict, 'pay'>;
|
||||
iStateColor?: string;
|
||||
payConfig?: PayConfig;
|
||||
onClose: () => undefined;
|
||||
closable: boolean;
|
||||
metaUpdatable: boolean;
|
||||
startPayable: boolean;
|
||||
notSameApp?: boolean;
|
||||
type: string;
|
||||
offlines: ColoredOffline[];
|
||||
offline: ColoredOffline;
|
||||
}, {
|
||||
goBack: () => void;
|
||||
startPay: () => Promise<void>;
|
||||
}>): React.JSX.Element | null;
|
||||
export {};
|
||||
|
|
|
|||
|
|
@ -1,37 +1,77 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Input, Tag, Card, QRCode, Form, Descriptions, Typography, Alert, Select, Button, Modal } from 'antd';
|
||||
import { Tag, Card, QRCode, Form, Descriptions, Typography, Alert, Button, Modal, Radio } from 'antd';
|
||||
import { CheckCircleOutlined } from '@ant-design/icons';
|
||||
import { CentToString } from 'oak-domain/lib/utils/money';
|
||||
import Styles from './web.pc.module.less';
|
||||
import * as dayJs from 'dayjs';
|
||||
import duration from 'dayjs/plugin/duration';
|
||||
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
|
||||
dayJs.extend(duration);
|
||||
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';
|
||||
export function RenderOffline(props) {
|
||||
const { pay, t, offline, updateMeta, metaUpdatable } = props;
|
||||
const { meta, iState } = pay;
|
||||
const { pay, t, offline, offlines, updateOfflineId } = props;
|
||||
const { iState, phantom3 } = 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>,
|
||||
<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>
|
||||
];
|
||||
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>);
|
||||
}
|
||||
}
|
||||
return (<>
|
||||
<Form labelCol={{ span: 4 }} wrapperCol={{ span: 14 }} layout="horizontal" style={{ width: '100%', marginTop: 12 }}>
|
||||
{metaUpdatable && <Form.Item label={t("offline.label.tips")}>
|
||||
<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 => ({
|
||||
label: ele,
|
||||
value: ele,
|
||||
}))} disabled={!metaUpdatable} onSelect={(option) => updateMeta({
|
||||
...meta,
|
||||
option,
|
||||
})}/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t("offline.label.serial")}>
|
||||
<Input value={meta?.serial} disabled={!metaUpdatable} placeholder={metaUpdatable ? t('offline.placeholder.serial') : t('offline.placeholder.none')} onChange={({ currentTarget }) => updateMeta({
|
||||
...meta,
|
||||
serial: currentTarget.value,
|
||||
})}/>
|
||||
</Form.Item>
|
||||
<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>
|
||||
</>);
|
||||
return null;
|
||||
}
|
||||
function Counter(props) {
|
||||
const { deadline } = props;
|
||||
|
|
@ -51,7 +91,7 @@ function Counter(props) {
|
|||
}
|
||||
function RenderWechatPay(props) {
|
||||
const { pay, t } = props;
|
||||
const { externalId, channel, timeoutAt, iState } = pay;
|
||||
const { externalId, wpProduct, timeoutAt, iState } = pay;
|
||||
if (iState === 'paid') {
|
||||
return (<div className={Styles.paid}>
|
||||
<CheckCircleOutlined style={{
|
||||
|
|
@ -64,18 +104,18 @@ function RenderWechatPay(props) {
|
|||
</div>
|
||||
</div>);
|
||||
}
|
||||
switch (channel) {
|
||||
case PAY_CHANNEL_WECHAT_NATIVE_NAME: {
|
||||
switch (wpProduct.type) {
|
||||
case 'native': {
|
||||
if (iState === 'paying') {
|
||||
return (<>
|
||||
return (<div className={Styles.paid}>
|
||||
<Counter deadline={timeoutAt}/>
|
||||
<QRCode value={externalId} size={280}/>
|
||||
<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;
|
||||
}
|
||||
|
|
@ -83,41 +123,32 @@ function RenderWechatPay(props) {
|
|||
return null;
|
||||
}
|
||||
function RenderPayMeta(props) {
|
||||
const { pay, notSameApp, t, payConfig, updateMeta, metaUpdatable } = props;
|
||||
const { iState, channel } = pay;
|
||||
if (metaUpdatable && notSameApp) {
|
||||
const { pay, notSameApp, t, offlines, offline, updateOfflineId } = props;
|
||||
const { iState, entity } = pay;
|
||||
if (entity !== 'offlineAccount' && notSameApp) {
|
||||
return <Alert type='warning' message={t('notSameApp')}/>;
|
||||
}
|
||||
switch (channel) {
|
||||
case PAY_CHANNEL_OFFLINE_NAME: {
|
||||
switch (entity) {
|
||||
case 'offlineAccount': {
|
||||
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
|
||||
})}
|
||||
{iState === 'paying' && <Alert type='info' message={t('code.help')} style={{ marginBottom: 10 }}/>}
|
||||
<RenderOffline t={t} pay={pay} offline={offline} offlines={offlines} updateOfflineId={updateOfflineId}/>
|
||||
</>);
|
||||
}
|
||||
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: {
|
||||
case 'wpProduct': {
|
||||
return <RenderWechatPay pay={pay} t={t}/>;
|
||||
}
|
||||
}
|
||||
// todo 要支持注入第三方支付的渲染组件
|
||||
return null;
|
||||
}
|
||||
export default function Render(props) {
|
||||
const { pay, iStateColor, payConfig, notSameApp, type, startPayable, oakExecutable, onClose, closable, metaUpdatable } = props.data;
|
||||
const { pay, iStateColor, notSameApp, type, startPayable, oakExecutable, onClose, closable, offline, offlines } = props.data;
|
||||
const { t, update, execute, clean, goBack, startPay } = props.methods;
|
||||
if (pay) {
|
||||
const { iState, channel, price } = pay;
|
||||
const { iState, price, entity } = pay;
|
||||
const BtnPart = startPayable ? (<Button style={{ backgroundColor: '#04BE02' }} className={Styles.btnWechatPay} onClick={() => startPay()}>
|
||||
{t('wechatPay')}
|
||||
{t('pay')}
|
||||
</Button>) : oakExecutable === true ? (<>
|
||||
<Button type="primary" onClick={() => execute()}>
|
||||
{t('common::action.update')}
|
||||
|
|
@ -132,7 +163,9 @@ export default function Render(props) {
|
|||
onOk: async () => {
|
||||
await execute('close');
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
okText: t('common::confirm'),
|
||||
cancelText: t('common::action.cancel'),
|
||||
});
|
||||
}}>
|
||||
{t('pay:action.close')}
|
||||
|
|
@ -155,13 +188,30 @@ export default function Render(props) {
|
|||
},
|
||||
{
|
||||
key: '2',
|
||||
label: t('pay:attr.channel'),
|
||||
children: <span className={Styles.value}>{t(`payChannel::${channel}`)}</span>,
|
||||
label: t('pay:attr.entity'),
|
||||
children: <span className={Styles.value}>{t(`payChannel::${entity}`)}</span>,
|
||||
}
|
||||
]}/>
|
||||
</div>
|
||||
<div className={Styles.oper}>
|
||||
<RenderPayMeta pay={pay} t={t} notSameApp={notSameApp} payConfig={payConfig} updateMeta={(meta) => update({ meta })} metaUpdatable={metaUpdatable}/>
|
||||
<RenderPayMeta pay={pay} t={t} 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
]);
|
||||
}}/>
|
||||
</div>
|
||||
<div className={Styles.btn}>
|
||||
{BtnPart}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@
|
|||
.value {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.paid {
|
||||
|
|
@ -31,15 +35,25 @@
|
|||
|
||||
.oper {
|
||||
margin-top: 40px;
|
||||
margin-bottom: 20px;
|
||||
width: 60%;
|
||||
max-width: 600px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
align-items: stretch;
|
||||
|
||||
.qrCodeTips {
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.qrCode:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
|
|
@ -47,7 +61,7 @@
|
|||
max-width: 600px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
|
||||
|
||||
.btnWechatPay {
|
||||
span {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { PAY_CHANNEL_OFFLINE_NAME } from "../../../types/PayConfig";
|
||||
export default OakComponent({
|
||||
entity: 'pay',
|
||||
isList: true,
|
||||
|
|
@ -7,17 +6,13 @@ export default OakComponent({
|
|||
applicationId: 1,
|
||||
price: 1,
|
||||
iState: 1,
|
||||
channel: 1,
|
||||
paid: 1,
|
||||
refunded: 1,
|
||||
orderId: 1,
|
||||
meta: 1,
|
||||
accountId: 1,
|
||||
account: {
|
||||
id: 1,
|
||||
entityId: 1,
|
||||
entity: 1,
|
||||
},
|
||||
depositId: 1,
|
||||
entityId: 1,
|
||||
entity: 1,
|
||||
order: {
|
||||
id: 1,
|
||||
creatorId: 1,
|
||||
|
|
@ -39,6 +34,15 @@ export default OakComponent({
|
|||
indexFrom: 0,
|
||||
count: 1,
|
||||
}
|
||||
},
|
||||
phantom3: 1,
|
||||
wpProduct: {
|
||||
id: 1,
|
||||
type: 1,
|
||||
},
|
||||
offlineAccount: {
|
||||
id: 1,
|
||||
type: 1,
|
||||
}
|
||||
},
|
||||
filters: [
|
||||
|
|
@ -65,10 +69,26 @@ export default OakComponent({
|
|||
}
|
||||
],
|
||||
formData({ data }) {
|
||||
const payConfig = this.features.pay.getPayConfigs();
|
||||
const offlineConfig = payConfig?.find(ele => ele.channel === PAY_CHANNEL_OFFLINE_NAME);
|
||||
const offlines = this.features.cache.get('offlineAccount', {
|
||||
data: {
|
||||
id: 1,
|
||||
type: 1,
|
||||
channel: 1,
|
||||
name: 1,
|
||||
qrCode: 1,
|
||||
},
|
||||
filter: {
|
||||
systemId: this.features.application.getApplication().systemId,
|
||||
}
|
||||
}).map(ele => {
|
||||
const color = this.features.style.getColor('offlineAccount', 'type', ele.type);
|
||||
return {
|
||||
color,
|
||||
...ele,
|
||||
};
|
||||
});
|
||||
return {
|
||||
offlineConfig,
|
||||
offlines,
|
||||
pays: data.map((ele) => {
|
||||
const { creator } = ele;
|
||||
const { nickname, name, mobile$user: mobiles } = creator;
|
||||
|
|
@ -82,5 +102,32 @@ export default OakComponent({
|
|||
};
|
||||
},
|
||||
getTotal: 100,
|
||||
actions: ['close', 'succeedPaying']
|
||||
actions: ['close', 'succeedPaying'],
|
||||
methods: {
|
||||
refreshOfflineAccounts() {
|
||||
this.features.cache.refresh('offlineAccount', {
|
||||
data: {
|
||||
id: 1,
|
||||
channel: 1,
|
||||
name: 1,
|
||||
type: 1,
|
||||
qrCode: 1,
|
||||
},
|
||||
filter: {
|
||||
systemId: this.features.application.getApplication().systemId,
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
lifetimes: {
|
||||
ready() {
|
||||
this.refreshOfflineAccounts();
|
||||
},
|
||||
},
|
||||
features: [{
|
||||
feature: 'application',
|
||||
callback() {
|
||||
this.refreshOfflineAccounts();
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"label": {
|
||||
"source": "来源",
|
||||
"code": "转帐凭证号",
|
||||
"cn": "创建者",
|
||||
"cm": "创建者手机号"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
import React from 'react';
|
||||
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
|
||||
import { EntityDict } from '../../../oak-app-domain';
|
||||
import { OfflinePayConfig } from '../../../types/PayConfig';
|
||||
type ColoredOffline = EntityDict['offlineAccount']['OpSchema'] & {
|
||||
color: string;
|
||||
};
|
||||
export default function Render(props: WebComponentProps<EntityDict, 'pay', false, {
|
||||
pays: (RowWithActions<EntityDict, 'pay'> & {
|
||||
creatorName: string;
|
||||
creatorMobile?: string;
|
||||
})[];
|
||||
offlineConfig: OfflinePayConfig;
|
||||
offlines: ColoredOffline[];
|
||||
}>): React.JSX.Element | null;
|
||||
export {};
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Tag, Modal, Alert, Divider, Button } from 'antd';
|
||||
import Styles from './web.pc.module.less';
|
||||
import { PAY_CHANNEL_OFFLINE_NAME } from '../../../types/PayConfig';
|
||||
import ListPro from 'oak-frontend-base/es/components/listPro';
|
||||
import { RenderOffline } from '../detail/web.pc';
|
||||
import { generateNewId } from 'oak-domain/lib/utils/uuid';
|
||||
import { generateNewId, generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
|
||||
export default function Render(props) {
|
||||
const { pays, offlineConfig } = props.data;
|
||||
const { pays, offlines } = props.data;
|
||||
const { t, execute, updateItem, setMessage, clean } = props.methods;
|
||||
const [spId, setSpId] = useState('');
|
||||
if (pays) {
|
||||
|
|
@ -39,19 +38,40 @@ export default function Render(props) {
|
|||
path: 'creatorName',
|
||||
label: t('label.cn'),
|
||||
span: 1,
|
||||
type: 'string',
|
||||
render: (row) => {
|
||||
const { creatorName, creatorMobile } = row;
|
||||
return (<div>
|
||||
<div>{creatorName}</div>
|
||||
<div>{creatorMobile}</div>
|
||||
</div>);
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'creatorMobile',
|
||||
label: t('label.cm'),
|
||||
path: 'phantom3',
|
||||
span: 1,
|
||||
type: 'string',
|
||||
label: t('label.code'),
|
||||
},
|
||||
{
|
||||
path: 'channel',
|
||||
span: 1,
|
||||
label: t('pay:attr.channel'),
|
||||
render: (row) => <Tag>{t(`payChannel::${row.channel}`)}</Tag>
|
||||
label: t('pay:attr.entity'),
|
||||
render: (row) => {
|
||||
const { entity } = row;
|
||||
const colorDict = {
|
||||
'account': 'blue',
|
||||
'offlineAccount': 'red',
|
||||
'wpProduct': 'green',
|
||||
};
|
||||
return (<div>
|
||||
<Tag color={colorDict[entity] || 'gray'}>{t(`payChannel::${row.entity}`)}</Tag>
|
||||
{entity === 'offlineAccount' && (<div className={Styles.entityDetail}>
|
||||
{t(`offlineAccount:v.type.${row.offlineAccount.type}`)}
|
||||
</div>)}
|
||||
{entity === 'wpProduct' && <div className={Styles.entityDetail}>
|
||||
{t(`wpProduct:v.type.${row.wpProduct.type}`)}
|
||||
</div>}
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
]} onAction={(row, action) => {
|
||||
if (action === 'close') {
|
||||
|
|
@ -72,12 +92,14 @@ export default function Render(props) {
|
|||
},
|
||||
}
|
||||
]);
|
||||
}
|
||||
},
|
||||
okText: t('common::confirm'),
|
||||
cancelText: t('common::action.cancel')
|
||||
});
|
||||
}
|
||||
else if (action === 'succeedPaying') {
|
||||
const { channel } = row;
|
||||
if (channel !== PAY_CHANNEL_OFFLINE_NAME) {
|
||||
const { entity } = row;
|
||||
if (entity !== 'offlineAccount') {
|
||||
// 以后可能也有手动成功offline以外的pay的情况,先封掉
|
||||
setMessage({
|
||||
title: t('spFail.title'),
|
||||
|
|
@ -86,9 +108,6 @@ export default function Render(props) {
|
|||
});
|
||||
}
|
||||
else {
|
||||
updateItem({
|
||||
paid:row.price,
|
||||
}, row.id, 'succeedPaying');
|
||||
setSpId(row.id);
|
||||
}
|
||||
}
|
||||
|
|
@ -100,12 +119,28 @@ export default function Render(props) {
|
|||
<div className={Styles.spCon}>
|
||||
<Alert type="warning" message={t('csp.tips')}/>
|
||||
<Divider style={{ width: '80%', alignSelf: 'flex-end' }}/>
|
||||
<RenderOffline pay={spRow} t={t} offline={offlineConfig} metaUpdatable={true} updateMeta={(meta) => {
|
||||
updateItem({ meta }, spRow.id, 'succeedPaying');
|
||||
}}/>
|
||||
<RenderOffline pay={spRow} t={t} offlines={offlines} offline={offlines.find(ele => ele.id === spRow.entityId)} updateOfflineId={(entityId) => updateItem({
|
||||
entity: 'offlineAccount',
|
||||
entityId
|
||||
}, spId)}/>
|
||||
<div className={Styles.btn}>
|
||||
<Button type="primary" onClick={async () => {
|
||||
await execute();
|
||||
const spRow = pays.find(ele => ele.id === spId);
|
||||
await execute(undefined, undefined, undefined, [
|
||||
{
|
||||
entity: 'pay',
|
||||
operation: {
|
||||
id: await generateNewIdAsync(),
|
||||
action: 'succeedPaying',
|
||||
data: {
|
||||
paid: spRow.price,
|
||||
},
|
||||
filter: {
|
||||
id: spId,
|
||||
},
|
||||
},
|
||||
}
|
||||
]);
|
||||
setSpId('');
|
||||
}}>
|
||||
{t('common::confirm')}
|
||||
|
|
|
|||
|
|
@ -8,4 +8,9 @@
|
|||
margin-top: 13px;
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.entityDetail {
|
||||
font-size: small;
|
||||
color: var(--oak-color-primary);
|
||||
}
|
||||
|
|
@ -1,38 +1,26 @@
|
|||
import { composeServerUrl } from 'oak-general-business/es/utils/domain';
|
||||
export default OakComponent({
|
||||
entity: 'system',
|
||||
isList: false,
|
||||
projection: {
|
||||
id: 1,
|
||||
payConfig: 1,
|
||||
application$system: {
|
||||
$entity: 'application',
|
||||
wpAccount$system: {
|
||||
$entity: 'wpAccount',
|
||||
data: {
|
||||
id: 1,
|
||||
payConfig: 1,
|
||||
name: 1,
|
||||
type: 1,
|
||||
},
|
||||
}
|
||||
},
|
||||
domain$system: {
|
||||
$entity: 'domain',
|
||||
offlineAccount$system: {
|
||||
$entity: 'offlineAccount',
|
||||
data: {
|
||||
id: 1,
|
||||
protocol: 1,
|
||||
url: 1,
|
||||
apiPath: 1,
|
||||
port: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
formData({ data, features }) {
|
||||
const operation = this.state.oakFullpath && this.features.runningTree.getOperations(this.state.oakFullpath);
|
||||
const domain = data && data.domain$system[0];
|
||||
const serverUrl = domain && composeServerUrl(domain);
|
||||
return {
|
||||
operation: operation && operation[0].operation,
|
||||
system: data,
|
||||
serverUrl,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,17 +1,11 @@
|
|||
import React from 'react';
|
||||
import { RowWithActions, WebComponentProps } from 'oak-frontend-base';
|
||||
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/types/Entity';
|
||||
import { EntityDict } from '../../../oak-app-domain';
|
||||
/**
|
||||
*
|
||||
* @param channel [label, value]
|
||||
*/
|
||||
export declare function registerSystemPayChannel(channel: [string, string]): void;
|
||||
/**
|
||||
*
|
||||
* @param type
|
||||
* @param channel [label, value]
|
||||
*/
|
||||
export declare function registerApplicationPayChannel(type: EntityDict['application']['OpSchema']['type'], channel: [string, string]): void;
|
||||
export declare function registerPayChannelComponent<ED extends EntityDict & BaseEntityDict, T extends keyof ED>(entity: T, component: (option: {
|
||||
oakPath: string;
|
||||
systemId: string;
|
||||
}) => React.ReactElement): void;
|
||||
export default function render(props: WebComponentProps<EntityDict, 'system', false, {
|
||||
system: RowWithActions<EntityDict, 'system'>;
|
||||
operation?: EntityDict['system']['Update'];
|
||||
|
|
|
|||
|
|
@ -1,101 +1,39 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Button, Row, Tabs } from 'antd';
|
||||
import { Tabs } from 'antd';
|
||||
import Styles from './web.pc.module.less';
|
||||
import PayConfigUpsert from '../upsert';
|
||||
import { generateNewId } from 'oak-domain/lib/utils/uuid';
|
||||
import { AppTypeToPayChannelDict } from '../../../utils/payClazz';
|
||||
const SystemPayChannels = []; // [label, value]
|
||||
const ApplicationPayChannels = {};
|
||||
/**
|
||||
*
|
||||
* @param channel [label, value]
|
||||
*/
|
||||
export function registerSystemPayChannel(channel) {
|
||||
SystemPayChannels.push(channel);
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param type
|
||||
* @param channel [label, value]
|
||||
*/
|
||||
export function registerApplicationPayChannel(type, channel) {
|
||||
if (ApplicationPayChannels[type]) {
|
||||
ApplicationPayChannels[type].push(channel);
|
||||
}
|
||||
else {
|
||||
ApplicationPayChannels[type] = [channel];
|
||||
}
|
||||
import OfflineConfig from '../../offlineAccount/config';
|
||||
import WpAccountConfig from '../../wpAccount/config';
|
||||
const PayChannelConfigDict = {
|
||||
'wpAccount': WpAccountConfig,
|
||||
};
|
||||
export function registerPayChannelComponent(entity, component) {
|
||||
PayChannelConfigDict[entity] = component;
|
||||
}
|
||||
export default function render(props) {
|
||||
const { system, oakFullpath, operation, oakDirty, serverUrl, oakExecutable } = props.data;
|
||||
const { t, update, setMessage, execute } = props.methods;
|
||||
const defaultApplicationPayChannelDict = {
|
||||
web: AppTypeToPayChannelDict.web.map(ele => [t(`payChannel::internal.${ele}`), ele]),
|
||||
wechatMp: AppTypeToPayChannelDict.wechatMp.map(ele => [t(`payChannel::internal.${ele}`), ele]),
|
||||
wechatPublic: AppTypeToPayChannelDict.wechatPublic.map(ele => [t(`payChannel::internal.${ele}`), ele]),
|
||||
};
|
||||
const [key, setKey] = useState('');
|
||||
if (system && oakFullpath) {
|
||||
const { payConfig, application$system: applications } = system;
|
||||
return (<div className={Styles.container}>
|
||||
<Row justify="end" style={{ marginBottom: 12 }}>
|
||||
<Button type="primary" disabled={!oakDirty && oakExecutable !== true} onClick={() => execute()}>
|
||||
{t('common::action.save')}
|
||||
</Button>
|
||||
</Row>
|
||||
<Tabs tabPosition="left" items={[
|
||||
<Tabs className={Styles.tabs} tabPosition="left" items={[
|
||||
{
|
||||
label: (<div className={Styles.systemLabel}>
|
||||
{t('system')}
|
||||
{t('offlineAccount:name')}
|
||||
</div>),
|
||||
key: 'system',
|
||||
children: (<PayConfigUpsert key="system" serverUrl={serverUrl} config={payConfig} update={(config) => update({ payConfig: config })} channels={SystemPayChannels.concat([[t('payChannel::internal.ACCOUNT'), 'ACCOUNT'], [t('payChannel::internal.OFFLINE'), 'OFFLINE']])} t={t}/>),
|
||||
key: 'offlineAccount',
|
||||
children: (<OfflineConfig oakPath={`${oakFullpath}.offlineAccount$system`} systemId={system.id}/>),
|
||||
},
|
||||
{
|
||||
label: (<div className={Styles.padding}>
|
||||
{t('appsBelow')}
|
||||
</div>),
|
||||
disabled: true,
|
||||
key: 'padding',
|
||||
},
|
||||
...applications.map((app) => {
|
||||
const { type, id, payConfig: appPayConfig } = app;
|
||||
...Object.keys(PayChannelConfigDict).map((ele) => {
|
||||
const C = PayChannelConfigDict[ele];
|
||||
return {
|
||||
key: id,
|
||||
label: (<div>
|
||||
{app.name}
|
||||
label: (<div className={Styles.systemLabel}>
|
||||
{t(`${ele}:name`)}
|
||||
</div>),
|
||||
children: (<PayConfigUpsert key={id} serverUrl={serverUrl} config={appPayConfig} update={(config) => update({
|
||||
application$system: {
|
||||
id: generateNewId(),
|
||||
action: 'update',
|
||||
data: {
|
||||
payConfig: config,
|
||||
},
|
||||
filter: {
|
||||
id,
|
||||
}
|
||||
}
|
||||
})} channels={(defaultApplicationPayChannelDict[type] || []).concat(ApplicationPayChannels[type] || [])} t={t}/>),
|
||||
key: 'ele',
|
||||
children: <C oakPath={`${oakFullpath}.${ele}$system`} systemId={system.id}/>
|
||||
};
|
||||
})
|
||||
]} onTabClick={(activeKey) => {
|
||||
if (key && operation) {
|
||||
const { application$system } = operation.data;
|
||||
if (application$system) {
|
||||
const { filter } = application$system;
|
||||
if (filter?.id === key) {
|
||||
setMessage({
|
||||
type: 'warning',
|
||||
content: t('mayLossUpdate', {
|
||||
name: applications?.find(ele => ele.id === key).name
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
setKey(activeKey);
|
||||
}}/>
|
||||
]}/>
|
||||
</div>);
|
||||
}
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@
|
|||
padding: 8px;
|
||||
height: 100%;
|
||||
|
||||
.tabs {
|
||||
height: 100%;
|
||||
}
|
||||
// .tabLabel {
|
||||
// writing-mode: vertical-rl;
|
||||
// letter-spacing: .2rem;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "wechatPay", false, {
|
||||
systemId: string;
|
||||
}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import { composeServerUrl } from "oak-general-business/es/utils/domain";
|
||||
export default OakComponent({
|
||||
entity: 'wechatPay',
|
||||
projection: {
|
||||
id: 1,
|
||||
refundNotifyUrl: 1,
|
||||
payNotifyUrl: 1,
|
||||
system: {
|
||||
id: 1,
|
||||
domain$system: {
|
||||
$entity: 'domain',
|
||||
data: {
|
||||
id: 1,
|
||||
protocol: 1,
|
||||
url: 1,
|
||||
apiPath: 1,
|
||||
port: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
isList: false,
|
||||
formData({ data }) {
|
||||
const domain = data && data.system?.domain$system?.[0];
|
||||
const serverUrl = domain && composeServerUrl(domain);
|
||||
return {
|
||||
wechatPay: data,
|
||||
serverUrl,
|
||||
};
|
||||
},
|
||||
properties: {
|
||||
systemId: '',
|
||||
},
|
||||
lifetimes: {
|
||||
ready() {
|
||||
const { systemId } = this.props;
|
||||
if (this.isCreation()) {
|
||||
this.update({
|
||||
systemId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"placeholder": {
|
||||
"lossRatio": "填百分比(0.6就代表千分之六)",
|
||||
"payNotifyUrl": "endpoint",
|
||||
"refundNotifyUrl": "endpoint"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import React from 'react';
|
||||
import { EntityDict } from "../../../oak-app-domain";
|
||||
import { RowWithActions, WebComponentProps } from "oak-frontend-base";
|
||||
export default function render(props: WebComponentProps<EntityDict, 'wechatPay', false, {
|
||||
wechatPay?: (RowWithActions<EntityDict, 'wechatPay'>);
|
||||
serverUrl: string;
|
||||
}>): React.JSX.Element | null;
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import React from 'react';
|
||||
import { Form, Input } from 'antd';
|
||||
export default function render(props) {
|
||||
const { wechatPay, serverUrl } = props.data;
|
||||
const { t, update } = props.methods;
|
||||
if (wechatPay) {
|
||||
return (<>
|
||||
<Form.Item label={t('wechatPay:attr.payNotifyUrl')}>
|
||||
<Input prefix={`${serverUrl}/endpoint`} suffix='/${payId}' value={wechatPay.payNotifyUrl} placeholder={t('placeholder.payNotifyUrl')} onChange={({ currentTarget }) => {
|
||||
const payNotifyUrl = currentTarget.value;
|
||||
update({ payNotifyUrl });
|
||||
}}/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t('wechatPay:attr.refundNotifyUrl')}>
|
||||
<Input prefix={`${serverUrl}/endpoint`} suffix='/${refundId}' value={wechatPay.refundNotifyUrl} placeholder={t('placeholder.refundNotifyUrl')} onChange={({ currentTarget }) => {
|
||||
const refundNotifyUrl = currentTarget.value;
|
||||
update({ refundNotifyUrl });
|
||||
}}/>
|
||||
</Form.Item>
|
||||
</>);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.container {
|
||||
height: 100%;
|
||||
}
|
||||
|
|
@ -41,9 +41,9 @@ export default OakComponent({
|
|||
const { value, withdrawData } = this.state;
|
||||
const refundData = withdrawData && withdrawData.refund$entity?.map(ele => ele.data).map((refund) => {
|
||||
const { meta, price, loss } = refund;
|
||||
const { refundLossRatio, refundLossFloor, channel } = meta;
|
||||
const { lossExplanation, lossExplanationParams, channel } = meta;
|
||||
return {
|
||||
lossExp: refundLossRatio ? this.t('refund.lossExp.ratio', { ratio: refundLossRatio }) : (refundLossFloor ? this.t(`refund.lossExp.floor.${refundLossFloor}`) : this.t('refund.lossExp.none')),
|
||||
lossExp: lossExplanation ? this.t(lossExplanation, lossExplanationParams) : this.t(''),
|
||||
channel: this.t(`payChannel::${channel}`),
|
||||
priceYuan: ThousandCont(ToYuan(price), 2),
|
||||
lossYuan: ThousandCont(ToYuan(loss), 2),
|
||||
|
|
|
|||
|
|
@ -25,15 +25,5 @@
|
|||
"overflowRefundAmount": "请先提现自动退款部分的额度",
|
||||
"overflowManualAmount": "提现额度不能超过人工划款的额度"
|
||||
},
|
||||
"refund": {
|
||||
"lossExp": {
|
||||
"ratio": "%{ratio}%",
|
||||
"floor": {
|
||||
"1": "无",
|
||||
"2": "按角取整",
|
||||
"3": "按元取整"
|
||||
},
|
||||
"none": "无"
|
||||
}
|
||||
}
|
||||
"innerLogic": "自动运算"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,12 +17,12 @@ export default function Detail(props) {
|
|||
<div className={Styles.step}>
|
||||
<Steps labelPlacement="vertical" current={step} items={[
|
||||
{
|
||||
title: t('steps.1.title'),
|
||||
title: t('steps.first.title'),
|
||||
icon: <FileAddOutlined />,
|
||||
description: createAt,
|
||||
},
|
||||
{
|
||||
title: t('steps.2.title'),
|
||||
title: t('steps.second.title'),
|
||||
icon: <FieldTimeOutlined />,
|
||||
description: t(`method.v.${withdrawMethod}`)
|
||||
},
|
||||
|
|
@ -31,9 +31,9 @@ export default function Detail(props) {
|
|||
? (iState === 'failed'
|
||||
? <span style={{ color: 'red' }}>{t('steps.3.failed')}</span>
|
||||
: (iState === 'partiallySuccessful'
|
||||
? <span style={{ color: 'green' }}>{t('steps.3.partiallySuccess')}</span>
|
||||
: <span style={{ color: 'green' }}>{t('steps.3.success')}</span>))
|
||||
: t('steps.3.success'),
|
||||
? <span style={{ color: 'green' }}>{t('steps.third.partiallySuccess')}</span>
|
||||
: <span style={{ color: 'green' }}>{t('steps.third.success')}</span>))
|
||||
: t('steps.third.success'),
|
||||
icon: step === 2 ?
|
||||
(iState === 'failed' ? <CloseCircleOutlined style={{ color: 'red' }}/> : <CheckCircleOutlined style={{ color: 'green' }}/>)
|
||||
: <CheckCircleOutlined />
|
||||
|
|
@ -56,14 +56,14 @@ export default function Detail(props) {
|
|||
</span>
|
||||
</div>}
|
||||
<div className={Styles.item}>
|
||||
<span className={Styles.label}>{t('refund.label.0')}</span>
|
||||
<span className={Styles.label}>{t('refund.label.zero')}</span>
|
||||
<span className={Styles.value}>
|
||||
<span className={Styles.symbol}>{t('common::pay.symbol')}</span>
|
||||
{data.priceYuan}
|
||||
</span>
|
||||
</div>
|
||||
<div className={Styles.item}>
|
||||
<span className={Styles.label}>{t('refund.label.1')}</span>
|
||||
<span className={Styles.label}>{t('refund.label.one')}</span>
|
||||
<span className={Styles.value}>
|
||||
<span className={Styles.symbol}>{t('common::pay.symbol')}</span>
|
||||
{data.finalYuan}
|
||||
|
|
@ -71,18 +71,18 @@ export default function Detail(props) {
|
|||
</div>
|
||||
{data.iState !== 'failed' && <>
|
||||
<div className={Styles.item}>
|
||||
<span className={Styles.label}>{t('refund.label.2')}</span>
|
||||
<span className={Styles.label}>{t('refund.label.two')}</span>
|
||||
<span className={Styles.value}>
|
||||
<span className={Styles.symbol}>{t('common::pay.symbol')}</span>
|
||||
{data.lossYuan}
|
||||
</span>
|
||||
</div>
|
||||
<div className={Styles.item}>
|
||||
<span className={Styles.label}>{t('refund.label.3')}</span>
|
||||
<span className={Styles.label}>{t('refund.label.three')}</span>
|
||||
<span className={Styles.value}>{data.lossExp}</span>
|
||||
</div>
|
||||
<div className={Styles.item}>
|
||||
<span className={Styles.label}>{t('refund.label.4')}</span>
|
||||
<span className={Styles.label}>{t('refund.label.four')}</span>
|
||||
<span className={Styles.value}>{data.channel}</span>
|
||||
</div>
|
||||
</>}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { EntityDict } from "../../../oak-app-domain";
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<EntityDict, keyof EntityDict, boolean, {
|
||||
createAt: string;
|
||||
withdrawMethod: "channel" | "refund";
|
||||
withdrawMethod: "refund" | "channel";
|
||||
refundData: {
|
||||
lossExp: string;
|
||||
channel: string;
|
||||
|
|
@ -15,7 +15,7 @@ declare const _default: (props: import("oak-frontend-base").ReactComponentProps<
|
|||
}[];
|
||||
withdrawExactPrice: string;
|
||||
t: (k: string, p?: any) => string;
|
||||
step: 0 | 1 | 2;
|
||||
step: 0 | 2 | 1;
|
||||
iState: string | null | undefined;
|
||||
}>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
"one": "到帐金额",
|
||||
"two": "手续费",
|
||||
"three": "手续费率",
|
||||
"four": "收款帐户",
|
||||
"four": "退款通道",
|
||||
"updateAt": "更新时间",
|
||||
"reason": "异常原因"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
/// <reference types="wechat-miniprogram" />
|
||||
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<import("../../../oak-app-domain").EntityDict, "wpAccount", true, WechatMiniprogram.Component.DataOption>) => React.ReactElement;
|
||||
export default _default;
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
export default OakComponent({
|
||||
entity: 'wpAccount',
|
||||
isList: true,
|
||||
projection: {
|
||||
id: 1,
|
||||
price: 1,
|
||||
mchId: 1,
|
||||
refundGapDays: 1,
|
||||
refundLossRatio: 1,
|
||||
refundLossFloor: 1,
|
||||
taxlossRatio: 1,
|
||||
enabled: 1,
|
||||
},
|
||||
formData({ data, legalActions }) {
|
||||
return {
|
||||
accounts: data,
|
||||
canCreate: legalActions?.includes('create'),
|
||||
};
|
||||
},
|
||||
actions: ['create', 'update', 'remove'],
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue