This commit is contained in:
Xu Chang 2024-06-07 21:26:07 +08:00
commit 8f66c1281f
672 changed files with 21166 additions and 4801 deletions

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

5
es/checkers/offlineAccount.d.ts vendored Normal file
View File

@ -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;

View File

@ -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;

View File

@ -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: {

5
es/checkers/wpProduct.d.ts vendored Normal file
View File

@ -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;

113
es/checkers/wpProduct.js Normal file
View File

@ -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;

View File

@ -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, };

View File

@ -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"
}
}

View File

@ -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>);
}

View File

@ -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>);
}

View File

@ -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;

View File

@ -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() {

View File

@ -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"
}
}

View File

@ -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;
}
}

View File

@ -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>

View File

@ -12,9 +12,10 @@
"content": "您有未支付的充值,请先完成支付",
"go": "前往支付"
},
"history": "账单",
"history": "账单",
"error": {
"maxOverflow": "充值金额不得高于%{value}元",
"minOverflow": "充值金额不得低于%{value}元"
}
},
"yuan": "(元)"
}

View File

@ -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;

View File

@ -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>

View File

@ -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%;
}
}
}

View File

@ -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;

View File

@ -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 />&nbsp;{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 />&nbsp;{t('history')}</span>
<span className={Styles.title}><ScheduleOutlined />&nbsp;{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>

12
es/components/deposit/new/index.d.ts vendored Normal file
View File

@ -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;

View File

@ -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);
},
}
});

View File

@ -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"
}
}

View File

@ -0,0 +1,7 @@
.container {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}

View File

@ -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>

View File

@ -0,0 +1,14 @@
{
"placeholder": "一次最大充值%{max}元",
"label": {
"depPrice": "充值金额",
"channel": "充值渠道"
},
"tips": {
"depositLoss": "线上充值将按照充值渠道的扣款比例扣除相应费用一般是0.6%),敬请知晓",
"depositLossRatio": "线上充值将按照%{ratio}%的比例扣除相应费用,敬请知晓"
},
"error": {
"noChannel": "未配置交费渠道,请联系系统管理员"
}
}

17
es/components/deposit/new/web.d.ts vendored Normal file
View File

@ -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;

View File

@ -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>);
}

16
es/components/deposit/new/web.pc.d.ts vendored Normal file
View File

@ -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;

View File

@ -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>);
}

View File

@ -0,0 +1,11 @@
.tips {
margin-bottom: 4px;
}
.container {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}

View File

@ -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;

View File

@ -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'],
});

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,7 @@
{
"tips": "线下账户是需要system的相关工作人员手动同步收款和打款结果的账户",
"notnull": "属性\"%{value}\"不能为空",
"noData": "没有数据",
"confirmDelete": "确定删除",
"areYouSure": "确认删除本数据吗?"
}

View File

@ -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;

View File

@ -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>);
}

View File

@ -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);
}
}
}

View File

@ -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;

View File

@ -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,
};
},
});

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,3 @@
{
"tips": "线下交易账户是需要人工同步收款和打款行为的账户"
}

View File

@ -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;

View File

@ -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,
});
}
}
}
});

View File

@ -0,0 +1 @@
{}

View File

@ -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": "是否允许用户在系统中主动向此账号发起支付"
}
}

View File

@ -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;

View File

@ -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>);
}
}

View File

@ -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;

View File

@ -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>);
}
}

View File

@ -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>);
}

View File

@ -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>);
}

View File

@ -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;

View File

@ -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);
},
}
});

View File

@ -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"
}
}
}

View File

@ -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>

View File

@ -0,0 +1,3 @@
{
"offline": "线下充值"
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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;

View File

@ -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)}/>);
}

View File

@ -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;

View File

@ -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();
}
}
}
},
});

View File

@ -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"
}
}

View File

@ -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;
}

View File

@ -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>

View File

@ -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": "渠道"
}
}

View File

@ -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 {};

View File

@ -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}>

View File

@ -19,6 +19,10 @@
font-size: var(--oak-font-size-headline-medium);
font-weight: bolder;
}
.qrCode:hover {
cursor: pointer;
}
}
.paid {

View File

@ -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 {};

View File

@ -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}

View File

@ -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 {

View File

@ -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();
}
}]
});

View File

@ -1,6 +1,7 @@
{
"label": {
"source": "来源",
"code": "转帐凭证号",
"cn": "创建者",
"cm": "创建者手机号"
},

View File

@ -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 {};

View File

@ -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')}

View File

@ -8,4 +8,9 @@
margin-top: 13px;
align-self: flex-end;
}
}
.entityDetail {
font-size: small;
color: var(--oak-color-primary);
}

View File

@ -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,
};
},
});

View File

@ -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'];

View File

@ -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;

View File

@ -3,6 +3,9 @@
padding: 8px;
height: 100%;
.tabs {
height: 100%;
}
// .tabLabel {
// writing-mode: vertical-rl;
// letter-spacing: .2rem;

View File

@ -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;

View File

@ -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,
});
}
}
}
});

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,7 @@
{
"placeholder": {
"lossRatio": "填百分比(0.6就代表千分之六)",
"payNotifyUrl": "endpoint",
"refundNotifyUrl": "endpoint"
}
}

View File

@ -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;

View File

@ -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;
}

View File

@ -0,0 +1,3 @@
.container {
height: 100%;
}

View File

@ -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),

View File

@ -25,15 +25,5 @@
"overflowRefundAmount": "请先提现自动退款部分的额度",
"overflowManualAmount": "提现额度不能超过人工划款的额度"
},
"refund": {
"lossExp": {
"ratio": "%{ratio}%",
"floor": {
"1": "无",
"2": "按角取整",
"3": "按元取整"
},
"none": "无"
}
}
"innerLogic": "自动运算"
}

View File

@ -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>
</>}

View File

@ -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;

View File

@ -31,7 +31,7 @@
"one": "到帐金额",
"two": "手续费",
"three": "手续费率",
"four": "收款帐户",
"four": "退款通道",
"updateAt": "更新时间",
"reason": "异常原因"
}

View File

@ -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;

View File

@ -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