payment-kit 1.20.5 → 1.20.6
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/index.ts +11 -3
- package/api/src/index.ts +18 -14
- package/api/src/libs/adapters/launcher-adapter.ts +177 -0
- package/api/src/libs/env.ts +7 -0
- package/api/src/libs/url.ts +77 -0
- package/api/src/libs/vendor-adapter-factory.ts +22 -0
- package/api/src/libs/vendor-adapter.ts +109 -0
- package/api/src/libs/vendor-fulfillment.ts +321 -0
- package/api/src/queues/payment.ts +14 -10
- package/api/src/queues/payout.ts +1 -0
- package/api/src/queues/vendor/vendor-commission.ts +192 -0
- package/api/src/queues/vendor/vendor-fulfillment-coordinator.ts +627 -0
- package/api/src/queues/vendor/vendor-fulfillment.ts +97 -0
- package/api/src/queues/vendor/vendor-status-check.ts +179 -0
- package/api/src/routes/checkout-sessions.ts +3 -0
- package/api/src/routes/index.ts +2 -0
- package/api/src/routes/products.ts +72 -1
- package/api/src/routes/vendor.ts +526 -0
- package/api/src/store/migrations/20250820-add-product-vendor.ts +102 -0
- package/api/src/store/migrations/20250822-add-vendor-config-to-products.ts +56 -0
- package/api/src/store/models/checkout-session.ts +84 -18
- package/api/src/store/models/index.ts +3 -0
- package/api/src/store/models/payout.ts +11 -0
- package/api/src/store/models/product-vendor.ts +118 -0
- package/api/src/store/models/product.ts +15 -0
- package/blocklet.yml +8 -2
- package/doc/vendor_fulfillment_system.md +931 -0
- package/package.json +5 -4
- package/src/components/collapse.tsx +1 -0
- package/src/components/product/edit.tsx +9 -0
- package/src/components/product/form.tsx +11 -0
- package/src/components/product/vendor-config.tsx +249 -0
- package/src/components/vendor/actions.tsx +145 -0
- package/src/locales/en.tsx +89 -0
- package/src/locales/zh.tsx +89 -0
- package/src/pages/admin/products/index.tsx +11 -1
- package/src/pages/admin/products/products/detail.tsx +79 -2
- package/src/pages/admin/products/vendors/create.tsx +418 -0
- package/src/pages/admin/products/vendors/index.tsx +313 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.6",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"lint": "tsc --noEmit && eslint src api/src --ext .mjs,.js,.jsx,.ts,.tsx",
|
|
@@ -55,7 +55,8 @@
|
|
|
55
55
|
"@blocklet/error": "^0.2.5",
|
|
56
56
|
"@blocklet/js-sdk": "^1.16.52-beta-20250908-085420-224a58fa",
|
|
57
57
|
"@blocklet/logger": "^1.16.52-beta-20250908-085420-224a58fa",
|
|
58
|
-
"@blocklet/payment-react": "1.20.
|
|
58
|
+
"@blocklet/payment-react": "1.20.6",
|
|
59
|
+
"@blocklet/payment-vendor": "1.20.6",
|
|
59
60
|
"@blocklet/sdk": "^1.16.52-beta-20250908-085420-224a58fa",
|
|
60
61
|
"@blocklet/ui-react": "^3.1.37",
|
|
61
62
|
"@blocklet/uploader": "^0.2.10",
|
|
@@ -124,7 +125,7 @@
|
|
|
124
125
|
"devDependencies": {
|
|
125
126
|
"@abtnode/types": "^1.16.52-beta-20250908-085420-224a58fa",
|
|
126
127
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
127
|
-
"@blocklet/payment-types": "1.20.
|
|
128
|
+
"@blocklet/payment-types": "1.20.6",
|
|
128
129
|
"@types/cookie-parser": "^1.4.9",
|
|
129
130
|
"@types/cors": "^2.8.19",
|
|
130
131
|
"@types/debug": "^4.1.12",
|
|
@@ -171,5 +172,5 @@
|
|
|
171
172
|
"parser": "typescript"
|
|
172
173
|
}
|
|
173
174
|
},
|
|
174
|
-
"gitHead": "
|
|
175
|
+
"gitHead": "d04ed7906bd1e13240198623aa0d4eef4db8886e"
|
|
175
176
|
}
|
|
@@ -74,6 +74,7 @@ export default function IconCollapse(rawProps: Props) {
|
|
|
74
74
|
{props.addons} {expanded ? <ExpandLessOutlined /> : <ExpandMoreOutlined />}
|
|
75
75
|
</Stack>
|
|
76
76
|
</Stack>
|
|
77
|
+
|
|
77
78
|
<Collapse in={expanded} sx={{ width: '100%' }}>
|
|
78
79
|
{expanded || props.lazy ? props.children : null}
|
|
79
80
|
</Collapse>
|
|
@@ -30,6 +30,15 @@ export default function EditProduct({
|
|
|
30
30
|
: // @ts-ignore
|
|
31
31
|
Object.keys(product.metadata).map((x) => ({ key: x, value: product.metadata[x] })),
|
|
32
32
|
prices: [],
|
|
33
|
+
// 处理 vendor_config 数据回显
|
|
34
|
+
vendor_config: product.vendor_config
|
|
35
|
+
? product.vendor_config.map((vendor: any) => ({
|
|
36
|
+
vendor_id: vendor.vendor_id, // 直接使用 vendor_id
|
|
37
|
+
commission_rate: vendor.commission_rate?.toString() || '0',
|
|
38
|
+
commission_type:
|
|
39
|
+
vendor.commission_type === 'fixed_amount' ? 'fixed' : vendor.commission_type || 'percentage',
|
|
40
|
+
}))
|
|
41
|
+
: [],
|
|
33
42
|
},
|
|
34
43
|
});
|
|
35
44
|
|
|
@@ -9,9 +9,15 @@ import MetadataForm from '../metadata/form';
|
|
|
9
9
|
import type { Price } from '../price/form';
|
|
10
10
|
import Uploader from '../uploader';
|
|
11
11
|
import ProductFeatures from './features';
|
|
12
|
+
import VendorConfig from './vendor-config';
|
|
12
13
|
|
|
13
14
|
export type Product = InferFormType<TProduct> & {
|
|
14
15
|
prices: Price[];
|
|
16
|
+
vendor_config: {
|
|
17
|
+
vendor_id: string;
|
|
18
|
+
commission_rate: number;
|
|
19
|
+
commission_type: 'percentage' | 'fixed';
|
|
20
|
+
}[];
|
|
15
21
|
};
|
|
16
22
|
|
|
17
23
|
type Props = {
|
|
@@ -114,6 +120,11 @@ export default function ProductForm({ simple = false }: Props) {
|
|
|
114
120
|
<Uploader onUploaded={onUploaded} preview={images[0]} />
|
|
115
121
|
</Box>
|
|
116
122
|
</Stack>
|
|
123
|
+
<Stack sx={{ '& .vendor-config': { mt: 2 } }}>
|
|
124
|
+
<Collapse trigger={t('admin.product.vendorConfig.title')}>
|
|
125
|
+
<VendorConfig />
|
|
126
|
+
</Collapse>
|
|
127
|
+
</Stack>
|
|
117
128
|
<Collapse trigger={t('admin.product.additional')}>
|
|
118
129
|
<Stack
|
|
119
130
|
spacing={2}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
|
+
import { FormInput, FormLabel, api } from '@blocklet/payment-react';
|
|
3
|
+
import { AddOutlined, DeleteOutlineOutlined } from '@mui/icons-material';
|
|
4
|
+
import { Alert, Box, Button, Divider, IconButton, MenuItem, Select, Stack, Typography } from '@mui/material';
|
|
5
|
+
import { useEffect, useRef, useState } from 'react';
|
|
6
|
+
import { Controller, useFieldArray, useFormContext, useWatch } from 'react-hook-form';
|
|
7
|
+
|
|
8
|
+
interface Vendor {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
vendor_key: string;
|
|
13
|
+
default_commission_rate: number;
|
|
14
|
+
default_commission_type: 'percentage' | 'fixed_amount';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function VendorConfig() {
|
|
18
|
+
const { t } = useLocaleContext();
|
|
19
|
+
const { control, setValue, setError: setFormError, clearErrors } = useFormContext();
|
|
20
|
+
const [vendors, setVendors] = useState<Vendor[]>([]);
|
|
21
|
+
const [loading, setLoading] = useState(false);
|
|
22
|
+
const [error, setError] = useState<string | null>(null);
|
|
23
|
+
const lastItemRef = useRef<HTMLDivElement | null>(null);
|
|
24
|
+
|
|
25
|
+
const vendorConfigs = useFieldArray({ control, name: 'vendor_config' });
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const fetchVendors = async () => {
|
|
29
|
+
try {
|
|
30
|
+
setLoading(true);
|
|
31
|
+
setError(null);
|
|
32
|
+
const response = await api.get('/api/vendors');
|
|
33
|
+
setVendors(response.data?.data || []);
|
|
34
|
+
} catch (err: any) {
|
|
35
|
+
setError(err.message || 'Failed to fetch vendors');
|
|
36
|
+
console.error('Failed to fetch vendors:', err);
|
|
37
|
+
} finally {
|
|
38
|
+
setLoading(false);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
fetchVendors();
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
const selectedVendorIds = vendorConfigs.fields.map((item: any) => item.vendor_id);
|
|
46
|
+
const noneSelectedVendors = vendors.filter((v) => !selectedVendorIds.includes(v.id));
|
|
47
|
+
|
|
48
|
+
const watchedVendorConfig = useWatch({ control, name: 'vendor_config' });
|
|
49
|
+
|
|
50
|
+
const [totalCommission, setTotalCommission] = useState(0);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const vendorConfigArray = watchedVendorConfig || [];
|
|
54
|
+
|
|
55
|
+
const percentageFields = vendorConfigArray.filter((field: any) => field?.commission_type === 'percentage');
|
|
56
|
+
|
|
57
|
+
const total = percentageFields.reduce((sum: number, field: any) => {
|
|
58
|
+
const rate = Number(field?.commission_rate || 0);
|
|
59
|
+
return sum + rate;
|
|
60
|
+
}, 0);
|
|
61
|
+
|
|
62
|
+
setTotalCommission(total);
|
|
63
|
+
|
|
64
|
+
const isValid = total <= 100;
|
|
65
|
+
|
|
66
|
+
if (vendorConfigArray.length > 0) {
|
|
67
|
+
if (isValid) {
|
|
68
|
+
clearErrors('vendor_config_total');
|
|
69
|
+
} else {
|
|
70
|
+
setFormError('vendor_config_total', {
|
|
71
|
+
type: 'manual',
|
|
72
|
+
message: t('admin.product.vendorConfig.totalCommissionExceeded', {
|
|
73
|
+
total: total.toFixed(0),
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
clearErrors('vendor_config_total');
|
|
79
|
+
}
|
|
80
|
+
}, [watchedVendorConfig, clearErrors, setFormError, t]);
|
|
81
|
+
|
|
82
|
+
const validateTotalCommission = () => {
|
|
83
|
+
return totalCommission <= 100;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<Box sx={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column' }} className="vendor-config">
|
|
88
|
+
{error && (
|
|
89
|
+
<Alert severity="error" onClose={() => setError(null)} sx={{ mt: 1 }}>
|
|
90
|
+
{error}
|
|
91
|
+
</Alert>
|
|
92
|
+
)}
|
|
93
|
+
|
|
94
|
+
<Stack
|
|
95
|
+
sx={{
|
|
96
|
+
maxHeight: 400,
|
|
97
|
+
minHeight: 'auto',
|
|
98
|
+
overflow: 'auto',
|
|
99
|
+
flex: 1,
|
|
100
|
+
}}>
|
|
101
|
+
{vendorConfigs.fields.map((vendorField: any, index: number) => (
|
|
102
|
+
<Stack
|
|
103
|
+
key={vendorField.id}
|
|
104
|
+
ref={index === vendorConfigs.fields.length - 1 ? lastItemRef : null}
|
|
105
|
+
sx={{ mb: 2, alignItems: 'flex-end', '&:last-child': { mb: 0 } }}
|
|
106
|
+
spacing={2}
|
|
107
|
+
direction="row">
|
|
108
|
+
<Stack direction="row" spacing={2} sx={{ flex: 1 }}>
|
|
109
|
+
<Controller
|
|
110
|
+
name={`vendor_config.${index}.vendor_id`}
|
|
111
|
+
control={control}
|
|
112
|
+
render={({ field }) => {
|
|
113
|
+
return (
|
|
114
|
+
<Box sx={{ minWidth: 320 }}>
|
|
115
|
+
<FormLabel>{t('admin.product.vendorConfig.productCode')}</FormLabel>
|
|
116
|
+
<Select
|
|
117
|
+
{...field}
|
|
118
|
+
fullWidth
|
|
119
|
+
onChange={(e) => {
|
|
120
|
+
const vendorId = e.target.value as string;
|
|
121
|
+
field.onChange(vendorId);
|
|
122
|
+
|
|
123
|
+
const selectedVendor = vendors.find((v) => v.id === vendorId);
|
|
124
|
+
if (selectedVendor) {
|
|
125
|
+
setValue(`vendor_config.${index}.commission_rate`, selectedVendor.default_commission_rate);
|
|
126
|
+
const commissionType =
|
|
127
|
+
selectedVendor.default_commission_type === 'fixed_amount'
|
|
128
|
+
? 'fixed'
|
|
129
|
+
: selectedVendor.default_commission_type;
|
|
130
|
+
setValue(`vendor_config.${index}.commission_type`, commissionType);
|
|
131
|
+
}
|
|
132
|
+
}}
|
|
133
|
+
renderValue={(value: string) => {
|
|
134
|
+
const vendor = vendors.find((v) => v.id === value);
|
|
135
|
+
if (!vendor) return null;
|
|
136
|
+
return <Box sx={{ display: 'flex', flexDirection: 'column' }}>{vendor.name}</Box>;
|
|
137
|
+
}}>
|
|
138
|
+
{noneSelectedVendors.length ? (
|
|
139
|
+
noneSelectedVendors.map((vendor) => (
|
|
140
|
+
<MenuItem key={vendor.id} value={vendor.id} sx={{ gap: 1 }}>
|
|
141
|
+
<Typography variant="h5">{vendor.name}</Typography>
|
|
142
|
+
<Typography variant="body2" color="text.secondary">
|
|
143
|
+
{vendor.vendor_key}
|
|
144
|
+
</Typography>
|
|
145
|
+
</MenuItem>
|
|
146
|
+
))
|
|
147
|
+
) : (
|
|
148
|
+
<MenuItem disabled value="">
|
|
149
|
+
{t('admin.product.vendorConfig.noVendor')}
|
|
150
|
+
</MenuItem>
|
|
151
|
+
)}
|
|
152
|
+
</Select>
|
|
153
|
+
</Box>
|
|
154
|
+
);
|
|
155
|
+
}}
|
|
156
|
+
/>
|
|
157
|
+
|
|
158
|
+
<Controller
|
|
159
|
+
name={`vendor_config.${index}.commission_type`}
|
|
160
|
+
control={control}
|
|
161
|
+
render={({ field }) => (
|
|
162
|
+
<Box sx={{ minWidth: 140, display: 'none' }}>
|
|
163
|
+
<FormLabel>{t('admin.product.vendorConfig.commissionType')}</FormLabel>
|
|
164
|
+
<Select {...field} fullWidth>
|
|
165
|
+
<MenuItem value="percentage">{t('admin.vendor.percentage')}</MenuItem>
|
|
166
|
+
<MenuItem value="fixed">{t('admin.vendor.fixedAmount')}</MenuItem>
|
|
167
|
+
</Select>
|
|
168
|
+
</Box>
|
|
169
|
+
)}
|
|
170
|
+
/>
|
|
171
|
+
|
|
172
|
+
<Controller
|
|
173
|
+
name={`vendor_config.${index}.commission_rate`}
|
|
174
|
+
control={control}
|
|
175
|
+
rules={{
|
|
176
|
+
required: t('admin.product.vendorConfig.commissionRateRequired'),
|
|
177
|
+
min: { value: 0, message: t('admin.product.vendorConfig.commissionRateMin') },
|
|
178
|
+
max: { value: 100, message: t('admin.product.vendorConfig.commissionRateMax') },
|
|
179
|
+
}}
|
|
180
|
+
render={({ field }) => (
|
|
181
|
+
<FormInput
|
|
182
|
+
{...(field as any)}
|
|
183
|
+
size="small"
|
|
184
|
+
placeholder={t('admin.product.vendorConfig.commissionRate')}
|
|
185
|
+
label={t('admin.product.vendorConfig.commissionRate')}
|
|
186
|
+
type="number"
|
|
187
|
+
inputProps={{
|
|
188
|
+
min: 0,
|
|
189
|
+
max: 100,
|
|
190
|
+
step: 10,
|
|
191
|
+
}}
|
|
192
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
193
|
+
field.onChange(e);
|
|
194
|
+
}}
|
|
195
|
+
/>
|
|
196
|
+
)}
|
|
197
|
+
/>
|
|
198
|
+
</Stack>
|
|
199
|
+
<IconButton
|
|
200
|
+
onClick={() => vendorConfigs.remove(index)}
|
|
201
|
+
sx={{
|
|
202
|
+
border: '1px solid',
|
|
203
|
+
borderColor: 'divider',
|
|
204
|
+
borderRadius: 1,
|
|
205
|
+
padding: '8px',
|
|
206
|
+
}}>
|
|
207
|
+
<DeleteOutlineOutlined color="error" sx={{ opacity: 0.75 }} />
|
|
208
|
+
</IconButton>
|
|
209
|
+
</Stack>
|
|
210
|
+
))}
|
|
211
|
+
</Stack>
|
|
212
|
+
{(watchedVendorConfig || []).length > 0 && (
|
|
213
|
+
<Alert severity={validateTotalCommission() ? 'info' : 'error'} sx={{ mt: 1, mb: 2 }}>
|
|
214
|
+
{validateTotalCommission()
|
|
215
|
+
? t('admin.product.vendorConfig.totalCommission', {
|
|
216
|
+
total: totalCommission.toFixed(0),
|
|
217
|
+
})
|
|
218
|
+
: t('admin.product.vendorConfig.totalCommissionExceeded', {
|
|
219
|
+
total: totalCommission.toFixed(0),
|
|
220
|
+
})}
|
|
221
|
+
</Alert>
|
|
222
|
+
)}
|
|
223
|
+
|
|
224
|
+
<Divider />
|
|
225
|
+
<Stack
|
|
226
|
+
direction="row"
|
|
227
|
+
sx={{
|
|
228
|
+
mt: vendorConfigs.fields.length ? 2 : 1,
|
|
229
|
+
justifyContent: 'space-between',
|
|
230
|
+
}}>
|
|
231
|
+
<Button
|
|
232
|
+
size="small"
|
|
233
|
+
variant="outlined"
|
|
234
|
+
color="primary"
|
|
235
|
+
sx={{ color: 'text.primary' }}
|
|
236
|
+
onClick={() =>
|
|
237
|
+
vendorConfigs.append({
|
|
238
|
+
vendor_id: '',
|
|
239
|
+
commission_rate: 0,
|
|
240
|
+
commission_type: 'percentage',
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
disabled={loading || selectedVendorIds.length === vendors.length || noneSelectedVendors.length === 0}>
|
|
244
|
+
<AddOutlined fontSize="small" /> {t('admin.product.vendorConfig.add')}
|
|
245
|
+
</Button>
|
|
246
|
+
</Stack>
|
|
247
|
+
</Box>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
|
+
import Toast from '@arcblock/ux/lib/Toast';
|
|
3
|
+
import { ConfirmDialog, api, formatError } from '@blocklet/payment-react';
|
|
4
|
+
import { useSetState } from 'ahooks';
|
|
5
|
+
import type { LiteralUnion } from 'type-fest';
|
|
6
|
+
|
|
7
|
+
import Actions from '../actions';
|
|
8
|
+
import ClickBoundary from '../click-boundary';
|
|
9
|
+
|
|
10
|
+
interface Vendor {
|
|
11
|
+
id: string;
|
|
12
|
+
vendor_key: string;
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
app_url: string;
|
|
16
|
+
webhook_path: string;
|
|
17
|
+
default_commission_rate: number;
|
|
18
|
+
default_commission_type: 'percentage' | 'fixed_amount';
|
|
19
|
+
status: 'active' | 'inactive';
|
|
20
|
+
order_create_params: Record<string, any>;
|
|
21
|
+
metadata: Record<string, any>;
|
|
22
|
+
created_at: string;
|
|
23
|
+
updated_at: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type VendorActionProps = {
|
|
27
|
+
data: Vendor;
|
|
28
|
+
onChange: (action: string) => void;
|
|
29
|
+
variant?: LiteralUnion<'compact' | 'normal', string>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export default function VendorActions({ data, variant = 'compact', onChange }: VendorActionProps) {
|
|
33
|
+
const { t } = useLocaleContext();
|
|
34
|
+
const [state, setState] = useSetState({
|
|
35
|
+
action: '',
|
|
36
|
+
loading: false,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const onToggleStatus = async () => {
|
|
40
|
+
try {
|
|
41
|
+
setState({ loading: true });
|
|
42
|
+
const newStatus = data.status === 'active' ? 'inactive' : 'active';
|
|
43
|
+
await api.put(`/api/vendors/${data.id}`, { status: newStatus }).then((res: any) => res.data);
|
|
44
|
+
Toast.success(t('common.saved'));
|
|
45
|
+
onChange(state.action);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.error(err);
|
|
48
|
+
Toast.error(formatError(err));
|
|
49
|
+
} finally {
|
|
50
|
+
setState({ loading: false, action: '' });
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const onRemove = async () => {
|
|
55
|
+
try {
|
|
56
|
+
setState({ loading: true });
|
|
57
|
+
await api.delete(`/api/vendors/${data.id}`).then((res: any) => res.data);
|
|
58
|
+
Toast.success(t('common.removed'));
|
|
59
|
+
onChange(state.action);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error(err);
|
|
62
|
+
Toast.error(formatError(err));
|
|
63
|
+
} finally {
|
|
64
|
+
setState({ loading: false, action: '' });
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// 测试发货功能
|
|
69
|
+
const onTestFulfillment = async () => {
|
|
70
|
+
try {
|
|
71
|
+
setState({ loading: true });
|
|
72
|
+
const result = await api
|
|
73
|
+
.post(`/api/vendors/${data.id}/test-fulfillment`, {
|
|
74
|
+
productCode: 'test_product',
|
|
75
|
+
})
|
|
76
|
+
.then((res: any) => res.data);
|
|
77
|
+
|
|
78
|
+
Toast.success(
|
|
79
|
+
'发货测试成功!\n' +
|
|
80
|
+
`订单号: ${result.fulfillmentResult?.orderId || 'N/A'}\n` +
|
|
81
|
+
`状态: ${result.fulfillmentResult?.status || 'N/A'}\n` +
|
|
82
|
+
`服务地址: ${result.fulfillmentResult?.serviceUrl || 'N/A'}`
|
|
83
|
+
);
|
|
84
|
+
onChange(state.action);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
Toast.error(`发货测试失败: ${formatError(err)}`);
|
|
87
|
+
} finally {
|
|
88
|
+
setState({ loading: false, action: '' });
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<ClickBoundary>
|
|
94
|
+
<Actions
|
|
95
|
+
variant={variant}
|
|
96
|
+
actions={[
|
|
97
|
+
{
|
|
98
|
+
label: '测试发货',
|
|
99
|
+
handler: onTestFulfillment,
|
|
100
|
+
color: 'success',
|
|
101
|
+
},
|
|
102
|
+
data.status === 'active'
|
|
103
|
+
? {
|
|
104
|
+
label: t('admin.vendor.deactivate'),
|
|
105
|
+
handler: () => setState({ action: 'toggle_status' }),
|
|
106
|
+
color: 'primary',
|
|
107
|
+
}
|
|
108
|
+
: {
|
|
109
|
+
label: t('admin.vendor.activate'),
|
|
110
|
+
handler: () => setState({ action: 'toggle_status' }),
|
|
111
|
+
color: 'primary',
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
label: t('admin.vendor.delete'),
|
|
115
|
+
handler: () => setState({ action: 'remove' }),
|
|
116
|
+
color: 'error',
|
|
117
|
+
divider: true,
|
|
118
|
+
},
|
|
119
|
+
].filter(Boolean)}
|
|
120
|
+
/>
|
|
121
|
+
{state.action === 'toggle_status' && (
|
|
122
|
+
<ConfirmDialog
|
|
123
|
+
onConfirm={onToggleStatus}
|
|
124
|
+
onCancel={() => setState({ action: '' })}
|
|
125
|
+
title={data.status === 'active' ? t('admin.vendor.deactivate') : t('admin.vendor.activate')}
|
|
126
|
+
message={
|
|
127
|
+
data.status === 'active'
|
|
128
|
+
? t('admin.vendor.deactivateTip', { name: data.name })
|
|
129
|
+
: t('admin.vendor.activateTip', { name: data.name })
|
|
130
|
+
}
|
|
131
|
+
loading={state.loading}
|
|
132
|
+
/>
|
|
133
|
+
)}
|
|
134
|
+
{state.action === 'remove' && (
|
|
135
|
+
<ConfirmDialog
|
|
136
|
+
onConfirm={onRemove}
|
|
137
|
+
onCancel={() => setState({ action: '' })}
|
|
138
|
+
title={t('admin.vendor.delete')}
|
|
139
|
+
message={t('admin.vendor.deleteTip', { name: data.name })}
|
|
140
|
+
loading={state.loading}
|
|
141
|
+
/>
|
|
142
|
+
)}
|
|
143
|
+
</ClickBoundary>
|
|
144
|
+
);
|
|
145
|
+
}
|
package/src/locales/en.tsx
CHANGED
|
@@ -40,6 +40,7 @@ export default flat({
|
|
|
40
40
|
advancedFeatures: 'Advanced Features',
|
|
41
41
|
quickStarts: 'Quick Starts',
|
|
42
42
|
copy: 'Copy',
|
|
43
|
+
copied: 'Copied',
|
|
43
44
|
copySuccess: 'Copy Success',
|
|
44
45
|
copyFailed: 'Copy Failed',
|
|
45
46
|
copyTip: 'Please copy manually',
|
|
@@ -98,6 +99,13 @@ export default flat({
|
|
|
98
99
|
overview: 'Overview',
|
|
99
100
|
payments: 'Payments',
|
|
100
101
|
connections: 'Connections',
|
|
102
|
+
back: 'Back',
|
|
103
|
+
actions: 'Actions',
|
|
104
|
+
create: 'Create',
|
|
105
|
+
creating: 'Creating...',
|
|
106
|
+
save: 'Save',
|
|
107
|
+
saving: 'Saving...',
|
|
108
|
+
cancel: 'Cancel',
|
|
101
109
|
paymentLinks: 'Payment links',
|
|
102
110
|
paymentMethods: 'Payment methods',
|
|
103
111
|
customers: 'Customers',
|
|
@@ -105,6 +113,7 @@ export default flat({
|
|
|
105
113
|
pricing: 'Pricing',
|
|
106
114
|
coupons: 'Coupons',
|
|
107
115
|
pricingTables: 'Pricing tables',
|
|
116
|
+
vendors: 'Vendors',
|
|
108
117
|
billing: 'Billing',
|
|
109
118
|
invoices: 'Invoices',
|
|
110
119
|
subscriptions: 'Subscriptions',
|
|
@@ -419,6 +428,25 @@ export default flat({
|
|
|
419
428
|
credit: 'Credit',
|
|
420
429
|
good: 'Good',
|
|
421
430
|
},
|
|
431
|
+
vendorConfig: {
|
|
432
|
+
title: 'Vendor Configuration',
|
|
433
|
+
add: 'Add Vendor',
|
|
434
|
+
empty: 'No vendors configured. Click "Add Vendor" to configure vendor services.',
|
|
435
|
+
vendor: 'Vendor',
|
|
436
|
+
vendorRequired: 'Vendor is required',
|
|
437
|
+
productCode: 'Product Code',
|
|
438
|
+
productCodeRequired: 'Product code is required',
|
|
439
|
+
commissionType: 'Commission Type',
|
|
440
|
+
commissionRate: 'Commission Rate',
|
|
441
|
+
commissionRateRequired: 'Commission rate is required',
|
|
442
|
+
commissionRateMin: 'Commission rate must be greater than or equal to 0',
|
|
443
|
+
commissionRateMax: 'Commission rate cannot exceed 100%',
|
|
444
|
+
amount: 'Fixed Amount',
|
|
445
|
+
noVendor: 'No vendor available',
|
|
446
|
+
amountRequired: 'Fixed amount is required',
|
|
447
|
+
totalCommission: 'Total commission rate: {total}%',
|
|
448
|
+
totalCommissionExceeded: 'Total commission rate exceeds 100%: {total}%',
|
|
449
|
+
},
|
|
422
450
|
},
|
|
423
451
|
price: {
|
|
424
452
|
name: 'Price',
|
|
@@ -835,6 +863,67 @@ export default flat({
|
|
|
835
863
|
success: 'Invoice voided',
|
|
836
864
|
},
|
|
837
865
|
},
|
|
866
|
+
vendor: {
|
|
867
|
+
create: 'Create Vendor',
|
|
868
|
+
edit: 'Edit Vendor',
|
|
869
|
+
delete: 'Delete Vendor',
|
|
870
|
+
save: 'Save Vendor',
|
|
871
|
+
saved: 'Vendor saved successfully',
|
|
872
|
+
test: 'Test',
|
|
873
|
+
testConnection: 'Test Connection',
|
|
874
|
+
testSuccess: 'Connection test successful',
|
|
875
|
+
testAfterSave: 'Vendor saved successfully. You can test the connection to verify the configuration.',
|
|
876
|
+
testToEnable: 'Vendor saved successfully. Test the connection to enable the vendor.',
|
|
877
|
+
enabled: 'Enabled',
|
|
878
|
+
disabled: 'Disabled',
|
|
879
|
+
addressCheckFailed: 'Address check failed, cannot save when enabled',
|
|
880
|
+
updateSuccess: 'Vendor updated successfully',
|
|
881
|
+
testFailed: 'Connection test failed (Status: {status})',
|
|
882
|
+
testError: 'Connection test failed - network error',
|
|
883
|
+
vendorKeyInvalid: 'Vendor type must contain only lowercase letters, numbers, and underscores',
|
|
884
|
+
deleteTitle: 'Delete Vendor',
|
|
885
|
+
deleteContent: 'Are you sure you want to delete vendor "{name}"? This action cannot be undone.',
|
|
886
|
+
deleteTip: 'Are you sure you want to delete vendor "{name}"? This action cannot be undone.',
|
|
887
|
+
activate: 'Activate Vendor',
|
|
888
|
+
activateTip: 'Are you sure you want to activate vendor "{name}"?',
|
|
889
|
+
deactivate: 'Deactivate Vendor',
|
|
890
|
+
deactivateTip: 'Are you sure you want to deactivate vendor "{name}"?',
|
|
891
|
+
name: 'Vendor Name',
|
|
892
|
+
nameRequired: 'Vendor name is required',
|
|
893
|
+
vendorKey: 'Vendor Type',
|
|
894
|
+
vendorKeyRequired: 'Vendor type is required',
|
|
895
|
+
vendorKeyHelp: 'Unique identifier for the vendor (e.g., launcher, did_names)',
|
|
896
|
+
description: 'Description',
|
|
897
|
+
appUrl: 'App URL',
|
|
898
|
+
appUrlRequired: 'App URL is required',
|
|
899
|
+
appUrlInvalid: 'Please enter a valid URL starting with http:// or https://',
|
|
900
|
+
appUrlHelp: 'The base URL of the vendor application',
|
|
901
|
+
webhookPath: 'Webhook Path',
|
|
902
|
+
webhookPathInvalid: 'Please enter a valid path starting with /',
|
|
903
|
+
webhookPathHelp: 'Optional webhook callback path (e.g., /webhooks/status)',
|
|
904
|
+
blockletMetaUrl: 'Blocklet Meta URL',
|
|
905
|
+
blockletMetaUrlRequired: 'Blocklet Meta URL is required',
|
|
906
|
+
blockletMetaUrlInvalid: 'Please enter a valid URL starting with http:// or https://',
|
|
907
|
+
blockletMetaUrlHelp: 'Required blocklet metadata URL for application information',
|
|
908
|
+
commission: 'Commission',
|
|
909
|
+
commissionRate: 'Commission Rate',
|
|
910
|
+
commissionRateRequired: 'Commission rate is required',
|
|
911
|
+
commissionRateMin: 'Commission rate must be greater than or equal to 0',
|
|
912
|
+
commissionRateMax: 'Commission rate is too high',
|
|
913
|
+
commissionType: 'Commission Type',
|
|
914
|
+
commissionRateHelp: 'Commission rate as percentage',
|
|
915
|
+
commissionAmountHelp: 'Fixed commission amount',
|
|
916
|
+
percentage: 'Percentage',
|
|
917
|
+
fixedAmount: 'Fixed Amount',
|
|
918
|
+
active: 'Active',
|
|
919
|
+
inactive: 'Inactive',
|
|
920
|
+
basicInfo: 'Basic Information',
|
|
921
|
+
apiConfig: 'API Configuration',
|
|
922
|
+
commissionConfig: 'Commission Configuration',
|
|
923
|
+
status: 'Status',
|
|
924
|
+
servicePublicKey: 'Service Public Key',
|
|
925
|
+
servicePublicKeyDescription: 'Used for vendor communication authentication',
|
|
926
|
+
},
|
|
838
927
|
subscription: {
|
|
839
928
|
view: 'View subscription',
|
|
840
929
|
name: 'Subscription',
|