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.
Files changed (39) hide show
  1. package/api/src/crons/index.ts +11 -3
  2. package/api/src/index.ts +18 -14
  3. package/api/src/libs/adapters/launcher-adapter.ts +177 -0
  4. package/api/src/libs/env.ts +7 -0
  5. package/api/src/libs/url.ts +77 -0
  6. package/api/src/libs/vendor-adapter-factory.ts +22 -0
  7. package/api/src/libs/vendor-adapter.ts +109 -0
  8. package/api/src/libs/vendor-fulfillment.ts +321 -0
  9. package/api/src/queues/payment.ts +14 -10
  10. package/api/src/queues/payout.ts +1 -0
  11. package/api/src/queues/vendor/vendor-commission.ts +192 -0
  12. package/api/src/queues/vendor/vendor-fulfillment-coordinator.ts +627 -0
  13. package/api/src/queues/vendor/vendor-fulfillment.ts +97 -0
  14. package/api/src/queues/vendor/vendor-status-check.ts +179 -0
  15. package/api/src/routes/checkout-sessions.ts +3 -0
  16. package/api/src/routes/index.ts +2 -0
  17. package/api/src/routes/products.ts +72 -1
  18. package/api/src/routes/vendor.ts +526 -0
  19. package/api/src/store/migrations/20250820-add-product-vendor.ts +102 -0
  20. package/api/src/store/migrations/20250822-add-vendor-config-to-products.ts +56 -0
  21. package/api/src/store/models/checkout-session.ts +84 -18
  22. package/api/src/store/models/index.ts +3 -0
  23. package/api/src/store/models/payout.ts +11 -0
  24. package/api/src/store/models/product-vendor.ts +118 -0
  25. package/api/src/store/models/product.ts +15 -0
  26. package/blocklet.yml +8 -2
  27. package/doc/vendor_fulfillment_system.md +931 -0
  28. package/package.json +5 -4
  29. package/src/components/collapse.tsx +1 -0
  30. package/src/components/product/edit.tsx +9 -0
  31. package/src/components/product/form.tsx +11 -0
  32. package/src/components/product/vendor-config.tsx +249 -0
  33. package/src/components/vendor/actions.tsx +145 -0
  34. package/src/locales/en.tsx +89 -0
  35. package/src/locales/zh.tsx +89 -0
  36. package/src/pages/admin/products/index.tsx +11 -1
  37. package/src/pages/admin/products/products/detail.tsx +79 -2
  38. package/src/pages/admin/products/vendors/create.tsx +418 -0
  39. package/src/pages/admin/products/vendors/index.tsx +313 -0
@@ -39,6 +39,7 @@ export default flat({
39
39
  advancedFeatures: '高级功能',
40
40
  quickStarts: '快速入门',
41
41
  copy: '复制',
42
+ copied: '已复制',
42
43
  copySuccess: '复制成功',
43
44
  copyFailed: '复制失败',
44
45
  copyTip: '请手动复制',
@@ -97,6 +98,13 @@ export default flat({
97
98
  overview: '总览',
98
99
  payments: '支付管理',
99
100
  connections: '连接',
101
+ back: '返回',
102
+ actions: '操作',
103
+ create: '创建',
104
+ creating: '创建中...',
105
+ save: '保存',
106
+ saving: '保存中...',
107
+ cancel: '取消',
100
108
  paymentLinks: '支付链接',
101
109
  paymentMethods: '支付方式',
102
110
  customers: '客户管理',
@@ -104,6 +112,7 @@ export default flat({
104
112
  coupons: '优惠券',
105
113
  pricing: '定价',
106
114
  pricingTables: '定价表',
115
+ vendors: '供应商',
107
116
  billing: '订阅和账单',
108
117
  invoices: '账单',
109
118
  subscriptions: '订阅',
@@ -392,6 +401,25 @@ export default flat({
392
401
  credit: 'Credit',
393
402
  good: '商品',
394
403
  },
404
+ vendorConfig: {
405
+ title: '供应商配置',
406
+ add: '添加供应商',
407
+ empty: '未配置供应商。点击"添加供应商"来配置供应商服务。',
408
+ vendor: '供应商',
409
+ vendorRequired: '供应商是必填项',
410
+ productCode: '产品代码',
411
+ productCodeRequired: '产品代码是必填项',
412
+ commissionType: '分成类型',
413
+ commissionRate: '分成比例',
414
+ commissionRateRequired: '分成比例是必填项',
415
+ commissionRateMin: '分成比例必须大于或等于0',
416
+ commissionRateMax: '分成比例不能超过100%',
417
+ amount: '固定金额',
418
+ noVendor: '无供应商可选',
419
+ amountRequired: '固定金额是必填项',
420
+ totalCommission: '总分成比例:{total}%',
421
+ totalCommissionExceeded: '总分成比例超过100%:{total}%',
422
+ },
395
423
  },
396
424
  price: {
397
425
  name: '价格',
@@ -813,6 +841,67 @@ export default flat({
813
841
  success: '账单作废成功',
814
842
  },
815
843
  },
844
+ vendor: {
845
+ create: '创建供应商',
846
+ edit: '编辑供应商',
847
+ delete: '删除供应商',
848
+ save: '保存供应商',
849
+ saved: '供应商保存成功',
850
+ test: '测试',
851
+ testConnection: '测试连接',
852
+ testSuccess: '连接测试成功',
853
+ testAfterSave: '供应商保存成功。您可以测试连接以验证配置是否正确。',
854
+ testToEnable: '供应商保存成功。请测试连接以启用供应商。',
855
+ enabled: '已启用',
856
+ disabled: '未启用',
857
+ addressCheckFailed: '地址检测失败,启用状态下无法保存',
858
+ updateSuccess: '供应商更新成功',
859
+ testFailed: '连接测试失败 (状态码: {status})',
860
+ testError: '连接测试失败 - 网络错误',
861
+ vendorKeyInvalid: '供应商类型只能包含小写字母、数字和下划线',
862
+ deleteTitle: '删除供应商',
863
+ deleteContent: '确定要删除供应商 "{name}" 吗?此操作无法撤销。',
864
+ deleteTip: '确定要删除供应商 "{name}" 吗?此操作无法撤销。',
865
+ activate: '激活供应商',
866
+ activateTip: '确定要激活供应商 "{name}" 吗?',
867
+ deactivate: '停用供应商',
868
+ deactivateTip: '确定要停用供应商 "{name}" 吗?',
869
+ name: '供应商名称',
870
+ nameRequired: '供应商名称是必填项',
871
+ vendorKey: '供应商类型',
872
+ vendorKeyRequired: '供应商类型是必填项',
873
+ vendorKeyHelp: '供应商的唯一标识符(如:launcher、did_names)',
874
+ description: '描述',
875
+ appUrl: '应用地址',
876
+ appUrlRequired: '应用地址是必填项',
877
+ appUrlInvalid: '请输入以http://或https://开头的有效URL',
878
+ appUrlHelp: '供应商应用的基础URL',
879
+ webhookPath: 'Webhook路径',
880
+ webhookPathInvalid: '请输入以/开头的有效路径',
881
+ webhookPathHelp: '可选的webhook回调路径(如:/webhooks/status)',
882
+ blockletMetaUrl: 'Blocklet元数据URL',
883
+ blockletMetaUrlRequired: 'Blocklet元数据URL是必填项',
884
+ blockletMetaUrlInvalid: '请输入有效的URL,以http://或https://开头',
885
+ blockletMetaUrlHelp: '必填的blocklet元数据URL,用于获取应用信息',
886
+ commission: '分成',
887
+ commissionRate: '分成比例',
888
+ commissionRateRequired: '分成比例是必填项',
889
+ commissionRateMin: '分成比例必须大于或等于0',
890
+ commissionRateMax: '分成比例过高',
891
+ commissionType: '分成类型',
892
+ commissionRateHelp: '分成比例,以百分比形式',
893
+ commissionAmountHelp: '固定分成金额',
894
+ percentage: '百分比',
895
+ fixedAmount: '固定金额',
896
+ active: '启用',
897
+ inactive: '禁用',
898
+ basicInfo: '基本信息',
899
+ apiConfig: 'API配置',
900
+ commissionConfig: '分成配置',
901
+ status: '状态',
902
+ servicePublicKey: '服务公钥',
903
+ servicePublicKeyDescription: '用于供应商通信认证',
904
+ },
816
905
  subscription: {
817
906
  view: '查看订阅',
818
907
  name: '订阅',
@@ -13,12 +13,14 @@ const PaymentLinkCreate = React.lazy(() => import('./links/create'));
13
13
  const PaymentLinkDetail = React.lazy(() => import('./links/detail'));
14
14
  const PricingTableCreate = React.lazy(() => import('./pricing-tables/create'));
15
15
  const PricingTableDetail = React.lazy(() => import('./pricing-tables/detail'));
16
+ const VendorCreate = React.lazy(() => import('./vendors/create'));
16
17
 
17
18
  const pages = {
18
19
  products: React.lazy(() => import('./products')),
19
20
  links: React.lazy(() => import('./links')),
20
21
  'pricing-tables': React.lazy(() => import('./pricing-tables')),
21
22
  passports: React.lazy(() => import('./passports')),
23
+ vendors: React.lazy(() => import('./vendors')),
22
24
  // coupons: React.lazy(() => import('./coupons')),
23
25
  };
24
26
 
@@ -47,11 +49,17 @@ export default function Products() {
47
49
 
48
50
  // @ts-ignore
49
51
  const TabComponent = pages[page] || pages.products;
52
+
53
+ if (page.startsWith('pv_')) {
54
+ // 供应商详情页面暂时跳转到列表
55
+ return <TabComponent />;
56
+ }
50
57
  const tabs = [
51
58
  { label: t('admin.products'), value: 'products' },
52
59
  { label: t('admin.paymentLinks'), value: 'links' },
53
60
  { label: t('admin.pricingTables'), value: 'pricing-tables' },
54
61
  { label: t('admin.passports'), value: 'passports' },
62
+ { label: t('admin.vendors'), value: 'vendors' },
55
63
  // { label: t('admin.coupons'), value: 'coupons' },
56
64
  ];
57
65
 
@@ -62,6 +70,8 @@ export default function Products() {
62
70
  extra = <PaymentLinkCreate />;
63
71
  } else if (page === 'pricing-tables') {
64
72
  extra = <PricingTableCreate />;
73
+ } else if (page === 'vendors') {
74
+ extra = <VendorCreate open={createProduct} onClose={() => setCreateProduct(false)} />;
65
75
  }
66
76
 
67
77
  return (
@@ -105,7 +115,7 @@ export default function Products() {
105
115
  },
106
116
  }}
107
117
  />
108
- <Box> {extra}</Box>
118
+ <Box>{extra}</Box>
109
119
  </Stack>
110
120
  {isValidElement(TabComponent) ? TabComponent : <TabComponent />}
111
121
  </>
@@ -37,7 +37,7 @@ import { formatProductPrice, goBackOrFallback } from '../../../../libs/util';
37
37
  import PricesList from '../prices/list';
38
38
 
39
39
  const getProduct = (id: string): Promise<TProductExpanded> => {
40
- return api.get(`/api/products/${id}`).then((res) => res.data);
40
+ return api.get(`/api/products/${id}`).then((res: any) => res.data);
41
41
  };
42
42
 
43
43
  export default function ProductDetail(props: { id: string }) {
@@ -77,7 +77,7 @@ export default function ProductDetail(props: { id: string }) {
77
77
  const createProductUpdater = (key: string) => async (updates: TProduct) => {
78
78
  try {
79
79
  setState((prev) => ({ loading: { ...prev.loading, [key]: true } }));
80
- await api.put(`/api/products/${props.id}`, updates).then((res) => res.data);
80
+ await api.put(`/api/products/${props.id}`, updates).then((res: any) => res.data);
81
81
  Toast.success(t('common.saved'));
82
82
  dispatch('product.updated');
83
83
  runAsync();
@@ -389,6 +389,83 @@ export default function ProductDetail(props: { id: string }) {
389
389
  </Box>
390
390
  </Box>
391
391
  <Divider />
392
+ {/* 供应商配置展示 */}
393
+ {data.type === 'service' && (
394
+ <>
395
+ <Box className="section">
396
+ <SectionHeader title={t('admin.product.vendorConfig.title')}>
397
+ <Button
398
+ variant="text"
399
+ color="inherit"
400
+ size="small"
401
+ sx={{ color: 'text.link' }}
402
+ onClick={() => setState((prev) => ({ editing: { ...prev.editing, product: true } }))}>
403
+ {t('common.edit')}
404
+ </Button>
405
+ </SectionHeader>
406
+ <Box className="section-body">
407
+ {data.vendor_config && data.vendor_config.length > 0 ? (
408
+ <Stack spacing={2}>
409
+ {/* 总分成比例信息 */}
410
+ {(() => {
411
+ const totalPercentage = data.vendor_config
412
+ .filter((v: any) => v.commission_type === 'percentage')
413
+ .reduce((sum: number, v: any) => sum + (v.commission_rate || 0), 0);
414
+
415
+ return (
416
+ <Typography variant="body2" sx={{ color: 'text.secondary', mb: 1 }}>
417
+ {t('admin.product.vendorConfig.totalCommission', {
418
+ total: totalPercentage.toFixed(0),
419
+ })}
420
+ </Typography>
421
+ );
422
+ })()}
423
+ {data.vendor_config.map((vendor: any) => (
424
+ <Box
425
+ key={`vendor-${vendor.vendor_id}`}
426
+ sx={{
427
+ p: 2,
428
+ border: '1px solid',
429
+ borderColor: 'divider',
430
+ borderRadius: 1,
431
+ backgroundColor: 'background.paper',
432
+ }}>
433
+ <Stack direction="row" justifyContent="space-between" alignItems="center">
434
+ <Box>
435
+ <Typography variant="body1" sx={{ color: 'text.primary', fontWeight: 500 }}>
436
+ {vendor.name || vendor.vendor_key || vendor.vendor_id}
437
+ </Typography>
438
+ {vendor.description && (
439
+ <Typography variant="body2" sx={{ color: 'text.secondary', mt: 0.5 }}>
440
+ {vendor.description}
441
+ </Typography>
442
+ )}
443
+ </Box>
444
+ <Stack direction="row" spacing={3} alignItems="center">
445
+ <Typography variant="body2" sx={{ color: 'text.secondary' }}>
446
+ {vendor.commission_type === 'percentage'
447
+ ? t('admin.vendor.percentage')
448
+ : t('admin.vendor.fixedAmount')}
449
+ </Typography>
450
+ <Typography variant="body1" sx={{ color: 'text.primary', fontWeight: 600 }}>
451
+ {vendor.commission_rate}
452
+ {vendor.commission_type === 'percentage' ? '%' : ''}
453
+ </Typography>
454
+ </Stack>
455
+ </Stack>
456
+ </Box>
457
+ ))}
458
+ </Stack>
459
+ ) : (
460
+ <Typography variant="body2" sx={{ color: 'text.secondary', textAlign: 'center', py: 2 }}>
461
+ {t('admin.product.vendorConfig.empty')}
462
+ </Typography>
463
+ )}
464
+ </Box>
465
+ </Box>
466
+ <Divider />
467
+ </>
468
+ )}
392
469
  <Box className="section">
393
470
  <SectionHeader title={t('admin.events.title')} />
394
471
  <Box className="section-body">
@@ -0,0 +1,418 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import Toast from '@arcblock/ux/lib/Toast';
3
+ import { api, formatError } from '@blocklet/payment-react';
4
+ import { AddOutlined } from '@mui/icons-material';
5
+ import {
6
+ Button,
7
+ CircularProgress,
8
+ Stack,
9
+ TextField,
10
+ FormControlLabel,
11
+ Switch,
12
+ FormControl,
13
+ InputLabel,
14
+ Select,
15
+ MenuItem,
16
+ } from '@mui/material';
17
+ import { useState } from 'react';
18
+ import { useForm, Controller } from 'react-hook-form';
19
+ import { dispatch } from 'use-bus';
20
+
21
+ import { joinURL, withQuery } from 'ufo';
22
+ import DrawerForm from '../../../../components/drawer-form';
23
+ import { formatProxyUrl } from '../../../../libs/util';
24
+
25
+ interface Vendor {
26
+ id: string;
27
+ vendor_key: string;
28
+ name: string;
29
+ description: string;
30
+ app_url: string;
31
+ webhook_path: string;
32
+ default_commission_rate: number;
33
+ default_commission_type: 'percentage' | 'fixed_amount';
34
+ status: 'active' | 'inactive';
35
+ order_create_params: Record<string, any>;
36
+ metadata: Record<string, any>;
37
+ created_at: string;
38
+ updated_at: string;
39
+ }
40
+
41
+ interface VendorCreateProps {
42
+ open?: boolean;
43
+ onClose?: () => void;
44
+ onSubmit?: () => void;
45
+ vendorData?: Vendor;
46
+ }
47
+
48
+ interface VendorFormData {
49
+ vendor_key: string;
50
+ name: string;
51
+ description: string;
52
+ app_url: string;
53
+ webhook_path: string;
54
+ blocklet_meta_url: string;
55
+ default_commission_rate: number;
56
+ default_commission_type: 'percentage' | 'fixed_amount';
57
+ status: 'active' | 'inactive';
58
+ order_create_params: Record<string, any>;
59
+ metadata: Record<string, any>;
60
+ app_pid?: string;
61
+ app_logo?: string;
62
+ }
63
+
64
+ export default function VendorCreate({
65
+ open = true,
66
+ onClose = () => {},
67
+ onSubmit: onSubmitCallback = () => {},
68
+ vendorData = undefined,
69
+ }: VendorCreateProps) {
70
+ const { t } = useLocaleContext();
71
+ const [loading, setLoading] = useState(false);
72
+
73
+ const isEditMode = !!vendorData;
74
+
75
+ const {
76
+ control,
77
+ handleSubmit,
78
+ reset,
79
+ formState: { errors, isValid },
80
+ watch,
81
+ } = useForm<VendorFormData>({
82
+ mode: 'onChange',
83
+ defaultValues: vendorData
84
+ ? {
85
+ ...vendorData,
86
+ default_commission_rate: vendorData.default_commission_rate,
87
+ blocklet_meta_url: vendorData.metadata?.blockletMetaUrl || '',
88
+ }
89
+ : {
90
+ vendor_key: '',
91
+ name: '',
92
+ description: '',
93
+ app_url: '',
94
+ webhook_path: '',
95
+ blocklet_meta_url: '',
96
+ default_commission_rate: 80,
97
+ default_commission_type: 'percentage',
98
+ status: 'inactive',
99
+ order_create_params: {},
100
+ metadata: {},
101
+ app_pid: '',
102
+ app_logo: '',
103
+ },
104
+ });
105
+
106
+ const watchedValues = watch();
107
+
108
+ const isPercentage = watchedValues.default_commission_type === 'percentage';
109
+
110
+ const validateUrl = (url: string) => {
111
+ if (!url) return t('admin.vendor.appUrlRequired');
112
+ try {
113
+ // eslint-disable-next-line no-new
114
+ new URL(url);
115
+ return true;
116
+ } catch {
117
+ return t('admin.vendor.appUrlInvalid');
118
+ }
119
+ };
120
+
121
+ const validateWebhookPath = (path: string) => {
122
+ if (!path) return true;
123
+ if (!path.startsWith('/')) {
124
+ return t('admin.vendor.webhookPathInvalid');
125
+ }
126
+ return true;
127
+ };
128
+
129
+ const validateBlockletMetaUrl = (url: string) => {
130
+ if (!url) return t('admin.vendor.blockletMetaUrlRequired'); // 必填字段
131
+ try {
132
+ // eslint-disable-next-line no-new
133
+ new URL(url);
134
+ return true;
135
+ } catch {
136
+ return t('admin.vendor.blockletMetaUrlInvalid');
137
+ }
138
+ };
139
+
140
+ const onSubmit = async (data: VendorFormData) => {
141
+ try {
142
+ setLoading(true);
143
+
144
+ // 轻量级校验:检查URL格式
145
+ try {
146
+ // eslint-disable-next-line no-new
147
+ new URL(data.app_url);
148
+ } catch {
149
+ Toast.error(t('admin.vendor.appUrlInvalid'));
150
+ setLoading(false);
151
+ return;
152
+ }
153
+
154
+ // 准备提交数据,将 blocklet_meta_url 放入 metadata
155
+ const { blocklet_meta_url: blockletMetaUrl, ...restData } = data;
156
+ const submitData = {
157
+ ...restData,
158
+ metadata: {
159
+ ...data.metadata,
160
+ ...(blockletMetaUrl && { blockletMetaUrl }),
161
+ },
162
+ };
163
+
164
+ // 如果状态为启用,则检测应用地址可用性
165
+ if (submitData.status === 'active') {
166
+ try {
167
+ const response = await fetch(
168
+ formatProxyUrl(withQuery(joinURL(submitData.app_url, '__blocklet__.js'), { type: 'json' })),
169
+ {
170
+ method: 'GET',
171
+ headers: { 'Content-Type': 'application/json' },
172
+ }
173
+ );
174
+
175
+ if (!response.ok) {
176
+ Toast.error(t('admin.vendor.addressCheckFailed'));
177
+ setLoading(false);
178
+ return;
179
+ }
180
+
181
+ // 从响应中获取appPid和appLogo
182
+ const blockletInfo = await response.json();
183
+ if (blockletInfo) {
184
+ submitData.app_pid = blockletInfo.pid || blockletInfo.appPid;
185
+ submitData.app_logo = blockletInfo.logo || blockletInfo.appLogo;
186
+ }
187
+ } catch (error) {
188
+ Toast.error(t('admin.vendor.addressCheckFailed'));
189
+ setLoading(false);
190
+ return;
191
+ }
192
+ }
193
+
194
+ if (isEditMode && vendorData) {
195
+ // 编辑模式:更新供应商
196
+ await api.put(`/api/vendors/${vendorData.id}`, submitData);
197
+ Toast.success(t('admin.vendor.updateSuccess'));
198
+ dispatch('vendor.updated');
199
+ } else {
200
+ // 创建模式:新建供应商
201
+ await api.post('/api/vendors', submitData);
202
+ Toast.success(t('admin.vendor.saved'));
203
+ dispatch('vendor.created');
204
+ }
205
+
206
+ reset();
207
+ dispatch('drawer.submitted');
208
+ if (onSubmitCallback) {
209
+ onSubmitCallback();
210
+ }
211
+ if (onClose) {
212
+ onClose();
213
+ }
214
+ } catch (error) {
215
+ console.error('Failed to create vendor:', error);
216
+ Toast.error(formatError(error));
217
+ } finally {
218
+ setLoading(false);
219
+ }
220
+ };
221
+
222
+ const handleClose = () => {
223
+ reset();
224
+ if (onClose) {
225
+ onClose();
226
+ }
227
+ };
228
+
229
+ return (
230
+ <DrawerForm
231
+ icon={isEditMode ? null : <AddOutlined />}
232
+ text={isEditMode ? t('admin.vendor.edit') : t('admin.vendor.create')}
233
+ open={open}
234
+ onClose={handleClose}
235
+ width={800}
236
+ addons={
237
+ <Button variant="contained" size="small" onClick={handleSubmit(onSubmit)} disabled={loading || !isValid}>
238
+ {loading && <CircularProgress size="small" sx={{ mr: 1 }} />}
239
+ {isEditMode ? t('admin.save') : t('admin.vendor.save')}
240
+ </Button>
241
+ }>
242
+ <Stack spacing={3}>
243
+ <Stack direction="row" spacing={2}>
244
+ <Controller
245
+ name="vendor_key"
246
+ control={control}
247
+ rules={{
248
+ required: t('admin.vendor.vendorKeyRequired'),
249
+ pattern: {
250
+ value: /^[a-z0-9_]+$/,
251
+ message: t('admin.vendor.vendorKeyInvalid'),
252
+ },
253
+ }}
254
+ render={({ field }) => (
255
+ <TextField
256
+ {...field}
257
+ label={t('admin.vendor.vendorKey')}
258
+ required
259
+ fullWidth
260
+ disabled={isEditMode}
261
+ error={!!errors.vendor_key}
262
+ helperText={errors.vendor_key?.message || t('admin.vendor.vendorKeyHelp')}
263
+ />
264
+ )}
265
+ />
266
+ <Controller
267
+ name="name"
268
+ control={control}
269
+ rules={{
270
+ required: t('admin.vendor.nameRequired'),
271
+ }}
272
+ render={({ field }) => (
273
+ <TextField
274
+ {...field}
275
+ label={t('admin.vendor.name')}
276
+ required
277
+ fullWidth
278
+ error={!!errors.name}
279
+ helperText={errors.name?.message}
280
+ />
281
+ )}
282
+ />
283
+ </Stack>
284
+
285
+ <Controller
286
+ name="description"
287
+ control={control}
288
+ render={({ field }) => (
289
+ <TextField {...field} label={t('admin.vendor.description')} multiline rows={3} fullWidth />
290
+ )}
291
+ />
292
+
293
+ <Controller
294
+ name="app_url"
295
+ control={control}
296
+ rules={{
297
+ required: t('admin.vendor.appUrlRequired'),
298
+ validate: validateUrl,
299
+ }}
300
+ render={({ field }) => (
301
+ <TextField
302
+ {...field}
303
+ label={t('admin.vendor.appUrl')}
304
+ required
305
+ fullWidth
306
+ error={!!errors.app_url}
307
+ helperText={errors.app_url?.message || t('admin.vendor.appUrlHelp')}
308
+ />
309
+ )}
310
+ />
311
+
312
+ <Controller
313
+ name="webhook_path"
314
+ control={control}
315
+ rules={{
316
+ validate: validateWebhookPath,
317
+ }}
318
+ render={({ field }) => (
319
+ <TextField
320
+ {...field}
321
+ label={t('admin.vendor.webhookPath')}
322
+ fullWidth
323
+ error={!!errors.webhook_path}
324
+ helperText={errors.webhook_path?.message || t('admin.vendor.webhookPathHelp')}
325
+ />
326
+ )}
327
+ />
328
+
329
+ <Controller
330
+ name="blocklet_meta_url"
331
+ control={control}
332
+ rules={{
333
+ required: t('admin.vendor.blockletMetaUrlRequired'),
334
+ validate: validateBlockletMetaUrl,
335
+ }}
336
+ render={({ field }) => (
337
+ <TextField
338
+ {...field}
339
+ label={t('admin.vendor.blockletMetaUrl')}
340
+ required
341
+ fullWidth
342
+ error={!!errors.blocklet_meta_url}
343
+ helperText={errors.blocklet_meta_url?.message || t('admin.vendor.blockletMetaUrlHelp')}
344
+ />
345
+ )}
346
+ />
347
+
348
+ <Stack direction="row" spacing={0} sx={{ width: '30%' }}>
349
+ {/* 分成类型 - 隐藏,固定为比例分成 */}
350
+ <Controller
351
+ name="default_commission_type"
352
+ control={control}
353
+ render={({ field }) => (
354
+ <FormControl fullWidth sx={{ display: 'none' }}>
355
+ <InputLabel>{t('admin.vendor.commissionType')}</InputLabel>
356
+ <Select {...field} label={t('admin.vendor.commissionType')}>
357
+ <MenuItem value="percentage">{t('admin.vendor.percentage')}</MenuItem>
358
+ <MenuItem value="fixed_amount">{t('admin.vendor.fixedAmount')}</MenuItem>
359
+ </Select>
360
+ </FormControl>
361
+ )}
362
+ />
363
+ <Controller
364
+ name="default_commission_rate"
365
+ control={control}
366
+ rules={{
367
+ required: t('admin.vendor.commissionRateRequired'),
368
+ min: {
369
+ value: 0,
370
+ message: t('admin.vendor.commissionRateMin'),
371
+ },
372
+ max: {
373
+ value: isPercentage ? 100 : 999999,
374
+ message: t('admin.vendor.commissionRateMax'),
375
+ },
376
+ }}
377
+ render={({ field }) => (
378
+ <TextField
379
+ {...field}
380
+ label={`${t('admin.vendor.commissionRate')} ${isPercentage ? '(%)' : ''}`}
381
+ type="number"
382
+ required
383
+ fullWidth
384
+ placeholder="20"
385
+ error={!!errors.default_commission_rate}
386
+ helperText={
387
+ errors.default_commission_rate?.message ||
388
+ (isPercentage ? t('admin.vendor.commissionRateHelp') : t('admin.vendor.commissionAmountHelp'))
389
+ }
390
+ inputProps={{
391
+ min: 0,
392
+ max: isPercentage ? 100 : 999999,
393
+ step: isPercentage ? 10 : 1,
394
+ }}
395
+ />
396
+ )}
397
+ />
398
+ </Stack>
399
+
400
+ <Controller
401
+ name="status"
402
+ control={control}
403
+ render={({ field }) => (
404
+ <FormControlLabel
405
+ control={
406
+ <Switch
407
+ checked={field.value === 'active'}
408
+ onChange={(e) => field.onChange(e.target.checked ? 'active' : 'inactive')}
409
+ />
410
+ }
411
+ label={watchedValues.status === 'active' ? t('admin.vendor.enabled') : t('admin.vendor.disabled')}
412
+ />
413
+ )}
414
+ />
415
+ </Stack>
416
+ </DrawerForm>
417
+ );
418
+ }