openxiangda-cli 2.0.0-alpha.89 → 2.0.0-alpha.90
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/README.md +2 -2
- package/bin/run.js +1 -0
- package/dist/commands/skill.d.ts +26 -0
- package/dist/commands/skill.d.ts.map +1 -0
- package/dist/commands/skill.js +43 -0
- package/dist/commands/skill.js.map +1 -0
- package/dist/create-workspace.d.ts.map +1 -1
- package/dist/create-workspace.js +3 -3
- package/dist/create-workspace.js.map +1 -1
- package/dist/toolchain-capsule.d.ts.map +1 -1
- package/dist/toolchain-capsule.js +4 -8
- package/dist/toolchain-capsule.js.map +1 -1
- package/package.json +6 -7
- package/template/AGENTS.md +2 -1
- package/template/README.md +3 -3
- package/template/apps/server/package.json +1 -1
- package/template/apps/server/src/app.module.ts +1 -1
- package/template/apps/server/src/main.ts +1 -1
- package/template/apps/web/e2e/client-fixture.ts +5 -0
- package/template/apps/web/e2e/data-api-live-fixture.tsx +19 -14
- package/template/apps/web/e2e/field-protocol-fixture.tsx +87 -20
- package/template/apps/web/e2e/field-protocol.spec.ts +86 -0
- package/template/apps/web/e2e/resources.spec.ts +20 -181
- package/template/apps/web/field-protocol.e2e.html +3 -0
- package/template/apps/web/package.json +2 -5
- package/template/apps/web/src/main.tsx +12 -60
- package/template/apps/web/test/contracts.test.ts +52 -104
- package/template/openxiangda.config.ts +1 -1
- package/template/package.json +1 -3
- package/template/packages/contracts/src/generated.ts +1 -0
- package/template/scripts/verify-template-budget.mjs +2 -2
- package/template/apps/web/src/AuthoritativeSelector.tsx +0 -661
- package/template/apps/web/src/FilePreviewPage.tsx +0 -324
- package/template/apps/web/src/RoleSubjectSelect.tsx +0 -128
- package/template/apps/web/src/Shell.tsx +0 -445
- package/template/apps/web/src/appearance.tsx +0 -147
- package/template/apps/web/src/components/platform-fields/AddressField.tsx +0 -338
- package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +0 -245
- package/template/apps/web/src/components/platform-fields/CascadeField.tsx +0 -49
- package/template/apps/web/src/components/platform-fields/DateTimeField.tsx +0 -148
- package/template/apps/web/src/components/platform-fields/JsonField.tsx +0 -77
- package/template/apps/web/src/components/platform-fields/LocationField.tsx +0 -164
- package/template/apps/web/src/components/platform-fields/PlatformDirectoryPicker.tsx +0 -673
- package/template/apps/web/src/components/platform-fields/ResourceReferenceField.tsx +0 -67
- package/template/apps/web/src/components/platform-fields/RichTextField.tsx +0 -196
- package/template/apps/web/src/components/platform-fields/SignatureField.tsx +0 -315
- package/template/apps/web/src/components/platform-fields/SubtableField.tsx +0 -553
- package/template/apps/web/src/components/platform-fields/address-value.ts +0 -80
- package/template/apps/web/src/components/platform-fields/cascade-value.ts +0 -66
- package/template/apps/web/src/components/platform-fields/directory-value.ts +0 -53
- package/template/apps/web/src/components/platform-fields/field-form-codec.ts +0 -98
- package/template/apps/web/src/components/platform-fields/location-value.ts +0 -87
- package/template/apps/web/src/components/platform-fields/resource-query.ts +0 -131
- package/template/apps/web/src/components/platform-fields/rich-text-value.ts +0 -59
- package/template/apps/web/src/components/platform-fields/subtable-value.ts +0 -187
- package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +0 -1184
- package/template/apps/web/src/components/resource/ResourceBatchActions.tsx +0 -326
- package/template/apps/web/src/components/resource/StandardResourcePages.tsx +0 -392
- package/template/apps/web/src/components/resource/SurfaceFields.tsx +0 -915
- package/template/apps/web/src/components/resource/generated-resource-definition.ts +0 -18
- package/template/apps/web/src/components/resource/resource-import.ts +0 -205
- package/template/apps/web/src/data-provider.ts +0 -72
- package/template/apps/web/src/platform-client.ts +0 -708
- package/template/apps/web/src/runtime-meta.ts +0 -75
- package/template/apps/web/src/runtime.tsx +0 -227
- package/template/apps/web/src/styles.css +0 -1941
- package/template/apps/web/test/address-field.test.ts +0 -66
- package/template/apps/web/test/cascade-value.test.ts +0 -68
- package/template/apps/web/test/directory-value.test.ts +0 -55
- package/template/apps/web/test/field-bounds.test.ts +0 -17
- package/template/apps/web/test/field-form-codec.test.ts +0 -85
- package/template/apps/web/test/location-field.test.ts +0 -83
- package/template/apps/web/test/resource-import.test.ts +0 -143
- package/template/apps/web/test/resource-query.test.ts +0 -144
- package/template/apps/web/test/rich-json-field.test.ts +0 -53
- package/template/apps/web/test/signature-serial-field.test.ts +0 -40
- package/template/apps/web/test/subtable-value.test.ts +0 -144
|
@@ -1,915 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
PictureOutlined,
|
|
3
|
-
ReloadOutlined,
|
|
4
|
-
UploadOutlined,
|
|
5
|
-
} from '@ant-design/icons';
|
|
6
|
-
import {
|
|
7
|
-
Alert,
|
|
8
|
-
Button,
|
|
9
|
-
Checkbox,
|
|
10
|
-
DatePicker,
|
|
11
|
-
Form,
|
|
12
|
-
Input,
|
|
13
|
-
InputNumber,
|
|
14
|
-
Radio,
|
|
15
|
-
Select,
|
|
16
|
-
Space,
|
|
17
|
-
Switch,
|
|
18
|
-
Tag,
|
|
19
|
-
Upload,
|
|
20
|
-
} from 'antd';
|
|
21
|
-
import type {
|
|
22
|
-
DataFieldSurface,
|
|
23
|
-
DataFileRef,
|
|
24
|
-
DepartmentReferenceValue,
|
|
25
|
-
LabeledValue,
|
|
26
|
-
ResourceReferenceValue,
|
|
27
|
-
StableAddressValue,
|
|
28
|
-
StableLocationValue,
|
|
29
|
-
StableSignatureValue,
|
|
30
|
-
UserReferenceValue,
|
|
31
|
-
} from 'openxiangda-contracts/browser';
|
|
32
|
-
import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
|
33
|
-
import {
|
|
34
|
-
AuthoritativeSelector,
|
|
35
|
-
ResolvedValueText,
|
|
36
|
-
} from '../../AuthoritativeSelector';
|
|
37
|
-
import { AddressField, AddressValueDisplay } from '../platform-fields/AddressField';
|
|
38
|
-
import { AttachmentFileList, formatManagedFileSize } from '../platform-fields/AttachmentFileList';
|
|
39
|
-
import { CascadeField, CascadeValueDisplay } from '../platform-fields/CascadeField';
|
|
40
|
-
import type { CascadeStoredValue } from '../platform-fields/cascade-value';
|
|
41
|
-
import { DateTimeField, DateTimeFilter, DateTimeValueDisplay } from '../platform-fields/DateTimeField';
|
|
42
|
-
import { JsonField, JsonValueDisplay } from '../platform-fields/JsonField';
|
|
43
|
-
import { LocationField, LocationValueDisplay } from '../platform-fields/LocationField';
|
|
44
|
-
import { PlatformDirectoryPicker } from '../platform-fields/PlatformDirectoryPicker';
|
|
45
|
-
import { ResourceReferenceField } from '../platform-fields/ResourceReferenceField';
|
|
46
|
-
import { RichTextField, RichTextValueDisplay } from '../platform-fields/RichTextField';
|
|
47
|
-
import { SignatureField, SignatureValueDisplay } from '../platform-fields/SignatureField';
|
|
48
|
-
|
|
49
|
-
export type SurfaceField = DataFieldSurface & { key: string };
|
|
50
|
-
|
|
51
|
-
export function listSurfaceFields(fields: Record<string, DataFieldSurface>) {
|
|
52
|
-
return Object.entries(fields)
|
|
53
|
-
.filter(([, field]) => field.list)
|
|
54
|
-
.map(([key, field]) => ({ key, ...field }));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface SurfaceFieldEditContext {
|
|
58
|
-
field: SurfaceField;
|
|
59
|
-
disabled: boolean;
|
|
60
|
-
operation: 'create' | 'update';
|
|
61
|
-
resourceCode?: string;
|
|
62
|
-
recordId?: string;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export interface SurfaceFieldValueContext {
|
|
66
|
-
field: SurfaceField;
|
|
67
|
-
value: unknown;
|
|
68
|
-
resourceCode?: string;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export interface SurfaceFieldRenderers {
|
|
72
|
-
renderExtra?: (context: SurfaceFieldEditContext) => ReactNode;
|
|
73
|
-
renderScope?: (context: SurfaceFieldEditContext) => ReactNode;
|
|
74
|
-
renderSubtable?: (context: SurfaceFieldEditContext) => ReactNode;
|
|
75
|
-
renderFilter?: (context: SurfaceFilterContext) => ReactNode;
|
|
76
|
-
upload?: (
|
|
77
|
-
field: SurfaceField,
|
|
78
|
-
file: File,
|
|
79
|
-
recordId?: string
|
|
80
|
-
) => Promise<DataFileRef>;
|
|
81
|
-
signer?: UserReferenceValue;
|
|
82
|
-
renderValue?: (context: SurfaceFieldValueContext) => ReactNode | undefined;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export interface SurfaceFilterContext {
|
|
86
|
-
field: SurfaceField;
|
|
87
|
-
resourceCode?: string;
|
|
88
|
-
value: unknown;
|
|
89
|
-
onChange: (value: unknown) => void;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function numberInputProps(field: SurfaceField) {
|
|
93
|
-
const scale = field.type === 'number.integer' ? 0 : field.scale;
|
|
94
|
-
return {
|
|
95
|
-
...(scale === undefined ? {} : { precision: scale }),
|
|
96
|
-
step: scale === undefined ? 'any' : scale === 0 ? 1 : 10 ** -scale,
|
|
97
|
-
...(field.widget === 'money' ? { prefix: '¥' } : {}),
|
|
98
|
-
...(field.widget === 'percent' ? { suffix: '%' } : {}),
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** Render a standard filter control from the same field surface used by forms. */
|
|
103
|
-
export function SurfaceFilterControl({
|
|
104
|
-
field,
|
|
105
|
-
resourceCode,
|
|
106
|
-
value,
|
|
107
|
-
onChange,
|
|
108
|
-
range = false,
|
|
109
|
-
renderers,
|
|
110
|
-
}: SurfaceFilterContext & { range?: boolean; renderers?: Pick<SurfaceFieldRenderers, 'renderScope' | 'renderFilter'> }) {
|
|
111
|
-
const custom = renderers?.renderFilter?.({ field, value, onChange });
|
|
112
|
-
if (custom !== undefined) return custom;
|
|
113
|
-
const placeholder = `筛选${field.label}`;
|
|
114
|
-
switch (field.widget) {
|
|
115
|
-
case 'switch':
|
|
116
|
-
return (
|
|
117
|
-
<Select
|
|
118
|
-
allowClear
|
|
119
|
-
options={[{ label: '是', value: true }, { label: '否', value: false }]}
|
|
120
|
-
placeholder={placeholder}
|
|
121
|
-
value={value}
|
|
122
|
-
onChange={onChange}
|
|
123
|
-
/>
|
|
124
|
-
);
|
|
125
|
-
case 'select':
|
|
126
|
-
case 'multi-select':
|
|
127
|
-
return (
|
|
128
|
-
<Select
|
|
129
|
-
allowClear
|
|
130
|
-
mode={field.widget === 'multi-select' ? 'multiple' : undefined}
|
|
131
|
-
options={field.options}
|
|
132
|
-
placeholder={placeholder}
|
|
133
|
-
value={value}
|
|
134
|
-
onChange={onChange}
|
|
135
|
-
/>
|
|
136
|
-
);
|
|
137
|
-
case 'date':
|
|
138
|
-
case 'time':
|
|
139
|
-
case 'datetime':
|
|
140
|
-
case 'date-range':
|
|
141
|
-
case 'datetime-range':
|
|
142
|
-
return <DateTimeFilter field={field} onChange={onChange} value={value} />;
|
|
143
|
-
case 'number':
|
|
144
|
-
case 'money':
|
|
145
|
-
case 'percent':
|
|
146
|
-
if (range) {
|
|
147
|
-
const values = Array.isArray(value) ? value as [number | undefined, number | undefined] : [];
|
|
148
|
-
return (
|
|
149
|
-
<Space.Compact>
|
|
150
|
-
<InputNumber {...numberInputProps(field)} onChange={next => onChange([next === null ? undefined : Number(next), values[1]])} placeholder={`最低${field.label}`} value={values[0]} />
|
|
151
|
-
<InputNumber {...numberInputProps(field)} onChange={next => onChange([values[0], next === null ? undefined : Number(next)])} placeholder={`最高${field.label}`} value={values[1]} />
|
|
152
|
-
</Space.Compact>
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
return <InputNumber {...numberInputProps(field)} onChange={onChange} placeholder={placeholder} value={value as number | null | undefined} />;
|
|
156
|
-
case 'scope':
|
|
157
|
-
return renderers?.renderScope?.({ field, disabled: false, operation: 'update' }) || <Input allowClear value={value as string | undefined} onChange={event => onChange(event.target.value)} placeholder={placeholder} />;
|
|
158
|
-
case 'directory-user':
|
|
159
|
-
case 'directory-department':
|
|
160
|
-
return <PlatformDirectoryPicker kind={field.widget === 'directory-user' ? 'user' : 'department'} multiple={field.type.endsWith('.multiple')} onChange={onChange as (value: UserReferenceValue | DepartmentReferenceValue | Array<UserReferenceValue | DepartmentReferenceValue> | undefined) => void} placeholder={placeholder} value={value as UserReferenceValue | DepartmentReferenceValue | Array<UserReferenceValue | DepartmentReferenceValue> | undefined} />;
|
|
161
|
-
case 'resource':
|
|
162
|
-
return (
|
|
163
|
-
<AuthoritativeSelector
|
|
164
|
-
fieldCode={field.key}
|
|
165
|
-
multiple={field.type === 'resource-ref.multiple'}
|
|
166
|
-
onChange={onChange}
|
|
167
|
-
operation="update"
|
|
168
|
-
placeholder={placeholder}
|
|
169
|
-
resourceCode={resourceCode}
|
|
170
|
-
source="resource"
|
|
171
|
-
value={value as ResourceReferenceValue | ResourceReferenceValue[] | undefined}
|
|
172
|
-
/>
|
|
173
|
-
);
|
|
174
|
-
case 'cascade':
|
|
175
|
-
return (
|
|
176
|
-
<CascadeField
|
|
177
|
-
field={field}
|
|
178
|
-
onChange={onChange}
|
|
179
|
-
value={value as CascadeStoredValue | undefined}
|
|
180
|
-
/>
|
|
181
|
-
);
|
|
182
|
-
case 'address':
|
|
183
|
-
return (
|
|
184
|
-
<AddressField
|
|
185
|
-
detailEnabled={false}
|
|
186
|
-
onChange={onChange}
|
|
187
|
-
value={value as StableAddressValue | undefined}
|
|
188
|
-
/>
|
|
189
|
-
);
|
|
190
|
-
case 'json':
|
|
191
|
-
return <JsonField onChange={onChange} value={value} />;
|
|
192
|
-
default:
|
|
193
|
-
return <Input allowClear value={value as string | undefined} onChange={event => onChange(event.target.value)} placeholder={placeholder} />;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function snapshotIds(value: LabeledValue | LabeledValue[] | undefined) {
|
|
198
|
-
return (Array.isArray(value) ? value : value ? [value] : []).map(
|
|
199
|
-
item => item.value
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function StaticOptionField({
|
|
204
|
-
field,
|
|
205
|
-
value,
|
|
206
|
-
onChange,
|
|
207
|
-
disabled,
|
|
208
|
-
}: {
|
|
209
|
-
field: SurfaceField;
|
|
210
|
-
value?: LabeledValue | LabeledValue[];
|
|
211
|
-
onChange?: (value: LabeledValue | LabeledValue[] | undefined) => void;
|
|
212
|
-
disabled?: boolean;
|
|
213
|
-
}) {
|
|
214
|
-
const multiple = field.type === 'option.multiple';
|
|
215
|
-
const ids = snapshotIds(value);
|
|
216
|
-
const emit = (next: string | string[] | undefined) => {
|
|
217
|
-
const selected = (Array.isArray(next) ? next : next ? [next] : [])
|
|
218
|
-
.map(id => field.options?.find(option => option.value === id))
|
|
219
|
-
.filter((item): item is LabeledValue => Boolean(item))
|
|
220
|
-
.map(item => ({ ...item }));
|
|
221
|
-
onChange?.(multiple ? selected : selected[0]);
|
|
222
|
-
};
|
|
223
|
-
if (field.widget === 'radio') {
|
|
224
|
-
return (
|
|
225
|
-
<Radio.Group
|
|
226
|
-
disabled={disabled}
|
|
227
|
-
onChange={event => emit(event.target.value)}
|
|
228
|
-
options={field.options}
|
|
229
|
-
value={ids[0]}
|
|
230
|
-
/>
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
if (field.widget === 'checkbox') {
|
|
234
|
-
return (
|
|
235
|
-
<Checkbox.Group
|
|
236
|
-
disabled={disabled}
|
|
237
|
-
onChange={next => emit(next.map(String))}
|
|
238
|
-
options={field.options}
|
|
239
|
-
value={ids}
|
|
240
|
-
/>
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
return (
|
|
244
|
-
<Select
|
|
245
|
-
allowClear
|
|
246
|
-
disabled={disabled}
|
|
247
|
-
mode={multiple ? 'multiple' : undefined}
|
|
248
|
-
onChange={next => emit(next || undefined)}
|
|
249
|
-
options={field.options}
|
|
250
|
-
placeholder={`请选择${field.label}`}
|
|
251
|
-
value={multiple ? ids : ids[0]}
|
|
252
|
-
/>
|
|
253
|
-
);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
export function SurfaceFieldControl({
|
|
257
|
-
field,
|
|
258
|
-
disabled,
|
|
259
|
-
operation,
|
|
260
|
-
resourceCode,
|
|
261
|
-
recordId,
|
|
262
|
-
renderers,
|
|
263
|
-
}: SurfaceFieldEditContext & { renderers?: SurfaceFieldRenderers }) {
|
|
264
|
-
const rules = [
|
|
265
|
-
...(field.requiredHint
|
|
266
|
-
? [{ required: true, message: `请填写或选择${field.label}` }]
|
|
267
|
-
: []),
|
|
268
|
-
...(field.widget === 'email'
|
|
269
|
-
? [{ type: 'email' as const, message: '邮箱格式不正确' }]
|
|
270
|
-
: []),
|
|
271
|
-
...(field.widget === 'phone'
|
|
272
|
-
? [{ pattern: /^1\d{10}$/, message: '请输入 11 位手机号' }]
|
|
273
|
-
: []),
|
|
274
|
-
];
|
|
275
|
-
const common = {
|
|
276
|
-
disabled,
|
|
277
|
-
...(field.maxLength === undefined ? {} : { maxLength: field.maxLength }),
|
|
278
|
-
placeholder: `请输入${field.label}`,
|
|
279
|
-
};
|
|
280
|
-
let control: ReactNode = <Input {...common} />;
|
|
281
|
-
switch (field.widget) {
|
|
282
|
-
case 'textarea':
|
|
283
|
-
control = <Input.TextArea {...common} rows={3} />;
|
|
284
|
-
break;
|
|
285
|
-
case 'number':
|
|
286
|
-
case 'money':
|
|
287
|
-
case 'percent':
|
|
288
|
-
control = <InputNumber {...common} {...numberInputProps(field)} style={{ width: '100%' }} />;
|
|
289
|
-
break;
|
|
290
|
-
case 'date':
|
|
291
|
-
case 'time':
|
|
292
|
-
case 'datetime':
|
|
293
|
-
case 'date-range':
|
|
294
|
-
case 'datetime-range':
|
|
295
|
-
control = <DateTimeField disabled={disabled} field={field} />;
|
|
296
|
-
break;
|
|
297
|
-
case 'switch':
|
|
298
|
-
control = <Switch checkedChildren="是" disabled={disabled} unCheckedChildren="否" />;
|
|
299
|
-
break;
|
|
300
|
-
case 'select':
|
|
301
|
-
case 'multi-select':
|
|
302
|
-
case 'radio':
|
|
303
|
-
case 'checkbox':
|
|
304
|
-
control = field.source ? (
|
|
305
|
-
<ResourceReferenceField
|
|
306
|
-
disabled={disabled}
|
|
307
|
-
field={field}
|
|
308
|
-
fieldCode={field.key}
|
|
309
|
-
operation={operation}
|
|
310
|
-
resourceCode={resourceCode}
|
|
311
|
-
/>
|
|
312
|
-
) : (
|
|
313
|
-
<StaticOptionField disabled={disabled} field={field} />
|
|
314
|
-
);
|
|
315
|
-
break;
|
|
316
|
-
case 'email':
|
|
317
|
-
control = <Input {...common} type="email" />;
|
|
318
|
-
break;
|
|
319
|
-
case 'phone':
|
|
320
|
-
control = <Input {...common} type="tel" />;
|
|
321
|
-
break;
|
|
322
|
-
case 'scope':
|
|
323
|
-
control = renderers?.renderScope?.({ field, disabled, operation, recordId }) || (
|
|
324
|
-
<Input {...common} />
|
|
325
|
-
);
|
|
326
|
-
break;
|
|
327
|
-
case 'directory-user':
|
|
328
|
-
case 'directory-department':
|
|
329
|
-
control = (
|
|
330
|
-
<PlatformDirectoryPicker
|
|
331
|
-
disabled={disabled}
|
|
332
|
-
kind={field.widget === 'directory-user' ? 'user' : 'department'}
|
|
333
|
-
multiple={field.type.endsWith('.multiple')}
|
|
334
|
-
placeholder={`搜索并选择${field.label}`}
|
|
335
|
-
/>
|
|
336
|
-
);
|
|
337
|
-
break;
|
|
338
|
-
case 'resource':
|
|
339
|
-
control = (
|
|
340
|
-
<ResourceReferenceField
|
|
341
|
-
disabled={disabled}
|
|
342
|
-
field={field}
|
|
343
|
-
fieldCode={field.key}
|
|
344
|
-
operation={operation}
|
|
345
|
-
resourceCode={resourceCode}
|
|
346
|
-
/>
|
|
347
|
-
);
|
|
348
|
-
break;
|
|
349
|
-
case 'cascade':
|
|
350
|
-
control = <CascadeField disabled={disabled} field={field} />;
|
|
351
|
-
break;
|
|
352
|
-
case 'attachment':
|
|
353
|
-
case 'image':
|
|
354
|
-
control = (
|
|
355
|
-
<ManagedFileField
|
|
356
|
-
accept={field.accept}
|
|
357
|
-
disabled={disabled}
|
|
358
|
-
field={field}
|
|
359
|
-
maxCount={field.maxCount}
|
|
360
|
-
maxSizeMb={field.maxSizeMb}
|
|
361
|
-
multiple={field.type === 'file'}
|
|
362
|
-
onUpload={renderers?.upload}
|
|
363
|
-
recordId={recordId}
|
|
364
|
-
resourceCode={resourceCode}
|
|
365
|
-
/>
|
|
366
|
-
);
|
|
367
|
-
break;
|
|
368
|
-
case 'location':
|
|
369
|
-
control = <LocationField disabled={disabled} />;
|
|
370
|
-
break;
|
|
371
|
-
case 'signature':
|
|
372
|
-
control = (
|
|
373
|
-
<SignatureField
|
|
374
|
-
disabled={disabled}
|
|
375
|
-
onUpload={renderers?.upload
|
|
376
|
-
? file => renderers.upload!(field, file, recordId)
|
|
377
|
-
: undefined}
|
|
378
|
-
resourceCode={resourceCode}
|
|
379
|
-
signer={renderers?.signer}
|
|
380
|
-
/>
|
|
381
|
-
);
|
|
382
|
-
break;
|
|
383
|
-
case 'address':
|
|
384
|
-
control = <AddressField disabled={disabled} />;
|
|
385
|
-
break;
|
|
386
|
-
case 'rich-text':
|
|
387
|
-
control = (
|
|
388
|
-
<RichTextField
|
|
389
|
-
disabled={disabled}
|
|
390
|
-
onUpload={renderers?.upload
|
|
391
|
-
? file => renderers.upload!(field, file, recordId)
|
|
392
|
-
: undefined}
|
|
393
|
-
resourceCode={resourceCode}
|
|
394
|
-
/>
|
|
395
|
-
);
|
|
396
|
-
break;
|
|
397
|
-
case 'json':
|
|
398
|
-
control = <JsonField disabled={disabled} />;
|
|
399
|
-
break;
|
|
400
|
-
case 'subtable':
|
|
401
|
-
control = renderers?.renderSubtable?.({
|
|
402
|
-
field,
|
|
403
|
-
disabled,
|
|
404
|
-
operation,
|
|
405
|
-
resourceCode,
|
|
406
|
-
recordId,
|
|
407
|
-
}) || <Alert message="子表渲染器未配置" showIcon type="error" />;
|
|
408
|
-
break;
|
|
409
|
-
case 'readonly':
|
|
410
|
-
control = (
|
|
411
|
-
<Input
|
|
412
|
-
{...common}
|
|
413
|
-
placeholder={field.type === 'serial-number'
|
|
414
|
-
? '保存后由系统自动生成'
|
|
415
|
-
: '由系统生成'}
|
|
416
|
-
readOnly
|
|
417
|
-
/>
|
|
418
|
-
);
|
|
419
|
-
break;
|
|
420
|
-
case 'text':
|
|
421
|
-
default:
|
|
422
|
-
break;
|
|
423
|
-
}
|
|
424
|
-
const extra =
|
|
425
|
-
renderers?.renderExtra?.({ field, disabled, operation, recordId }) ||
|
|
426
|
-
(disabled ? '当前角色不可修改此字段' : undefined);
|
|
427
|
-
const wide = ['textarea', 'attachment', 'image', 'rich-text', 'address', 'location', 'signature', 'json', 'subtable'].includes(field.widget);
|
|
428
|
-
return (
|
|
429
|
-
<Form.Item
|
|
430
|
-
className={wide ? 'oxa-field-wide' : undefined}
|
|
431
|
-
extra={extra}
|
|
432
|
-
label={field.label}
|
|
433
|
-
name={field.key}
|
|
434
|
-
rules={rules}
|
|
435
|
-
valuePropName={field.widget === 'switch' ? 'checked' : 'value'}
|
|
436
|
-
>
|
|
437
|
-
{control}
|
|
438
|
-
</Form.Item>
|
|
439
|
-
);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
/** Mobile keeps the same field values and validation but uses native controls. */
|
|
443
|
-
export function MobileSurfaceFieldControl({
|
|
444
|
-
field,
|
|
445
|
-
disabled,
|
|
446
|
-
operation,
|
|
447
|
-
resourceCode,
|
|
448
|
-
recordId,
|
|
449
|
-
renderers,
|
|
450
|
-
}: SurfaceFieldEditContext & { renderers?: SurfaceFieldRenderers }) {
|
|
451
|
-
const rules = field.requiredHint
|
|
452
|
-
? [{ required: true, message: `请填写或选择${field.label}` }]
|
|
453
|
-
: [];
|
|
454
|
-
const inputProps = {
|
|
455
|
-
disabled,
|
|
456
|
-
className: 'oxa-mobile-input',
|
|
457
|
-
...(field.maxLength === undefined ? {} : { maxLength: field.maxLength }),
|
|
458
|
-
placeholder: `请输入${field.label}`,
|
|
459
|
-
};
|
|
460
|
-
let control: ReactNode = <input {...inputProps} />;
|
|
461
|
-
switch (field.widget) {
|
|
462
|
-
case 'textarea':
|
|
463
|
-
control = <textarea {...inputProps} rows={4} />;
|
|
464
|
-
break;
|
|
465
|
-
case 'number':
|
|
466
|
-
case 'money':
|
|
467
|
-
case 'percent':
|
|
468
|
-
control = (
|
|
469
|
-
<div className="oxa-mobile-number-field">
|
|
470
|
-
{field.widget === 'money' && <span>¥</span>}
|
|
471
|
-
<input
|
|
472
|
-
{...inputProps}
|
|
473
|
-
step={numberInputProps(field).step}
|
|
474
|
-
type="number"
|
|
475
|
-
/>
|
|
476
|
-
{field.widget === 'percent' && <span>%</span>}
|
|
477
|
-
</div>
|
|
478
|
-
);
|
|
479
|
-
break;
|
|
480
|
-
case 'date':
|
|
481
|
-
case 'time':
|
|
482
|
-
case 'datetime':
|
|
483
|
-
case 'date-range':
|
|
484
|
-
case 'datetime-range':
|
|
485
|
-
control = <DateTimeField disabled={disabled} field={field} mobile />;
|
|
486
|
-
break;
|
|
487
|
-
case 'switch':
|
|
488
|
-
control = <input disabled={disabled} type="checkbox" />;
|
|
489
|
-
break;
|
|
490
|
-
case 'select':
|
|
491
|
-
case 'multi-select':
|
|
492
|
-
case 'radio':
|
|
493
|
-
case 'checkbox':
|
|
494
|
-
control = field.source ? (
|
|
495
|
-
<ResourceReferenceField
|
|
496
|
-
disabled={disabled}
|
|
497
|
-
field={field}
|
|
498
|
-
fieldCode={field.key}
|
|
499
|
-
mobile
|
|
500
|
-
operation={operation}
|
|
501
|
-
resourceCode={resourceCode}
|
|
502
|
-
/>
|
|
503
|
-
) : (
|
|
504
|
-
<StaticOptionField disabled={disabled} field={field} />
|
|
505
|
-
);
|
|
506
|
-
break;
|
|
507
|
-
case 'email':
|
|
508
|
-
control = <input {...inputProps} type="email" />;
|
|
509
|
-
break;
|
|
510
|
-
case 'phone':
|
|
511
|
-
control = <input {...inputProps} type="tel" />;
|
|
512
|
-
break;
|
|
513
|
-
case 'scope':
|
|
514
|
-
control = renderers?.renderScope?.({ field, disabled, operation, recordId }) || (
|
|
515
|
-
<input {...inputProps} />
|
|
516
|
-
);
|
|
517
|
-
break;
|
|
518
|
-
case 'directory-user':
|
|
519
|
-
case 'directory-department':
|
|
520
|
-
control = (
|
|
521
|
-
<PlatformDirectoryPicker
|
|
522
|
-
disabled={disabled}
|
|
523
|
-
kind={field.widget === 'directory-user' ? 'user' : 'department'}
|
|
524
|
-
multiple={field.type.endsWith('.multiple')}
|
|
525
|
-
placeholder={`搜索并选择${field.label}`}
|
|
526
|
-
/>
|
|
527
|
-
);
|
|
528
|
-
break;
|
|
529
|
-
case 'resource':
|
|
530
|
-
control = (
|
|
531
|
-
<ResourceReferenceField
|
|
532
|
-
disabled={disabled}
|
|
533
|
-
field={field}
|
|
534
|
-
fieldCode={field.key}
|
|
535
|
-
mobile
|
|
536
|
-
operation={operation}
|
|
537
|
-
resourceCode={resourceCode}
|
|
538
|
-
/>
|
|
539
|
-
);
|
|
540
|
-
break;
|
|
541
|
-
case 'cascade':
|
|
542
|
-
control = <CascadeField disabled={disabled} field={field} />;
|
|
543
|
-
break;
|
|
544
|
-
case 'attachment':
|
|
545
|
-
case 'image':
|
|
546
|
-
control = (
|
|
547
|
-
<ManagedFileField
|
|
548
|
-
accept={field.accept}
|
|
549
|
-
disabled={disabled}
|
|
550
|
-
field={field}
|
|
551
|
-
maxCount={field.maxCount}
|
|
552
|
-
maxSizeMb={field.maxSizeMb}
|
|
553
|
-
multiple={field.type === 'file'}
|
|
554
|
-
mobile
|
|
555
|
-
onUpload={renderers?.upload}
|
|
556
|
-
recordId={recordId}
|
|
557
|
-
resourceCode={resourceCode}
|
|
558
|
-
/>
|
|
559
|
-
);
|
|
560
|
-
break;
|
|
561
|
-
case 'location':
|
|
562
|
-
control = <LocationField disabled={disabled} mobile />;
|
|
563
|
-
break;
|
|
564
|
-
case 'signature':
|
|
565
|
-
control = (
|
|
566
|
-
<SignatureField
|
|
567
|
-
disabled={disabled}
|
|
568
|
-
mobile
|
|
569
|
-
onUpload={renderers?.upload
|
|
570
|
-
? file => renderers.upload!(field, file, recordId)
|
|
571
|
-
: undefined}
|
|
572
|
-
resourceCode={resourceCode}
|
|
573
|
-
signer={renderers?.signer}
|
|
574
|
-
/>
|
|
575
|
-
);
|
|
576
|
-
break;
|
|
577
|
-
case 'address':
|
|
578
|
-
control = <AddressField disabled={disabled} mobile />;
|
|
579
|
-
break;
|
|
580
|
-
case 'rich-text':
|
|
581
|
-
control = (
|
|
582
|
-
<RichTextField
|
|
583
|
-
disabled={disabled}
|
|
584
|
-
mobile
|
|
585
|
-
onUpload={renderers?.upload
|
|
586
|
-
? file => renderers.upload!(field, file, recordId)
|
|
587
|
-
: undefined}
|
|
588
|
-
resourceCode={resourceCode}
|
|
589
|
-
/>
|
|
590
|
-
);
|
|
591
|
-
break;
|
|
592
|
-
case 'json':
|
|
593
|
-
control = <JsonField disabled={disabled} mobile />;
|
|
594
|
-
break;
|
|
595
|
-
case 'subtable':
|
|
596
|
-
control = renderers?.renderSubtable?.({
|
|
597
|
-
field,
|
|
598
|
-
disabled,
|
|
599
|
-
operation,
|
|
600
|
-
resourceCode,
|
|
601
|
-
recordId,
|
|
602
|
-
}) || <Alert message="子表渲染器未配置" showIcon type="error" />;
|
|
603
|
-
break;
|
|
604
|
-
case 'readonly':
|
|
605
|
-
control = (
|
|
606
|
-
<input
|
|
607
|
-
{...inputProps}
|
|
608
|
-
placeholder={field.type === 'serial-number'
|
|
609
|
-
? '保存后由系统自动生成'
|
|
610
|
-
: '由系统生成'}
|
|
611
|
-
readOnly
|
|
612
|
-
/>
|
|
613
|
-
);
|
|
614
|
-
break;
|
|
615
|
-
}
|
|
616
|
-
return (
|
|
617
|
-
<Form.Item
|
|
618
|
-
className="oxa-mobile-field"
|
|
619
|
-
extra={renderers?.renderExtra?.({ field, disabled, operation, recordId })}
|
|
620
|
-
label={field.label}
|
|
621
|
-
name={field.key}
|
|
622
|
-
rules={rules}
|
|
623
|
-
valuePropName={field.widget === 'switch' ? 'checked' : 'value'}
|
|
624
|
-
>
|
|
625
|
-
{control}
|
|
626
|
-
</Form.Item>
|
|
627
|
-
);
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
export function SurfaceFieldValue({
|
|
631
|
-
field,
|
|
632
|
-
value,
|
|
633
|
-
resourceCode,
|
|
634
|
-
renderers,
|
|
635
|
-
}: SurfaceFieldValueContext & { renderers?: SurfaceFieldRenderers }) {
|
|
636
|
-
if (value === undefined || value === null || value === '') return <>-</>;
|
|
637
|
-
const custom = renderers?.renderValue?.({ field, value });
|
|
638
|
-
if (custom !== undefined) return <>{custom}</>;
|
|
639
|
-
if (field.widget === 'directory-user' || field.widget === 'directory-department') {
|
|
640
|
-
return (
|
|
641
|
-
<ResolvedValueText
|
|
642
|
-
source={field.widget === 'directory-user' ? 'user' : 'department'}
|
|
643
|
-
value={value as never}
|
|
644
|
-
/>
|
|
645
|
-
);
|
|
646
|
-
}
|
|
647
|
-
if (field.type.startsWith('resource-ref.')) {
|
|
648
|
-
return (
|
|
649
|
-
<ResolvedValueText
|
|
650
|
-
source="resource"
|
|
651
|
-
value={value as ResourceReferenceValue | ResourceReferenceValue[]}
|
|
652
|
-
/>
|
|
653
|
-
);
|
|
654
|
-
}
|
|
655
|
-
if (field.type === 'file' || field.type === 'image') {
|
|
656
|
-
const values = (Array.isArray(value) ? value : [value]).filter(isFileRef);
|
|
657
|
-
return values.length ? <AttachmentFileList files={values} resourceCode={resourceCode} /> : <>-</>;
|
|
658
|
-
}
|
|
659
|
-
if (field.type === 'location') {
|
|
660
|
-
return <LocationValueDisplay value={value as StableLocationValue} />;
|
|
661
|
-
}
|
|
662
|
-
if (field.type === 'address') {
|
|
663
|
-
return <AddressValueDisplay value={value as StableAddressValue} />;
|
|
664
|
-
}
|
|
665
|
-
if (field.type === 'text.rich') {
|
|
666
|
-
return <RichTextValueDisplay value={String(value)} />;
|
|
667
|
-
}
|
|
668
|
-
if (field.type === 'json') {
|
|
669
|
-
return <JsonValueDisplay value={value} />;
|
|
670
|
-
}
|
|
671
|
-
if (field.type === 'signature') {
|
|
672
|
-
return (
|
|
673
|
-
<SignatureValueDisplay
|
|
674
|
-
resourceCode={resourceCode}
|
|
675
|
-
value={value as StableSignatureValue}
|
|
676
|
-
/>
|
|
677
|
-
);
|
|
678
|
-
}
|
|
679
|
-
if (field.type === 'cascade.single' || field.type === 'cascade.multiple') {
|
|
680
|
-
return (
|
|
681
|
-
<CascadeValueDisplay
|
|
682
|
-
multiple={field.type === 'cascade.multiple'}
|
|
683
|
-
value={value as CascadeStoredValue}
|
|
684
|
-
/>
|
|
685
|
-
);
|
|
686
|
-
}
|
|
687
|
-
if (field.type === 'boolean') {
|
|
688
|
-
return <Tag color={value ? 'green' : 'default'}>{value ? '是' : '否'}</Tag>;
|
|
689
|
-
}
|
|
690
|
-
if (Array.isArray(value)) {
|
|
691
|
-
return <>{value.map(item => optionLabel(field, item)).join('、') || '-'}</>;
|
|
692
|
-
}
|
|
693
|
-
if (['date', 'time', 'datetime', 'date-range', 'datetime-range'].includes(field.type)) {
|
|
694
|
-
return <DateTimeValueDisplay field={field} value={value} />;
|
|
695
|
-
}
|
|
696
|
-
return <>{optionLabel(field, value)}</>;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
function optionLabel(field: SurfaceField, value: unknown) {
|
|
700
|
-
if (value && typeof value === 'object' && 'label' in value) {
|
|
701
|
-
return String((value as LabeledValue).label);
|
|
702
|
-
}
|
|
703
|
-
return field.options?.find(option => option.value === value)?.label || String(value);
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
function isFileRef(value: unknown): value is DataFileRef {
|
|
707
|
-
return Boolean(value && typeof value === 'object' && 'id' in value);
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
type MobilePendingUpload = {
|
|
711
|
-
id: string;
|
|
712
|
-
file: File;
|
|
713
|
-
status: 'uploading' | 'error';
|
|
714
|
-
error?: string;
|
|
715
|
-
};
|
|
716
|
-
|
|
717
|
-
let mobileUploadSequence = 0;
|
|
718
|
-
|
|
719
|
-
function createMobileUploadId() {
|
|
720
|
-
mobileUploadSequence += 1;
|
|
721
|
-
return `mobile-upload-${Date.now()}-${mobileUploadSequence}`;
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
function ManagedFileField({
|
|
725
|
-
field,
|
|
726
|
-
value,
|
|
727
|
-
onChange,
|
|
728
|
-
recordId,
|
|
729
|
-
disabled,
|
|
730
|
-
multiple,
|
|
731
|
-
maxCount = 1,
|
|
732
|
-
maxSizeMb = 50,
|
|
733
|
-
accept,
|
|
734
|
-
onUpload,
|
|
735
|
-
resourceCode,
|
|
736
|
-
mobile = false,
|
|
737
|
-
}: {
|
|
738
|
-
field: SurfaceField;
|
|
739
|
-
value?: DataFileRef | DataFileRef[];
|
|
740
|
-
onChange?: (value: DataFileRef | DataFileRef[] | undefined) => void;
|
|
741
|
-
recordId?: string;
|
|
742
|
-
disabled?: boolean;
|
|
743
|
-
multiple?: boolean;
|
|
744
|
-
maxCount?: number;
|
|
745
|
-
maxSizeMb?: number;
|
|
746
|
-
accept?: string | string[];
|
|
747
|
-
onUpload?: SurfaceFieldRenderers['upload'];
|
|
748
|
-
resourceCode?: string;
|
|
749
|
-
mobile?: boolean;
|
|
750
|
-
}) {
|
|
751
|
-
const [uploading, setUploading] = useState(false);
|
|
752
|
-
const [uploadError, setUploadError] = useState('');
|
|
753
|
-
const [mobilePending, setMobilePending] = useState<MobilePendingUpload[]>([]);
|
|
754
|
-
const mobileInputRef = useRef<HTMLInputElement | null>(null);
|
|
755
|
-
const mobileActiveUploads = useRef(0);
|
|
756
|
-
const refs = useMemo(() => (Array.isArray(value) ? value : value ? [value] : []), [value]);
|
|
757
|
-
const refsRef = useRef(refs);
|
|
758
|
-
useEffect(() => {
|
|
759
|
-
refsRef.current = refs;
|
|
760
|
-
}, [refs]);
|
|
761
|
-
const assertSize = (file: File) => {
|
|
762
|
-
if (file.size > maxSizeMb * 1024 * 1024) {
|
|
763
|
-
throw new Error(`单个文件不能超过 ${maxSizeMb}MB`);
|
|
764
|
-
}
|
|
765
|
-
};
|
|
766
|
-
if (!onUpload) {
|
|
767
|
-
return <Alert type="info" showIcon title="该文件字段尚未配置上传能力" />;
|
|
768
|
-
}
|
|
769
|
-
if (mobile) {
|
|
770
|
-
const acceptValue = Array.isArray(accept) ? accept.join(',') : accept;
|
|
771
|
-
const uploadOne = async (item: MobilePendingUpload) => {
|
|
772
|
-
setMobilePending(current => current.map(entry => entry.id === item.id ? { ...entry, status: 'uploading', error: undefined } : entry));
|
|
773
|
-
mobileActiveUploads.current += 1;
|
|
774
|
-
setUploading(true);
|
|
775
|
-
try {
|
|
776
|
-
assertSize(item.file);
|
|
777
|
-
const uploaded = await onUpload(field, item.file, recordId);
|
|
778
|
-
const next = multiple ? [...refsRef.current, uploaded].slice(0, maxCount) : uploaded;
|
|
779
|
-
refsRef.current = Array.isArray(next) ? next : next ? [next] : [];
|
|
780
|
-
onChange?.(next);
|
|
781
|
-
setMobilePending(current => current.filter(entry => entry.id !== item.id));
|
|
782
|
-
} catch (error) {
|
|
783
|
-
setMobilePending(current => current.map(entry => entry.id === item.id ? { ...entry, status: 'error', error: error instanceof Error ? error.message : String(error) } : entry));
|
|
784
|
-
} finally {
|
|
785
|
-
mobileActiveUploads.current = Math.max(0, mobileActiveUploads.current - 1);
|
|
786
|
-
setUploading(mobileActiveUploads.current > 0);
|
|
787
|
-
}
|
|
788
|
-
};
|
|
789
|
-
const selectFiles = (files: File[]) => {
|
|
790
|
-
const pendingCount = mobilePending.length;
|
|
791
|
-
const available = Math.max(0, maxCount - refsRef.current.length - pendingCount);
|
|
792
|
-
const selected = multiple ? files.slice(0, available) : files.slice(0, 1);
|
|
793
|
-
if (!selected.length) return;
|
|
794
|
-
const items = selected.map(file => ({ id: createMobileUploadId(), file, status: 'uploading' as const }));
|
|
795
|
-
setMobilePending(current => multiple ? [...current, ...items] : items);
|
|
796
|
-
items.forEach(item => void uploadOne(item));
|
|
797
|
-
};
|
|
798
|
-
return (
|
|
799
|
-
<div className="oxa-file-field oxa-mobile-file-field">
|
|
800
|
-
<input
|
|
801
|
-
ref={mobileInputRef}
|
|
802
|
-
accept={acceptValue}
|
|
803
|
-
disabled={disabled || uploading || mobilePending.some(item => item.status === 'uploading') || refs.length + mobilePending.length >= maxCount}
|
|
804
|
-
multiple={multiple}
|
|
805
|
-
onChange={event => {
|
|
806
|
-
selectFiles(Array.from(event.target.files || []));
|
|
807
|
-
event.currentTarget.value = '';
|
|
808
|
-
}}
|
|
809
|
-
style={{ display: 'none' }}
|
|
810
|
-
type="file"
|
|
811
|
-
/>
|
|
812
|
-
{(!maxCount || refs.length + mobilePending.length < maxCount) && (
|
|
813
|
-
<Button
|
|
814
|
-
className="oxa-mobile-file-button"
|
|
815
|
-
disabled={disabled || uploading || mobilePending.some(item => item.status === 'uploading')}
|
|
816
|
-
icon={<UploadOutlined />}
|
|
817
|
-
onClick={() => mobileInputRef.current?.click()}
|
|
818
|
-
>
|
|
819
|
-
上传文件
|
|
820
|
-
</Button>
|
|
821
|
-
)}
|
|
822
|
-
{mobilePending.length > 0 && (
|
|
823
|
-
<div className="oxa-mobile-pending-files">
|
|
824
|
-
{mobilePending.map(item => (
|
|
825
|
-
<div className="oxa-mobile-pending-file" key={item.id}>
|
|
826
|
-
<span className="oxa-mobile-pending-file-copy">
|
|
827
|
-
<strong className="oxa-mobile-pending-file-name" title={item.file.name}>{item.file.name}</strong>
|
|
828
|
-
<small>{formatManagedFileSize(item.file.size)} · {item.status === 'error' ? '上传失败' : '上传中…'}</small>
|
|
829
|
-
</span>
|
|
830
|
-
{item.status === 'error' ? (
|
|
831
|
-
<>
|
|
832
|
-
<span className="oxa-mobile-pending-error">{item.error || '上传失败'}</span>
|
|
833
|
-
<Button onClick={() => void uploadOne(item)} size="small" type="link">重试</Button>
|
|
834
|
-
<Button onClick={() => setMobilePending(current => current.filter(entry => entry.id !== item.id))} size="small" type="link">移除</Button>
|
|
835
|
-
</>
|
|
836
|
-
) : (
|
|
837
|
-
<span className="oxa-mobile-pending-status">上传中…</span>
|
|
838
|
-
)}
|
|
839
|
-
</div>
|
|
840
|
-
))}
|
|
841
|
-
</div>
|
|
842
|
-
)}
|
|
843
|
-
{refs.length ? (
|
|
844
|
-
<AttachmentFileList files={refs} resourceCode={resourceCode} onRemove={file => {
|
|
845
|
-
const next = refs.filter(item => item.id !== file.id);
|
|
846
|
-
refsRef.current = next;
|
|
847
|
-
onChange?.(multiple ? next : undefined);
|
|
848
|
-
}} removable={!disabled} />
|
|
849
|
-
) : (
|
|
850
|
-
<div className="oxa-file-empty">尚未上传文件</div>
|
|
851
|
-
)}
|
|
852
|
-
</div>
|
|
853
|
-
);
|
|
854
|
-
}
|
|
855
|
-
const uploadProps = {
|
|
856
|
-
accept: Array.isArray(accept) ? accept.join(',') : accept,
|
|
857
|
-
disabled,
|
|
858
|
-
maxCount,
|
|
859
|
-
multiple,
|
|
860
|
-
customRequest: async (options: {
|
|
861
|
-
file: string | Blob;
|
|
862
|
-
onError?: (error: Error) => void;
|
|
863
|
-
onProgress?: (event: { percent: number }) => void;
|
|
864
|
-
onSuccess?: (body: unknown) => void;
|
|
865
|
-
}) => {
|
|
866
|
-
setUploading(true);
|
|
867
|
-
setUploadError('');
|
|
868
|
-
try {
|
|
869
|
-
assertSize(options.file as File);
|
|
870
|
-
options.onProgress?.({ percent: 15 });
|
|
871
|
-
const uploaded = await onUpload(field, options.file as File, recordId);
|
|
872
|
-
options.onProgress?.({ percent: 100 });
|
|
873
|
-
const next = multiple ? [...refsRef.current, uploaded] : uploaded;
|
|
874
|
-
refsRef.current = Array.isArray(next) ? next : [next];
|
|
875
|
-
onChange?.(next);
|
|
876
|
-
options.onSuccess?.(uploaded);
|
|
877
|
-
} catch (error) {
|
|
878
|
-
const failure = error instanceof Error ? error : new Error(String(error));
|
|
879
|
-
setUploadError(failure.message);
|
|
880
|
-
options.onError?.(failure);
|
|
881
|
-
} finally {
|
|
882
|
-
setUploading(false);
|
|
883
|
-
}
|
|
884
|
-
},
|
|
885
|
-
showUploadList: false,
|
|
886
|
-
};
|
|
887
|
-
return (
|
|
888
|
-
<div className="oxa-file-field">
|
|
889
|
-
<Upload {...uploadProps} pastable>
|
|
890
|
-
<Button disabled={disabled || uploading || refs.length >= maxCount} icon={field.accept?.toString().startsWith('image/') ? <PictureOutlined /> : <UploadOutlined />} loading={uploading}>
|
|
891
|
-
{field.accept?.toString().startsWith('image/') ? '上传图片' : '上传文件'}
|
|
892
|
-
</Button>
|
|
893
|
-
</Upload>
|
|
894
|
-
<Upload.Dragger {...uploadProps} className="oxa-file-dropzone" openFileDialogOnClick={false} pastable>
|
|
895
|
-
<span>可将文件拖到这里,或在此区域按 <kbd>⌘ V</kbd> 粘贴</span>
|
|
896
|
-
<small>{multiple ? `最多 ${maxCount} 个文件,单个不超过 ${maxSizeMb}MB` : `仅保留一个文件,单个不超过 ${maxSizeMb}MB`}</small>
|
|
897
|
-
</Upload.Dragger>
|
|
898
|
-
{uploadError && (
|
|
899
|
-
<div className="oxa-file-error">
|
|
900
|
-
<span>{uploadError}</span>
|
|
901
|
-
<Button icon={<ReloadOutlined />} onClick={() => setUploadError('')} size="small" type="text">关闭</Button>
|
|
902
|
-
</div>
|
|
903
|
-
)}
|
|
904
|
-
{refs.length ? (
|
|
905
|
-
<AttachmentFileList files={refs} resourceCode={resourceCode} onRemove={file => {
|
|
906
|
-
const next = refs.filter(item => item.id !== file.id);
|
|
907
|
-
refsRef.current = next;
|
|
908
|
-
onChange?.(multiple ? next : undefined);
|
|
909
|
-
}} removable={!disabled} />
|
|
910
|
-
) : (
|
|
911
|
-
<div className="oxa-file-empty">尚未上传文件</div>
|
|
912
|
-
)}
|
|
913
|
-
</div>
|
|
914
|
-
);
|
|
915
|
-
}
|