openxiangda-cli 2.0.0-alpha.85 → 2.0.0-alpha.86
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/package.json +4 -4
- package/template/AGENTS.md +4 -4
- package/template/README.md +2 -2
- package/template/apps/server/package.json +1 -1
- package/template/apps/web/data-api-live.e2e.html +15 -0
- package/template/apps/web/e2e/data-api-live-fixture.tsx +141 -0
- package/template/apps/web/e2e/data-api-live.spec.ts +138 -0
- package/template/apps/web/e2e/field-protocol-fixture.tsx +280 -0
- package/template/apps/web/e2e/field-protocol.spec.ts +100 -0
- package/template/apps/web/e2e/resources.spec.ts +258 -20
- package/template/apps/web/field-protocol.e2e.html +12 -0
- package/template/apps/web/package.json +1 -1
- package/template/apps/web/playwright.config.ts +3 -1
- package/template/apps/web/scripts/check.mjs +8 -5
- package/template/apps/web/scripts/verify-build.mjs +1 -1
- package/template/apps/web/src/AuthoritativeSelector.tsx +165 -132
- package/template/apps/web/src/FilePreviewPage.tsx +30 -9
- package/template/apps/web/src/RoleSubjectSelect.tsx +128 -0
- package/template/apps/web/src/Shell.tsx +93 -6
- package/template/apps/web/src/components/platform-fields/AddressField.tsx +338 -0
- package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +25 -6
- package/template/apps/web/src/components/platform-fields/CascadeField.tsx +49 -0
- package/template/apps/web/src/components/platform-fields/DateTimeField.tsx +148 -0
- package/template/apps/web/src/components/platform-fields/JsonField.tsx +77 -0
- package/template/apps/web/src/components/platform-fields/LocationField.tsx +164 -0
- package/template/apps/web/src/components/platform-fields/PlatformDirectoryPicker.tsx +19 -44
- package/template/apps/web/src/components/platform-fields/ResourceReferenceField.tsx +67 -0
- package/template/apps/web/src/components/platform-fields/RichTextField.tsx +196 -0
- package/template/apps/web/src/components/platform-fields/SignatureField.tsx +315 -0
- package/template/apps/web/src/components/platform-fields/SubtableField.tsx +553 -0
- package/template/apps/web/src/components/platform-fields/address-value.ts +80 -0
- package/template/apps/web/src/components/platform-fields/cascade-value.ts +66 -0
- package/template/apps/web/src/components/platform-fields/directory-value.ts +53 -0
- package/template/apps/web/src/components/platform-fields/field-form-codec.ts +98 -0
- package/template/apps/web/src/components/platform-fields/location-value.ts +87 -0
- package/template/apps/web/src/components/platform-fields/resource-query.ts +131 -0
- package/template/apps/web/src/components/platform-fields/rich-text-value.ts +59 -0
- package/template/apps/web/src/components/platform-fields/subtable-value.ts +187 -0
- package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +194 -108
- package/template/apps/web/src/components/resource/ResourceBatchActions.tsx +8 -1
- package/template/apps/web/src/components/resource/SurfaceFields.tsx +385 -79
- package/template/apps/web/src/components/resource/generated-resource-definition.ts +18 -0
- package/template/apps/web/src/components/resource/resource-import.ts +4 -4
- package/template/apps/web/src/data-provider.ts +59 -34
- package/template/apps/web/src/platform-client.ts +341 -136
- package/template/apps/web/src/runtime.tsx +156 -17
- package/template/apps/web/src/styles.css +264 -0
- package/template/apps/web/test/address-field.test.ts +66 -0
- package/template/apps/web/test/cascade-value.test.ts +68 -0
- package/template/apps/web/test/contracts.test.ts +29 -5
- package/template/apps/web/test/directory-value.test.ts +55 -0
- package/template/apps/web/test/field-bounds.test.ts +17 -0
- package/template/apps/web/test/field-form-codec.test.ts +85 -0
- package/template/apps/web/test/location-field.test.ts +83 -0
- package/template/apps/web/test/resource-import.test.ts +8 -7
- package/template/apps/web/test/resource-query.test.ts +144 -0
- package/template/apps/web/test/rich-json-field.test.ts +53 -0
- package/template/apps/web/test/signature-serial-field.test.ts +40 -0
- package/template/apps/web/test/subtable-value.test.ts +144 -0
- package/template/package.json +3 -3
- package/template/packages/contracts/src/generated.ts +69 -0
- package/template/scripts/verify-template-budget.mjs +11 -4
|
@@ -6,10 +6,12 @@ import {
|
|
|
6
6
|
import {
|
|
7
7
|
Alert,
|
|
8
8
|
Button,
|
|
9
|
+
Checkbox,
|
|
9
10
|
DatePicker,
|
|
10
11
|
Form,
|
|
11
12
|
Input,
|
|
12
13
|
InputNumber,
|
|
14
|
+
Radio,
|
|
13
15
|
Select,
|
|
14
16
|
Space,
|
|
15
17
|
Switch,
|
|
@@ -19,15 +21,30 @@ import {
|
|
|
19
21
|
import type {
|
|
20
22
|
DataFieldSurface,
|
|
21
23
|
DataFileRef,
|
|
24
|
+
DepartmentReferenceValue,
|
|
25
|
+
LabeledValue,
|
|
26
|
+
ResourceReferenceValue,
|
|
27
|
+
StableAddressValue,
|
|
28
|
+
StableLocationValue,
|
|
29
|
+
StableSignatureValue,
|
|
30
|
+
UserReferenceValue,
|
|
22
31
|
} from 'openxiangda-contracts/browser';
|
|
23
32
|
import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
|
24
33
|
import {
|
|
25
34
|
AuthoritativeSelector,
|
|
26
|
-
MobileAuthoritativeSelector,
|
|
27
35
|
ResolvedValueText,
|
|
28
36
|
} from '../../AuthoritativeSelector';
|
|
37
|
+
import { AddressField, AddressValueDisplay } from '../platform-fields/AddressField';
|
|
29
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';
|
|
30
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';
|
|
31
48
|
|
|
32
49
|
export type SurfaceField = DataFieldSurface & { key: string };
|
|
33
50
|
|
|
@@ -41,35 +58,51 @@ export interface SurfaceFieldEditContext {
|
|
|
41
58
|
field: SurfaceField;
|
|
42
59
|
disabled: boolean;
|
|
43
60
|
operation: 'create' | 'update';
|
|
61
|
+
resourceCode?: string;
|
|
44
62
|
recordId?: string;
|
|
45
63
|
}
|
|
46
64
|
|
|
47
65
|
export interface SurfaceFieldValueContext {
|
|
48
66
|
field: SurfaceField;
|
|
49
67
|
value: unknown;
|
|
68
|
+
resourceCode?: string;
|
|
50
69
|
}
|
|
51
70
|
|
|
52
71
|
export interface SurfaceFieldRenderers {
|
|
53
72
|
renderExtra?: (context: SurfaceFieldEditContext) => ReactNode;
|
|
54
73
|
renderScope?: (context: SurfaceFieldEditContext) => ReactNode;
|
|
74
|
+
renderSubtable?: (context: SurfaceFieldEditContext) => ReactNode;
|
|
55
75
|
renderFilter?: (context: SurfaceFilterContext) => ReactNode;
|
|
56
76
|
upload?: (
|
|
57
77
|
field: SurfaceField,
|
|
58
78
|
file: File,
|
|
59
79
|
recordId?: string
|
|
60
80
|
) => Promise<DataFileRef>;
|
|
81
|
+
signer?: UserReferenceValue;
|
|
61
82
|
renderValue?: (context: SurfaceFieldValueContext) => ReactNode | undefined;
|
|
62
83
|
}
|
|
63
84
|
|
|
64
85
|
export interface SurfaceFilterContext {
|
|
65
86
|
field: SurfaceField;
|
|
87
|
+
resourceCode?: string;
|
|
66
88
|
value: unknown;
|
|
67
89
|
onChange: (value: unknown) => void;
|
|
68
90
|
}
|
|
69
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
|
+
|
|
70
102
|
/** Render a standard filter control from the same field surface used by forms. */
|
|
71
103
|
export function SurfaceFilterControl({
|
|
72
104
|
field,
|
|
105
|
+
resourceCode,
|
|
73
106
|
value,
|
|
74
107
|
onChange,
|
|
75
108
|
range = false,
|
|
@@ -79,7 +112,7 @@ export function SurfaceFilterControl({
|
|
|
79
112
|
if (custom !== undefined) return custom;
|
|
80
113
|
const placeholder = `筛选${field.label}`;
|
|
81
114
|
switch (field.widget) {
|
|
82
|
-
case '
|
|
115
|
+
case 'switch':
|
|
83
116
|
return (
|
|
84
117
|
<Select
|
|
85
118
|
allowClear
|
|
@@ -90,11 +123,11 @@ export function SurfaceFilterControl({
|
|
|
90
123
|
/>
|
|
91
124
|
);
|
|
92
125
|
case 'select':
|
|
93
|
-
case 'multi':
|
|
126
|
+
case 'multi-select':
|
|
94
127
|
return (
|
|
95
128
|
<Select
|
|
96
129
|
allowClear
|
|
97
|
-
mode={field.widget === 'multi' ? 'multiple' : undefined}
|
|
130
|
+
mode={field.widget === 'multi-select' ? 'multiple' : undefined}
|
|
98
131
|
options={field.options}
|
|
99
132
|
placeholder={placeholder}
|
|
100
133
|
value={value}
|
|
@@ -102,14 +135,11 @@ export function SurfaceFilterControl({
|
|
|
102
135
|
/>
|
|
103
136
|
);
|
|
104
137
|
case 'date':
|
|
138
|
+
case 'time':
|
|
105
139
|
case 'datetime':
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
showTime={field.widget === 'datetime'}
|
|
110
|
-
onChange={(_, values) => onChange(values[0] && values[1] ? [values[0], values[1]] : undefined)}
|
|
111
|
-
/>
|
|
112
|
-
) : <DatePicker showTime={field.widget === 'datetime'} onChange={onChange} placeholder={placeholder} />;
|
|
140
|
+
case 'date-range':
|
|
141
|
+
case 'datetime-range':
|
|
142
|
+
return <DateTimeFilter field={field} onChange={onChange} value={value} />;
|
|
113
143
|
case 'number':
|
|
114
144
|
case 'money':
|
|
115
145
|
case 'percent':
|
|
@@ -117,28 +147,117 @@ export function SurfaceFilterControl({
|
|
|
117
147
|
const values = Array.isArray(value) ? value as [number | undefined, number | undefined] : [];
|
|
118
148
|
return (
|
|
119
149
|
<Space.Compact>
|
|
120
|
-
<InputNumber
|
|
121
|
-
<InputNumber
|
|
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]} />
|
|
122
152
|
</Space.Compact>
|
|
123
153
|
);
|
|
124
154
|
}
|
|
125
|
-
return <InputNumber
|
|
155
|
+
return <InputNumber {...numberInputProps(field)} onChange={onChange} placeholder={placeholder} value={value as number | null | undefined} />;
|
|
126
156
|
case 'scope':
|
|
127
157
|
return renderers?.renderScope?.({ field, disabled: false, operation: 'update' }) || <Input allowClear value={value as string | undefined} onChange={event => onChange(event.target.value)} placeholder={placeholder} />;
|
|
128
158
|
case 'directory-user':
|
|
129
159
|
case 'directory-department':
|
|
130
|
-
return <PlatformDirectoryPicker kind={field.widget === 'directory-user' ? 'user' : 'department'} multiple={field.multiple} onChange={onChange as (value:
|
|
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} />;
|
|
131
161
|
case 'resource':
|
|
132
|
-
return
|
|
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} />;
|
|
133
192
|
default:
|
|
134
193
|
return <Input allowClear value={value as string | undefined} onChange={event => onChange(event.target.value)} placeholder={placeholder} />;
|
|
135
194
|
}
|
|
136
195
|
}
|
|
137
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
|
+
|
|
138
256
|
export function SurfaceFieldControl({
|
|
139
257
|
field,
|
|
140
258
|
disabled,
|
|
141
259
|
operation,
|
|
260
|
+
resourceCode,
|
|
142
261
|
recordId,
|
|
143
262
|
renderers,
|
|
144
263
|
}: SurfaceFieldEditContext & { renderers?: SurfaceFieldRenderers }) {
|
|
@@ -153,7 +272,11 @@ export function SurfaceFieldControl({
|
|
|
153
272
|
? [{ pattern: /^1\d{10}$/, message: '请输入 11 位手机号' }]
|
|
154
273
|
: []),
|
|
155
274
|
];
|
|
156
|
-
const common = {
|
|
275
|
+
const common = {
|
|
276
|
+
disabled,
|
|
277
|
+
...(field.maxLength === undefined ? {} : { maxLength: field.maxLength }),
|
|
278
|
+
placeholder: `请输入${field.label}`,
|
|
279
|
+
};
|
|
157
280
|
let control: ReactNode = <Input {...common} />;
|
|
158
281
|
switch (field.widget) {
|
|
159
282
|
case 'textarea':
|
|
@@ -162,20 +285,33 @@ export function SurfaceFieldControl({
|
|
|
162
285
|
case 'number':
|
|
163
286
|
case 'money':
|
|
164
287
|
case 'percent':
|
|
165
|
-
control = <InputNumber {...common}
|
|
288
|
+
control = <InputNumber {...common} {...numberInputProps(field)} style={{ width: '100%' }} />;
|
|
166
289
|
break;
|
|
167
290
|
case 'date':
|
|
291
|
+
case 'time':
|
|
168
292
|
case 'datetime':
|
|
169
|
-
|
|
293
|
+
case 'date-range':
|
|
294
|
+
case 'datetime-range':
|
|
295
|
+
control = <DateTimeField disabled={disabled} field={field} />;
|
|
170
296
|
break;
|
|
171
|
-
case '
|
|
297
|
+
case 'switch':
|
|
172
298
|
control = <Switch checkedChildren="是" disabled={disabled} unCheckedChildren="否" />;
|
|
173
299
|
break;
|
|
174
300
|
case 'select':
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
case '
|
|
178
|
-
control =
|
|
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
|
+
);
|
|
179
315
|
break;
|
|
180
316
|
case 'email':
|
|
181
317
|
control = <Input {...common} type="email" />;
|
|
@@ -194,42 +330,92 @@ export function SurfaceFieldControl({
|
|
|
194
330
|
<PlatformDirectoryPicker
|
|
195
331
|
disabled={disabled}
|
|
196
332
|
kind={field.widget === 'directory-user' ? 'user' : 'department'}
|
|
197
|
-
multiple={field.multiple}
|
|
333
|
+
multiple={field.type.endsWith('.multiple')}
|
|
198
334
|
placeholder={`搜索并选择${field.label}`}
|
|
199
335
|
/>
|
|
200
336
|
);
|
|
201
337
|
break;
|
|
202
338
|
case 'resource':
|
|
203
339
|
control = (
|
|
204
|
-
<
|
|
340
|
+
<ResourceReferenceField
|
|
205
341
|
disabled={disabled}
|
|
206
|
-
|
|
207
|
-
|
|
342
|
+
field={field}
|
|
343
|
+
fieldCode={field.key}
|
|
208
344
|
operation={operation}
|
|
209
|
-
|
|
210
|
-
resourceCode={field.reference?.kind === 'resource' ? field.reference.resourceCode : undefined}
|
|
211
|
-
source="resource"
|
|
345
|
+
resourceCode={resourceCode}
|
|
212
346
|
/>
|
|
213
347
|
);
|
|
214
348
|
break;
|
|
215
|
-
case '
|
|
349
|
+
case 'cascade':
|
|
350
|
+
control = <CascadeField disabled={disabled} field={field} />;
|
|
351
|
+
break;
|
|
352
|
+
case 'attachment':
|
|
353
|
+
case 'image':
|
|
216
354
|
control = (
|
|
217
355
|
<ManagedFileField
|
|
218
356
|
accept={field.accept}
|
|
219
357
|
disabled={disabled}
|
|
220
358
|
field={field}
|
|
221
359
|
maxCount={field.maxCount}
|
|
222
|
-
|
|
360
|
+
maxSizeMb={field.maxSizeMb}
|
|
361
|
+
multiple={field.type === 'file'}
|
|
223
362
|
onUpload={renderers?.upload}
|
|
224
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}
|
|
225
394
|
/>
|
|
226
395
|
);
|
|
227
396
|
break;
|
|
228
397
|
case 'json':
|
|
229
|
-
control = <
|
|
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" />;
|
|
230
408
|
break;
|
|
231
409
|
case 'readonly':
|
|
232
|
-
control =
|
|
410
|
+
control = (
|
|
411
|
+
<Input
|
|
412
|
+
{...common}
|
|
413
|
+
placeholder={field.type === 'serial-number'
|
|
414
|
+
? '保存后由系统自动生成'
|
|
415
|
+
: '由系统生成'}
|
|
416
|
+
readOnly
|
|
417
|
+
/>
|
|
418
|
+
);
|
|
233
419
|
break;
|
|
234
420
|
case 'text':
|
|
235
421
|
default:
|
|
@@ -238,7 +424,7 @@ export function SurfaceFieldControl({
|
|
|
238
424
|
const extra =
|
|
239
425
|
renderers?.renderExtra?.({ field, disabled, operation, recordId }) ||
|
|
240
426
|
(disabled ? '当前角色不可修改此字段' : undefined);
|
|
241
|
-
const wide =
|
|
427
|
+
const wide = ['textarea', 'attachment', 'image', 'rich-text', 'address', 'location', 'signature', 'json', 'subtable'].includes(field.widget);
|
|
242
428
|
return (
|
|
243
429
|
<Form.Item
|
|
244
430
|
className={wide ? 'oxa-field-wide' : undefined}
|
|
@@ -246,7 +432,7 @@ export function SurfaceFieldControl({
|
|
|
246
432
|
label={field.label}
|
|
247
433
|
name={field.key}
|
|
248
434
|
rules={rules}
|
|
249
|
-
valuePropName={field.widget === '
|
|
435
|
+
valuePropName={field.widget === 'switch' ? 'checked' : 'value'}
|
|
250
436
|
>
|
|
251
437
|
{control}
|
|
252
438
|
</Form.Item>
|
|
@@ -258,47 +444,64 @@ export function MobileSurfaceFieldControl({
|
|
|
258
444
|
field,
|
|
259
445
|
disabled,
|
|
260
446
|
operation,
|
|
447
|
+
resourceCode,
|
|
261
448
|
recordId,
|
|
262
449
|
renderers,
|
|
263
450
|
}: SurfaceFieldEditContext & { renderers?: SurfaceFieldRenderers }) {
|
|
264
451
|
const rules = field.requiredHint
|
|
265
452
|
? [{ required: true, message: `请填写或选择${field.label}` }]
|
|
266
453
|
: [];
|
|
267
|
-
const inputProps = {
|
|
454
|
+
const inputProps = {
|
|
455
|
+
disabled,
|
|
456
|
+
className: 'oxa-mobile-input',
|
|
457
|
+
...(field.maxLength === undefined ? {} : { maxLength: field.maxLength }),
|
|
458
|
+
placeholder: `请输入${field.label}`,
|
|
459
|
+
};
|
|
268
460
|
let control: ReactNode = <input {...inputProps} />;
|
|
269
461
|
switch (field.widget) {
|
|
270
462
|
case 'textarea':
|
|
271
|
-
case 'json':
|
|
272
463
|
control = <textarea {...inputProps} rows={4} />;
|
|
273
464
|
break;
|
|
274
465
|
case 'number':
|
|
275
466
|
case 'money':
|
|
276
467
|
case 'percent':
|
|
277
|
-
control =
|
|
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
|
+
);
|
|
278
479
|
break;
|
|
279
480
|
case 'date':
|
|
280
|
-
|
|
281
|
-
break;
|
|
481
|
+
case 'time':
|
|
282
482
|
case 'datetime':
|
|
283
|
-
|
|
483
|
+
case 'date-range':
|
|
484
|
+
case 'datetime-range':
|
|
485
|
+
control = <DateTimeField disabled={disabled} field={field} mobile />;
|
|
284
486
|
break;
|
|
285
|
-
case '
|
|
487
|
+
case 'switch':
|
|
286
488
|
control = <input disabled={disabled} type="checkbox" />;
|
|
287
489
|
break;
|
|
288
490
|
case 'select':
|
|
289
|
-
case 'multi':
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
491
|
+
case 'multi-select':
|
|
492
|
+
case 'radio':
|
|
493
|
+
case 'checkbox':
|
|
494
|
+
control = field.source ? (
|
|
495
|
+
<ResourceReferenceField
|
|
293
496
|
disabled={disabled}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
497
|
+
field={field}
|
|
498
|
+
fieldCode={field.key}
|
|
499
|
+
mobile
|
|
500
|
+
operation={operation}
|
|
501
|
+
resourceCode={resourceCode}
|
|
502
|
+
/>
|
|
503
|
+
) : (
|
|
504
|
+
<StaticOptionField disabled={disabled} field={field} />
|
|
302
505
|
);
|
|
303
506
|
break;
|
|
304
507
|
case 'email':
|
|
@@ -318,40 +521,96 @@ export function MobileSurfaceFieldControl({
|
|
|
318
521
|
<PlatformDirectoryPicker
|
|
319
522
|
disabled={disabled}
|
|
320
523
|
kind={field.widget === 'directory-user' ? 'user' : 'department'}
|
|
321
|
-
multiple={field.multiple}
|
|
524
|
+
multiple={field.type.endsWith('.multiple')}
|
|
322
525
|
placeholder={`搜索并选择${field.label}`}
|
|
323
526
|
/>
|
|
324
527
|
);
|
|
325
528
|
break;
|
|
326
529
|
case 'resource':
|
|
327
530
|
control = (
|
|
328
|
-
<
|
|
531
|
+
<ResourceReferenceField
|
|
329
532
|
disabled={disabled}
|
|
330
|
-
|
|
331
|
-
|
|
533
|
+
field={field}
|
|
534
|
+
fieldCode={field.key}
|
|
535
|
+
mobile
|
|
332
536
|
operation={operation}
|
|
333
|
-
|
|
334
|
-
resourceCode={field.reference?.kind === 'resource' ? field.reference.resourceCode : undefined}
|
|
335
|
-
source="resource"
|
|
537
|
+
resourceCode={resourceCode}
|
|
336
538
|
/>
|
|
337
539
|
);
|
|
338
540
|
break;
|
|
339
|
-
case '
|
|
541
|
+
case 'cascade':
|
|
542
|
+
control = <CascadeField disabled={disabled} field={field} />;
|
|
543
|
+
break;
|
|
544
|
+
case 'attachment':
|
|
545
|
+
case 'image':
|
|
340
546
|
control = (
|
|
341
547
|
<ManagedFileField
|
|
342
548
|
accept={field.accept}
|
|
343
549
|
disabled={disabled}
|
|
344
550
|
field={field}
|
|
345
551
|
maxCount={field.maxCount}
|
|
346
|
-
|
|
552
|
+
maxSizeMb={field.maxSizeMb}
|
|
553
|
+
multiple={field.type === 'file'}
|
|
347
554
|
mobile
|
|
348
555
|
onUpload={renderers?.upload}
|
|
349
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}
|
|
350
574
|
/>
|
|
351
575
|
);
|
|
352
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;
|
|
353
604
|
case 'readonly':
|
|
354
|
-
control =
|
|
605
|
+
control = (
|
|
606
|
+
<input
|
|
607
|
+
{...inputProps}
|
|
608
|
+
placeholder={field.type === 'serial-number'
|
|
609
|
+
? '保存后由系统自动生成'
|
|
610
|
+
: '由系统生成'}
|
|
611
|
+
readOnly
|
|
612
|
+
/>
|
|
613
|
+
);
|
|
355
614
|
break;
|
|
356
615
|
}
|
|
357
616
|
return (
|
|
@@ -361,7 +620,7 @@ export function MobileSurfaceFieldControl({
|
|
|
361
620
|
label={field.label}
|
|
362
621
|
name={field.key}
|
|
363
622
|
rules={rules}
|
|
364
|
-
valuePropName={field.widget === '
|
|
623
|
+
valuePropName={field.widget === 'switch' ? 'checked' : 'value'}
|
|
365
624
|
>
|
|
366
625
|
{control}
|
|
367
626
|
</Form.Item>
|
|
@@ -371,6 +630,7 @@ export function MobileSurfaceFieldControl({
|
|
|
371
630
|
export function SurfaceFieldValue({
|
|
372
631
|
field,
|
|
373
632
|
value,
|
|
633
|
+
resourceCode,
|
|
374
634
|
renderers,
|
|
375
635
|
}: SurfaceFieldValueContext & { renderers?: SurfaceFieldRenderers }) {
|
|
376
636
|
if (value === undefined || value === null || value === '') return <>-</>;
|
|
@@ -380,31 +640,66 @@ export function SurfaceFieldValue({
|
|
|
380
640
|
return (
|
|
381
641
|
<ResolvedValueText
|
|
382
642
|
source={field.widget === 'directory-user' ? 'user' : 'department'}
|
|
383
|
-
value={
|
|
643
|
+
value={value as never}
|
|
384
644
|
/>
|
|
385
645
|
);
|
|
386
646
|
}
|
|
387
|
-
if (field.
|
|
388
|
-
return
|
|
647
|
+
if (field.type.startsWith('resource-ref.')) {
|
|
648
|
+
return (
|
|
649
|
+
<ResolvedValueText
|
|
650
|
+
source="resource"
|
|
651
|
+
value={value as ResourceReferenceValue | ResourceReferenceValue[]}
|
|
652
|
+
/>
|
|
653
|
+
);
|
|
389
654
|
}
|
|
390
|
-
if (field.
|
|
655
|
+
if (field.type === 'file' || field.type === 'image') {
|
|
391
656
|
const values = (Array.isArray(value) ? value : [value]).filter(isFileRef);
|
|
392
|
-
return values.length ? <AttachmentFileList files={values} /> : <>-</>;
|
|
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
|
+
);
|
|
393
686
|
}
|
|
394
|
-
if (field.
|
|
687
|
+
if (field.type === 'boolean') {
|
|
395
688
|
return <Tag color={value ? 'green' : 'default'}>{value ? '是' : '否'}</Tag>;
|
|
396
689
|
}
|
|
397
690
|
if (Array.isArray(value)) {
|
|
398
691
|
return <>{value.map(item => optionLabel(field, item)).join('、') || '-'}</>;
|
|
399
692
|
}
|
|
400
|
-
if (
|
|
401
|
-
|
|
402
|
-
return <>{Number.isNaN(parsed.getTime()) ? String(value) : parsed.toLocaleString('zh-CN', { hour12: false })}</>;
|
|
693
|
+
if (['date', 'time', 'datetime', 'date-range', 'datetime-range'].includes(field.type)) {
|
|
694
|
+
return <DateTimeValueDisplay field={field} value={value} />;
|
|
403
695
|
}
|
|
404
696
|
return <>{optionLabel(field, value)}</>;
|
|
405
697
|
}
|
|
406
698
|
|
|
407
699
|
function optionLabel(field: SurfaceField, value: unknown) {
|
|
700
|
+
if (value && typeof value === 'object' && 'label' in value) {
|
|
701
|
+
return String((value as LabeledValue).label);
|
|
702
|
+
}
|
|
408
703
|
return field.options?.find(option => option.value === value)?.label || String(value);
|
|
409
704
|
}
|
|
410
705
|
|
|
@@ -434,8 +729,10 @@ function ManagedFileField({
|
|
|
434
729
|
disabled,
|
|
435
730
|
multiple,
|
|
436
731
|
maxCount = 1,
|
|
732
|
+
maxSizeMb = 50,
|
|
437
733
|
accept,
|
|
438
734
|
onUpload,
|
|
735
|
+
resourceCode,
|
|
439
736
|
mobile = false,
|
|
440
737
|
}: {
|
|
441
738
|
field: SurfaceField;
|
|
@@ -445,8 +742,10 @@ function ManagedFileField({
|
|
|
445
742
|
disabled?: boolean;
|
|
446
743
|
multiple?: boolean;
|
|
447
744
|
maxCount?: number;
|
|
745
|
+
maxSizeMb?: number;
|
|
448
746
|
accept?: string | string[];
|
|
449
747
|
onUpload?: SurfaceFieldRenderers['upload'];
|
|
748
|
+
resourceCode?: string;
|
|
450
749
|
mobile?: boolean;
|
|
451
750
|
}) {
|
|
452
751
|
const [uploading, setUploading] = useState(false);
|
|
@@ -459,6 +758,11 @@ function ManagedFileField({
|
|
|
459
758
|
useEffect(() => {
|
|
460
759
|
refsRef.current = refs;
|
|
461
760
|
}, [refs]);
|
|
761
|
+
const assertSize = (file: File) => {
|
|
762
|
+
if (file.size > maxSizeMb * 1024 * 1024) {
|
|
763
|
+
throw new Error(`单个文件不能超过 ${maxSizeMb}MB`);
|
|
764
|
+
}
|
|
765
|
+
};
|
|
462
766
|
if (!onUpload) {
|
|
463
767
|
return <Alert type="info" showIcon title="该文件字段尚未配置上传能力" />;
|
|
464
768
|
}
|
|
@@ -469,6 +773,7 @@ function ManagedFileField({
|
|
|
469
773
|
mobileActiveUploads.current += 1;
|
|
470
774
|
setUploading(true);
|
|
471
775
|
try {
|
|
776
|
+
assertSize(item.file);
|
|
472
777
|
const uploaded = await onUpload(field, item.file, recordId);
|
|
473
778
|
const next = multiple ? [...refsRef.current, uploaded].slice(0, maxCount) : uploaded;
|
|
474
779
|
refsRef.current = Array.isArray(next) ? next : next ? [next] : [];
|
|
@@ -536,7 +841,7 @@ function ManagedFileField({
|
|
|
536
841
|
</div>
|
|
537
842
|
)}
|
|
538
843
|
{refs.length ? (
|
|
539
|
-
<AttachmentFileList files={refs} onRemove={file => {
|
|
844
|
+
<AttachmentFileList files={refs} resourceCode={resourceCode} onRemove={file => {
|
|
540
845
|
const next = refs.filter(item => item.id !== file.id);
|
|
541
846
|
refsRef.current = next;
|
|
542
847
|
onChange?.(multiple ? next : undefined);
|
|
@@ -561,6 +866,7 @@ function ManagedFileField({
|
|
|
561
866
|
setUploading(true);
|
|
562
867
|
setUploadError('');
|
|
563
868
|
try {
|
|
869
|
+
assertSize(options.file as File);
|
|
564
870
|
options.onProgress?.({ percent: 15 });
|
|
565
871
|
const uploaded = await onUpload(field, options.file as File, recordId);
|
|
566
872
|
options.onProgress?.({ percent: 100 });
|
|
@@ -587,7 +893,7 @@ function ManagedFileField({
|
|
|
587
893
|
</Upload>
|
|
588
894
|
<Upload.Dragger {...uploadProps} className="oxa-file-dropzone" openFileDialogOnClick={false} pastable>
|
|
589
895
|
<span>可将文件拖到这里,或在此区域按 <kbd>⌘ V</kbd> 粘贴</span>
|
|
590
|
-
<small>{multiple ? `最多 ${maxCount} 个文件,单个不超过
|
|
896
|
+
<small>{multiple ? `最多 ${maxCount} 个文件,单个不超过 ${maxSizeMb}MB` : `仅保留一个文件,单个不超过 ${maxSizeMb}MB`}</small>
|
|
591
897
|
</Upload.Dragger>
|
|
592
898
|
{uploadError && (
|
|
593
899
|
<div className="oxa-file-error">
|
|
@@ -596,7 +902,7 @@ function ManagedFileField({
|
|
|
596
902
|
</div>
|
|
597
903
|
)}
|
|
598
904
|
{refs.length ? (
|
|
599
|
-
<AttachmentFileList files={refs} onRemove={file => {
|
|
905
|
+
<AttachmentFileList files={refs} resourceCode={resourceCode} onRemove={file => {
|
|
600
906
|
const next = refs.filter(item => item.id !== file.id);
|
|
601
907
|
refsRef.current = next;
|
|
602
908
|
onChange?.(multiple ? next : undefined);
|