payment-kit 1.15.20 → 1.15.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/src/crons/base.ts +69 -7
- package/api/src/crons/subscription-trial-will-end.ts +20 -5
- package/api/src/crons/subscription-will-canceled.ts +22 -6
- package/api/src/crons/subscription-will-renew.ts +13 -4
- package/api/src/index.ts +4 -1
- package/api/src/integrations/arcblock/stake.ts +27 -0
- package/api/src/libs/audit.ts +4 -1
- package/api/src/libs/context.ts +48 -0
- package/api/src/libs/invoice.ts +2 -2
- package/api/src/libs/middleware.ts +39 -1
- package/api/src/libs/notification/template/subscription-canceled.ts +4 -0
- package/api/src/libs/notification/template/subscription-trial-will-end.ts +12 -34
- package/api/src/libs/notification/template/subscription-will-canceled.ts +82 -48
- package/api/src/libs/notification/template/subscription-will-renew.ts +16 -45
- package/api/src/libs/time.ts +13 -0
- package/api/src/libs/util.ts +17 -0
- package/api/src/locales/en.ts +12 -2
- package/api/src/locales/zh.ts +11 -2
- package/api/src/queues/checkout-session.ts +15 -0
- package/api/src/queues/event.ts +13 -4
- package/api/src/queues/invoice.ts +21 -3
- package/api/src/queues/payment.ts +3 -0
- package/api/src/queues/refund.ts +3 -0
- package/api/src/queues/subscription.ts +107 -2
- package/api/src/queues/usage-record.ts +4 -0
- package/api/src/queues/webhook.ts +9 -0
- package/api/src/routes/checkout-sessions.ts +40 -2
- package/api/src/routes/connect/recharge.ts +143 -0
- package/api/src/routes/connect/shared.ts +25 -0
- package/api/src/routes/customers.ts +2 -2
- package/api/src/routes/donations.ts +5 -1
- package/api/src/routes/events.ts +9 -4
- package/api/src/routes/payment-links.ts +40 -20
- package/api/src/routes/prices.ts +17 -4
- package/api/src/routes/products.ts +21 -2
- package/api/src/routes/refunds.ts +20 -3
- package/api/src/routes/subscription-items.ts +39 -2
- package/api/src/routes/subscriptions.ts +77 -40
- package/api/src/routes/usage-records.ts +29 -0
- package/api/src/store/models/event.ts +1 -0
- package/api/src/store/models/subscription.ts +2 -0
- package/api/tests/libs/time.spec.ts +54 -0
- package/blocklet.yml +1 -1
- package/package.json +19 -19
- package/src/app.tsx +10 -0
- package/src/components/subscription/actions/cancel.tsx +30 -9
- package/src/components/subscription/actions/index.tsx +11 -3
- package/src/components/webhook/attempts.tsx +122 -3
- package/src/locales/en.tsx +13 -0
- package/src/locales/zh.tsx +13 -0
- package/src/pages/customer/recharge.tsx +417 -0
- package/src/pages/customer/subscription/detail.tsx +38 -20
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.15.
|
|
3
|
+
"version": "1.15.22",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -43,29 +43,29 @@
|
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@abtnode/cron": "^1.16.32",
|
|
46
|
-
"@arcblock/did": "^1.18.
|
|
46
|
+
"@arcblock/did": "^1.18.136",
|
|
47
47
|
"@arcblock/did-auth-storage-nedb": "^1.7.1",
|
|
48
|
-
"@arcblock/did-connect": "^2.10.
|
|
49
|
-
"@arcblock/did-util": "^1.18.
|
|
50
|
-
"@arcblock/jwt": "^1.18.
|
|
51
|
-
"@arcblock/ux": "^2.10.
|
|
52
|
-
"@arcblock/validator": "^1.18.
|
|
48
|
+
"@arcblock/did-connect": "^2.10.51",
|
|
49
|
+
"@arcblock/did-util": "^1.18.136",
|
|
50
|
+
"@arcblock/jwt": "^1.18.136",
|
|
51
|
+
"@arcblock/ux": "^2.10.51",
|
|
52
|
+
"@arcblock/validator": "^1.18.136",
|
|
53
53
|
"@blocklet/js-sdk": "^1.16.32",
|
|
54
54
|
"@blocklet/logger": "^1.16.32",
|
|
55
|
-
"@blocklet/payment-react": "1.15.
|
|
55
|
+
"@blocklet/payment-react": "1.15.22",
|
|
56
56
|
"@blocklet/sdk": "^1.16.32",
|
|
57
|
-
"@blocklet/ui-react": "^2.10.
|
|
58
|
-
"@blocklet/uploader": "^0.1.
|
|
59
|
-
"@blocklet/xss": "^0.1.
|
|
57
|
+
"@blocklet/ui-react": "^2.10.51",
|
|
58
|
+
"@blocklet/uploader": "^0.1.46",
|
|
59
|
+
"@blocklet/xss": "^0.1.12",
|
|
60
60
|
"@mui/icons-material": "^5.16.6",
|
|
61
61
|
"@mui/lab": "^5.0.0-alpha.173",
|
|
62
62
|
"@mui/material": "^5.16.6",
|
|
63
63
|
"@mui/system": "^5.16.6",
|
|
64
|
-
"@ocap/asset": "^1.18.
|
|
65
|
-
"@ocap/client": "^1.18.
|
|
66
|
-
"@ocap/mcrypto": "^1.18.
|
|
67
|
-
"@ocap/util": "^1.18.
|
|
68
|
-
"@ocap/wallet": "^1.18.
|
|
64
|
+
"@ocap/asset": "^1.18.136",
|
|
65
|
+
"@ocap/client": "^1.18.136",
|
|
66
|
+
"@ocap/mcrypto": "^1.18.136",
|
|
67
|
+
"@ocap/util": "^1.18.136",
|
|
68
|
+
"@ocap/wallet": "^1.18.136",
|
|
69
69
|
"@react-pdf/renderer": "^3.4.4",
|
|
70
70
|
"@stripe/react-stripe-js": "^2.7.3",
|
|
71
71
|
"@stripe/stripe-js": "^2.4.0",
|
|
@@ -117,8 +117,8 @@
|
|
|
117
117
|
},
|
|
118
118
|
"devDependencies": {
|
|
119
119
|
"@abtnode/types": "^1.16.32",
|
|
120
|
-
"@arcblock/eslint-config-ts": "^0.3.
|
|
121
|
-
"@blocklet/payment-types": "1.15.
|
|
120
|
+
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
121
|
+
"@blocklet/payment-types": "1.15.22",
|
|
122
122
|
"@types/cookie-parser": "^1.4.7",
|
|
123
123
|
"@types/cors": "^2.8.17",
|
|
124
124
|
"@types/debug": "^4.1.12",
|
|
@@ -160,5 +160,5 @@
|
|
|
160
160
|
"parser": "typescript"
|
|
161
161
|
}
|
|
162
162
|
},
|
|
163
|
-
"gitHead": "
|
|
163
|
+
"gitHead": "12a3cbcf04d0c8bdbbf33c04d07cecfb24bd3e9b"
|
|
164
164
|
}
|
package/src/app.tsx
CHANGED
|
@@ -27,6 +27,7 @@ const CustomerSubscriptionDetail = React.lazy(() => import('./pages/customer/sub
|
|
|
27
27
|
const CustomerSubscriptionEmbed = React.lazy(() => import('./pages/customer/subscription/embed'));
|
|
28
28
|
const CustomerSubscriptionChangePlan = React.lazy(() => import('./pages/customer/subscription/change-plan'));
|
|
29
29
|
const CustomerSubscriptionChangePayment = React.lazy(() => import('./pages/customer/subscription/change-payment'));
|
|
30
|
+
const CustomerRecharge = React.lazy(() => import('./pages/customer/recharge'));
|
|
30
31
|
|
|
31
32
|
// const theme = createTheme({
|
|
32
33
|
// typography: {
|
|
@@ -92,6 +93,15 @@ function App() {
|
|
|
92
93
|
</Layout>
|
|
93
94
|
}
|
|
94
95
|
/>
|
|
96
|
+
<Route
|
|
97
|
+
key="customer-recharge"
|
|
98
|
+
path="/customer/subscription/:id/recharge"
|
|
99
|
+
element={
|
|
100
|
+
<Layout>
|
|
101
|
+
<CustomerRecharge />
|
|
102
|
+
</Layout>
|
|
103
|
+
}
|
|
104
|
+
/>
|
|
95
105
|
<Route
|
|
96
106
|
key="customer-embed"
|
|
97
107
|
path="/customer/embed/subscription"
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
|
-
import { api, formatAmount, formatTime } from '@blocklet/payment-react';
|
|
2
|
+
import { api, dayjs, formatAmount, formatTime } from '@blocklet/payment-react';
|
|
3
3
|
import type { TSubscriptionExpanded } from '@blocklet/payment-types';
|
|
4
4
|
import { Box, Divider, FormControlLabel, Radio, RadioGroup, Stack, TextField, Typography, styled } from '@mui/material';
|
|
5
5
|
import { useRequest } from 'ahooks';
|
|
6
|
-
import { useEffect } from 'react';
|
|
6
|
+
import { useEffect, useMemo } from 'react';
|
|
7
7
|
import { Controller, useFormContext, useWatch } from 'react-hook-form';
|
|
8
8
|
|
|
9
9
|
const fetchData = (id: string, time: string): Promise<{ total: string; unused: string }> => {
|
|
@@ -21,13 +21,23 @@ export default function SubscriptionCancelForm({ data }: { data: TSubscriptionEx
|
|
|
21
21
|
const cancelTime = useWatch({ control, name: 'cancel.time' });
|
|
22
22
|
const refundType = useWatch({ control, name: 'cancel.refund' });
|
|
23
23
|
const stakingType = useWatch({ control, name: 'cancel.staking' });
|
|
24
|
-
const {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
const actualCancelAt = useMemo(() => {
|
|
25
|
+
if (cancelAt === 'custom') {
|
|
26
|
+
return cancelTime;
|
|
27
|
+
}
|
|
28
|
+
if (cancelAt === 'current_period_end') {
|
|
29
|
+
return new Date(data.current_period_end * 1000);
|
|
30
|
+
}
|
|
31
|
+
return '';
|
|
32
|
+
}, [cancelAt, cancelTime]);
|
|
33
|
+
const { loading, data: refund, refresh } = useRequest(() => fetchData(data.id, actualCancelAt));
|
|
29
34
|
|
|
30
|
-
const { data: staking } = useRequest(() =>
|
|
35
|
+
const { data: staking } = useRequest(() => {
|
|
36
|
+
if (data.paymentMethod?.type === 'arcblock') {
|
|
37
|
+
return fetchStakingData(data.id, actualCancelAt);
|
|
38
|
+
}
|
|
39
|
+
return Promise.resolve({ return_amount: '0', slash_amount: '0' });
|
|
40
|
+
});
|
|
31
41
|
useEffect(() => {
|
|
32
42
|
if (data) {
|
|
33
43
|
refresh();
|
|
@@ -71,7 +81,18 @@ export default function SubscriptionCancelForm({ data }: { data: TSubscriptionEx
|
|
|
71
81
|
{isCustom && (
|
|
72
82
|
<Controller
|
|
73
83
|
name="cancel.time"
|
|
74
|
-
rules={{
|
|
84
|
+
rules={{
|
|
85
|
+
required: isCustom,
|
|
86
|
+
validate: (value) => {
|
|
87
|
+
const now = dayjs();
|
|
88
|
+
const selectedTime = dayjs(value);
|
|
89
|
+
const periodEndTime = dayjs.unix(data.current_period_end);
|
|
90
|
+
if (selectedTime.isBefore(now) || selectedTime.isAfter(periodEndTime)) {
|
|
91
|
+
return t('admin.subscription.cancel.at.timeError');
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
},
|
|
95
|
+
}}
|
|
75
96
|
control={control}
|
|
76
97
|
render={({ field }) => (
|
|
77
98
|
<TextField
|
|
@@ -44,9 +44,17 @@ function SubscriptionActionsInner({ data, variant, onChange }: Props) {
|
|
|
44
44
|
slash_amount: '0',
|
|
45
45
|
},
|
|
46
46
|
runAsync: fetchStakeResultAsync,
|
|
47
|
-
} = useRequest(
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
} = useRequest(
|
|
48
|
+
() => {
|
|
49
|
+
if (data.paymentMethod?.type === 'arcblock') {
|
|
50
|
+
return fetchStakingData(data.id, '');
|
|
51
|
+
}
|
|
52
|
+
return Promise.resolve({ return_amount: '0', slash_amount: '0', total: '0' });
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
manual: true,
|
|
56
|
+
}
|
|
57
|
+
);
|
|
50
58
|
|
|
51
59
|
const stakeValue = useMemo(() => {
|
|
52
60
|
return formatBNStr(stakeResult?.slash_amount, data?.paymentCurrency?.decimal);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import CodeBlock from '@arcblock/ux/lib/CodeBlock';
|
|
3
3
|
import { api, formatTime } from '@blocklet/payment-react';
|
|
4
4
|
import type { Paginated, TEvent, TWebhookAttemptExpanded } from '@blocklet/payment-types';
|
|
5
|
-
import { CheckCircleOutlined, ErrorOutlined } from '@mui/icons-material';
|
|
5
|
+
import { CheckCircleOutlined, ErrorOutlined, InfoOutlined } from '@mui/icons-material';
|
|
6
6
|
import {
|
|
7
7
|
Box,
|
|
8
8
|
Button,
|
|
@@ -15,12 +15,15 @@ import {
|
|
|
15
15
|
ListSubheader,
|
|
16
16
|
Stack,
|
|
17
17
|
Typography,
|
|
18
|
+
Popper,
|
|
19
|
+
Paper,
|
|
18
20
|
} from '@mui/material';
|
|
19
21
|
import { useInfiniteScroll } from 'ahooks';
|
|
20
22
|
import React, { useEffect, useState } from 'react';
|
|
21
23
|
|
|
22
24
|
import { isEmpty } from 'lodash';
|
|
23
25
|
import { isSuccessAttempt } from '../../libs/util';
|
|
26
|
+
import InfoCard from '../info-card';
|
|
24
27
|
|
|
25
28
|
const fetchData = (params: Record<string, any> = {}): Promise<Paginated<TWebhookAttemptExpanded>> => {
|
|
26
29
|
const search = new URLSearchParams();
|
|
@@ -45,7 +48,7 @@ const groupAttemptsByDate = (attempts: TWebhookAttemptExpanded[]) => {
|
|
|
45
48
|
type Props = {
|
|
46
49
|
event_id?: string;
|
|
47
50
|
webhook_endpoint_id?: string;
|
|
48
|
-
event?: TEvent;
|
|
51
|
+
event?: TEvent & { requestInfo?: { avatar: string; email: string; did: string } };
|
|
49
52
|
};
|
|
50
53
|
|
|
51
54
|
WebhookAttempts.defaultProps = {
|
|
@@ -82,6 +85,35 @@ export default function WebhookAttempts({ event_id, webhook_endpoint_id, event }
|
|
|
82
85
|
setSelected(attempt);
|
|
83
86
|
};
|
|
84
87
|
|
|
88
|
+
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
|
89
|
+
|
|
90
|
+
const handleClick = (e: React.MouseEvent<HTMLElement>) => {
|
|
91
|
+
setAnchorEl(anchorEl ? null : e.currentTarget);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
96
|
+
if (anchorEl && !anchorEl.contains(e.target as Node) && !(e.target as Element).closest('.popper-content')) {
|
|
97
|
+
setAnchorEl(null);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const handleScroll = (e: Event) => {
|
|
102
|
+
// @ts-ignore
|
|
103
|
+
if (anchorEl && !e.target?.closest('.popper-content')) {
|
|
104
|
+
setAnchorEl(null);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
document.addEventListener('click', handleClickOutside);
|
|
109
|
+
window.addEventListener('scroll', handleScroll, true);
|
|
110
|
+
|
|
111
|
+
return () => {
|
|
112
|
+
document.removeEventListener('click', handleClickOutside);
|
|
113
|
+
window.removeEventListener('scroll', handleScroll, true);
|
|
114
|
+
};
|
|
115
|
+
}, [anchorEl]);
|
|
116
|
+
|
|
85
117
|
if (loading) {
|
|
86
118
|
return <CircularProgress />;
|
|
87
119
|
}
|
|
@@ -146,7 +178,94 @@ export default function WebhookAttempts({ event_id, webhook_endpoint_id, event }
|
|
|
146
178
|
)}
|
|
147
179
|
{data?.list.length === 0 && event && (
|
|
148
180
|
<Box>
|
|
149
|
-
<
|
|
181
|
+
<Stack direction="row" alignItems="center" spacing={1}>
|
|
182
|
+
<Typography variant="h6">Event Data</Typography>
|
|
183
|
+
<>
|
|
184
|
+
{/* @ts-ignore */}
|
|
185
|
+
<InfoOutlined
|
|
186
|
+
fontSize="small"
|
|
187
|
+
onClick={handleClick}
|
|
188
|
+
sx={{
|
|
189
|
+
color: 'text.secondary',
|
|
190
|
+
opacity: 0.6,
|
|
191
|
+
cursor: 'pointer',
|
|
192
|
+
}}
|
|
193
|
+
/>
|
|
194
|
+
<Popper
|
|
195
|
+
open={Boolean(anchorEl)}
|
|
196
|
+
anchorEl={anchorEl}
|
|
197
|
+
placement="right"
|
|
198
|
+
sx={{
|
|
199
|
+
zIndex: 1000,
|
|
200
|
+
'@media (max-width: 600px)': {
|
|
201
|
+
'& .MuiPaper-root': {
|
|
202
|
+
width: 'calc(100vw - 32px)',
|
|
203
|
+
maxWidth: 'none',
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
}}
|
|
207
|
+
modifiers={[
|
|
208
|
+
{
|
|
209
|
+
name: 'preventOverflow',
|
|
210
|
+
options: {
|
|
211
|
+
boundary: window,
|
|
212
|
+
altAxis: true,
|
|
213
|
+
padding: 16,
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: 'flip',
|
|
218
|
+
options: {
|
|
219
|
+
fallbackPlacements: ['bottom'],
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: 'matchWidth',
|
|
224
|
+
enabled: true,
|
|
225
|
+
fn: ({ state }) => {
|
|
226
|
+
if (window.innerWidth <= 600) {
|
|
227
|
+
state.styles.popper = {
|
|
228
|
+
...state.styles.popper,
|
|
229
|
+
width: 'calc(100vw - 32px)',
|
|
230
|
+
maxWidth: 'none',
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return state;
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
]}>
|
|
237
|
+
<Paper
|
|
238
|
+
className="popper-content"
|
|
239
|
+
elevation={3}
|
|
240
|
+
sx={{
|
|
241
|
+
p: 2,
|
|
242
|
+
border: '1px solid',
|
|
243
|
+
borderColor: 'divider',
|
|
244
|
+
maxWidth: 300,
|
|
245
|
+
'@media (max-width: 600px)': {
|
|
246
|
+
maxWidth: 'none',
|
|
247
|
+
margin: '0 auto',
|
|
248
|
+
},
|
|
249
|
+
}}>
|
|
250
|
+
{event.requestInfo ? (
|
|
251
|
+
<>
|
|
252
|
+
<Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
|
|
253
|
+
Requested by:
|
|
254
|
+
</Typography>
|
|
255
|
+
<InfoCard
|
|
256
|
+
logo={event.requestInfo.avatar}
|
|
257
|
+
name={event.requestInfo.email}
|
|
258
|
+
description={event.requestInfo.did || event.request.requested_by}
|
|
259
|
+
size={40}
|
|
260
|
+
/>
|
|
261
|
+
</>
|
|
262
|
+
) : (
|
|
263
|
+
<Typography>Requested by: {event.request?.requested_by || 'system'}</Typography>
|
|
264
|
+
)}
|
|
265
|
+
</Paper>
|
|
266
|
+
</Popper>
|
|
267
|
+
</>
|
|
268
|
+
</Stack>
|
|
150
269
|
<CodeBlock language="json">{JSON.stringify(event.data, null, 2)}</CodeBlock>
|
|
151
270
|
</Box>
|
|
152
271
|
)}
|
package/src/locales/en.tsx
CHANGED
|
@@ -457,6 +457,7 @@ export default flat({
|
|
|
457
457
|
now: 'Immediately ({date})',
|
|
458
458
|
current_period_end: 'End of current period ({date})',
|
|
459
459
|
custom: 'On a custom date',
|
|
460
|
+
timeError: 'Cancel time must be within the current period',
|
|
460
461
|
},
|
|
461
462
|
refund: {
|
|
462
463
|
title: 'Refund',
|
|
@@ -616,5 +617,17 @@ export default flat({
|
|
|
616
617
|
product: {
|
|
617
618
|
empty: 'No Product',
|
|
618
619
|
},
|
|
620
|
+
recharge: {
|
|
621
|
+
title: 'Recharge',
|
|
622
|
+
amount: 'Amount',
|
|
623
|
+
submit: 'Submit',
|
|
624
|
+
unsupported: 'Unsupported currency, please select another one',
|
|
625
|
+
receiveAddress: 'Receive Address',
|
|
626
|
+
view: 'View Subscription',
|
|
627
|
+
success: 'Recharge successfully',
|
|
628
|
+
custom: 'Custom',
|
|
629
|
+
estimatedDuration: '{duration} {unit} est.',
|
|
630
|
+
intervals: 'intervals',
|
|
631
|
+
},
|
|
619
632
|
},
|
|
620
633
|
});
|
package/src/locales/zh.tsx
CHANGED
|
@@ -447,6 +447,7 @@ export default flat({
|
|
|
447
447
|
now: '立即取消({date})',
|
|
448
448
|
current_period_end: '本周期结束后({date})',
|
|
449
449
|
custom: '自定义取消日期',
|
|
450
|
+
timeError: '取消时间必须在当前周期内',
|
|
450
451
|
},
|
|
451
452
|
refund: {
|
|
452
453
|
title: '退款',
|
|
@@ -604,5 +605,17 @@ export default flat({
|
|
|
604
605
|
product: {
|
|
605
606
|
empty: '没有订阅产品',
|
|
606
607
|
},
|
|
608
|
+
recharge: {
|
|
609
|
+
title: '充值',
|
|
610
|
+
amount: '金额',
|
|
611
|
+
submit: '提交',
|
|
612
|
+
unsupported: '暂不支持该货币充值,请选择其他货币',
|
|
613
|
+
receiveAddress: '收款地址',
|
|
614
|
+
view: '查看订阅',
|
|
615
|
+
success: '充值成功',
|
|
616
|
+
estimatedDuration: '预计可用 {duration} {unit}',
|
|
617
|
+
custom: '自定义',
|
|
618
|
+
intervals: '个周期',
|
|
619
|
+
},
|
|
607
620
|
},
|
|
608
621
|
});
|