openxiangda-cli 2.0.0-alpha.59 → 2.0.0-alpha.61
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/dist/create-workspace.d.ts +1 -0
- package/dist/create-workspace.d.ts.map +1 -1
- package/package.json +4 -4
- package/template/apps/server/package.json +1 -1
- package/template/apps/web/e2e/instruments.spec.ts +1 -1
- package/template/apps/web/package.json +1 -1
- package/template/apps/web/scripts/check.mjs +5 -2
- package/template/apps/web/src/InstrumentDetailPage.tsx +119 -77
- package/template/apps/web/src/InstrumentForm.tsx +119 -314
- package/template/apps/web/src/InstrumentFormPage.tsx +40 -75
- package/template/apps/web/src/InstrumentListPage.tsx +264 -192
- package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +285 -0
- package/template/apps/web/src/components/resource/StandardResourcePages.tsx +431 -0
- package/template/apps/web/src/components/resource/SurfaceFields.tsx +469 -0
- package/template/apps/web/src/data-provider.ts +85 -1
- package/template/apps/web/src/fields.ts +9 -74
- package/template/apps/web/src/instrument.ts +1 -0
- package/template/apps/web/src/main.tsx +32 -2
- package/template/apps/web/src/platform-client.ts +95 -2
- package/template/apps/web/src/styles.css +97 -0
- package/template/apps/web/test/contracts.test.ts +80 -4
- package/template/package.json +3 -3
- package/template/packages/contracts/src/generated.ts +674 -0
- package/template/platform/data/instrument-surface.ts +125 -0
- package/template/platform/data/instruments.ts +2 -0
|
@@ -1,37 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
PictureOutlined,
|
|
3
|
-
ReloadOutlined,
|
|
4
|
-
UploadOutlined,
|
|
5
|
-
} from '@ant-design/icons';
|
|
6
1
|
import {
|
|
7
2
|
App,
|
|
8
3
|
Button,
|
|
9
4
|
Card,
|
|
10
|
-
DatePicker,
|
|
11
5
|
Descriptions,
|
|
12
6
|
Form,
|
|
13
|
-
Input,
|
|
14
|
-
InputNumber,
|
|
15
|
-
Select,
|
|
16
|
-
Switch,
|
|
17
7
|
Tag,
|
|
18
8
|
Timeline,
|
|
19
9
|
Typography,
|
|
20
|
-
Upload,
|
|
21
10
|
} from 'antd';
|
|
22
11
|
import dayjs from 'dayjs';
|
|
23
|
-
import { useEffect,
|
|
24
|
-
import type { DataAuditEntry
|
|
12
|
+
import { useEffect, useState } from 'react';
|
|
13
|
+
import type { DataAuditEntry } from 'openxiangda-contracts/browser';
|
|
25
14
|
import {
|
|
26
15
|
AuthoritativeSelector,
|
|
27
16
|
ResolvedValueText,
|
|
28
17
|
} from './AuthoritativeSelector';
|
|
29
|
-
import {
|
|
30
|
-
|
|
18
|
+
import {
|
|
19
|
+
MobileSurfaceFieldControl,
|
|
20
|
+
SurfaceFieldControl,
|
|
21
|
+
SurfaceFieldValue,
|
|
22
|
+
type SurfaceField,
|
|
23
|
+
} from './components/resource/SurfaceFields';
|
|
31
24
|
import { instrumentFields, sectionTitles, type InstrumentField } from './fields';
|
|
32
25
|
import type { InstrumentRecord } from './instrument';
|
|
33
26
|
import { instrumentDataApi } from './platform-client';
|
|
34
27
|
import { useRuntime } from './runtime';
|
|
28
|
+
import { instrumentSurface } from '../../../platform/data/instrument-surface.js';
|
|
35
29
|
|
|
36
30
|
type Values = Record<string, unknown>;
|
|
37
31
|
const createDefaults = {
|
|
@@ -69,6 +63,7 @@ export function sanitizeInstrumentValues(
|
|
|
69
63
|
export function InstrumentForm({
|
|
70
64
|
mode,
|
|
71
65
|
record,
|
|
66
|
+
variant = 'desktop',
|
|
72
67
|
onCancel,
|
|
73
68
|
onSubmit,
|
|
74
69
|
auditEntries = [],
|
|
@@ -76,6 +71,7 @@ export function InstrumentForm({
|
|
|
76
71
|
}: {
|
|
77
72
|
mode: 'create' | 'edit' | 'detail';
|
|
78
73
|
record?: InstrumentRecord;
|
|
74
|
+
variant?: 'desktop' | 'mobile';
|
|
79
75
|
onCancel: () => void;
|
|
80
76
|
onSubmit?: (values: Values) => Promise<void>;
|
|
81
77
|
auditEntries?: DataAuditEntry[];
|
|
@@ -89,7 +85,11 @@ export function InstrumentForm({
|
|
|
89
85
|
if (record) {
|
|
90
86
|
form.setFieldsValue({
|
|
91
87
|
...record,
|
|
92
|
-
enabledDate: record.enabledDate
|
|
88
|
+
enabledDate: record.enabledDate
|
|
89
|
+
? variant === 'mobile'
|
|
90
|
+
? record.enabledDate
|
|
91
|
+
: dayjs(record.enabledDate)
|
|
92
|
+
: undefined,
|
|
93
93
|
});
|
|
94
94
|
}
|
|
95
95
|
}, [form, record]);
|
|
@@ -99,6 +99,7 @@ export function InstrumentForm({
|
|
|
99
99
|
auditEntries={auditEntries}
|
|
100
100
|
auditError={auditError}
|
|
101
101
|
record={record}
|
|
102
|
+
variant={variant}
|
|
102
103
|
/>
|
|
103
104
|
);
|
|
104
105
|
}
|
|
@@ -136,9 +137,11 @@ export function InstrumentForm({
|
|
|
136
137
|
.filter(field => field.section === key && !field.system)
|
|
137
138
|
.map(field => field.key),
|
|
138
139
|
}));
|
|
140
|
+
const FieldControl =
|
|
141
|
+
variant === 'mobile' ? MobileSurfaceFieldControl : SurfaceFieldControl;
|
|
139
142
|
return (
|
|
140
143
|
<Form
|
|
141
|
-
className=
|
|
144
|
+
className={`oxa-instrument-form oxa-form-full${variant === 'mobile' ? ' oxa-mobile-form' : ''}`}
|
|
142
145
|
form={form}
|
|
143
146
|
initialValues={isCreate ? createDefaults : undefined}
|
|
144
147
|
layout="vertical"
|
|
@@ -153,14 +156,38 @@ export function InstrumentForm({
|
|
|
153
156
|
.map(key => instrumentFields.find(field => field.key === key))
|
|
154
157
|
.filter((field): field is InstrumentField => Boolean(field))
|
|
155
158
|
.map(field => (
|
|
156
|
-
<
|
|
159
|
+
<SurfaceFieldControl
|
|
157
160
|
disabled={
|
|
158
161
|
!instrumentFieldAllowed(field, writeMode, hasCapability)
|
|
159
162
|
}
|
|
160
|
-
field={field}
|
|
163
|
+
field={surfaceField(field.key)}
|
|
161
164
|
key={field.key}
|
|
162
165
|
operation={isCreate ? 'create' : 'update'}
|
|
163
166
|
recordId={record?.id}
|
|
167
|
+
renderers={{
|
|
168
|
+
renderExtra: context =>
|
|
169
|
+
context.disabled
|
|
170
|
+
? '当前角色不可修改此字段'
|
|
171
|
+
: context.field.widget === 'scope'
|
|
172
|
+
? '仅显示当前角色可管理的学院'
|
|
173
|
+
: context.field.key === 'instrumentAdminIds'
|
|
174
|
+
? '管理员选择结果将参与仪器数据权限判断'
|
|
175
|
+
: undefined,
|
|
176
|
+
renderScope: context => (
|
|
177
|
+
<AuthoritativeSelector
|
|
178
|
+
disabled={context.disabled}
|
|
179
|
+
operation={context.operation}
|
|
180
|
+
placeholder={`请选择${context.field.label}`}
|
|
181
|
+
source="college"
|
|
182
|
+
/>
|
|
183
|
+
),
|
|
184
|
+
upload: (field, file, recordId) =>
|
|
185
|
+
instrumentDataApi.upload(
|
|
186
|
+
field.key as 'image' | 'attachments',
|
|
187
|
+
file,
|
|
188
|
+
recordId
|
|
189
|
+
),
|
|
190
|
+
}}
|
|
164
191
|
/>
|
|
165
192
|
))}
|
|
166
193
|
</div>
|
|
@@ -184,253 +211,16 @@ export function InstrumentForm({
|
|
|
184
211
|
);
|
|
185
212
|
}
|
|
186
213
|
|
|
187
|
-
function FieldControl({
|
|
188
|
-
field,
|
|
189
|
-
disabled,
|
|
190
|
-
operation,
|
|
191
|
-
recordId,
|
|
192
|
-
}: {
|
|
193
|
-
field: InstrumentField;
|
|
194
|
-
disabled: boolean;
|
|
195
|
-
operation: 'create' | 'update';
|
|
196
|
-
recordId?: string;
|
|
197
|
-
}) {
|
|
198
|
-
const rules = [
|
|
199
|
-
...(field.required
|
|
200
|
-
? [{ required: true, message: `请填写或选择${field.label}` }]
|
|
201
|
-
: []),
|
|
202
|
-
...(field.kind === 'email'
|
|
203
|
-
? [{ type: 'email' as const, message: '邮箱格式不正确' }]
|
|
204
|
-
: []),
|
|
205
|
-
...(field.kind === 'phone'
|
|
206
|
-
? [{ pattern: /^1\d{10}$/, message: '请输入 11 位手机号' }]
|
|
207
|
-
: []),
|
|
208
|
-
];
|
|
209
|
-
const common = { disabled, placeholder: `请输入${field.label}` };
|
|
210
|
-
let control = <Input {...common} />;
|
|
211
|
-
if (field.kind === 'textarea') {
|
|
212
|
-
control = <Input.TextArea {...common} rows={3} />;
|
|
213
|
-
}
|
|
214
|
-
if (field.kind === 'number') {
|
|
215
|
-
control = <InputNumber {...common} min={0} style={{ width: '100%' }} />;
|
|
216
|
-
}
|
|
217
|
-
if (field.kind === 'date') {
|
|
218
|
-
control = <DatePicker disabled={disabled} style={{ width: '100%' }} />;
|
|
219
|
-
}
|
|
220
|
-
if (field.kind === 'boolean') {
|
|
221
|
-
control = (
|
|
222
|
-
<Switch
|
|
223
|
-
checkedChildren="是"
|
|
224
|
-
disabled={disabled}
|
|
225
|
-
unCheckedChildren="否"
|
|
226
|
-
/>
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
if (field.kind === 'select') {
|
|
230
|
-
control = (
|
|
231
|
-
<Select
|
|
232
|
-
disabled={disabled}
|
|
233
|
-
options={field.options}
|
|
234
|
-
placeholder={`请选择${field.label}`}
|
|
235
|
-
/>
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
if (field.kind === 'multi') {
|
|
239
|
-
control = (
|
|
240
|
-
<Select
|
|
241
|
-
disabled={disabled}
|
|
242
|
-
mode="multiple"
|
|
243
|
-
options={field.options}
|
|
244
|
-
placeholder={`请选择${field.label}`}
|
|
245
|
-
/>
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
if (field.kind === 'scope') {
|
|
249
|
-
control = (
|
|
250
|
-
<AuthoritativeSelector
|
|
251
|
-
disabled={disabled}
|
|
252
|
-
operation={operation}
|
|
253
|
-
placeholder={`请选择${field.label}`}
|
|
254
|
-
source="college"
|
|
255
|
-
/>
|
|
256
|
-
);
|
|
257
|
-
}
|
|
258
|
-
if (
|
|
259
|
-
field.kind === 'directory-user' ||
|
|
260
|
-
field.kind === 'directory-department'
|
|
261
|
-
) {
|
|
262
|
-
control = (
|
|
263
|
-
<PlatformDirectoryPicker
|
|
264
|
-
disabled={disabled}
|
|
265
|
-
kind={field.kind === 'directory-user' ? 'user' : 'department'}
|
|
266
|
-
multiple={field.multiple}
|
|
267
|
-
placeholder={`搜索并选择${field.label}`}
|
|
268
|
-
/>
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
if (field.kind === 'file') {
|
|
272
|
-
control = (
|
|
273
|
-
<FileValue
|
|
274
|
-
accept={field.accept}
|
|
275
|
-
disabled={disabled}
|
|
276
|
-
fieldCode={field.key as 'image' | 'attachments'}
|
|
277
|
-
maxCount={field.maxCount}
|
|
278
|
-
multiple={field.multiple}
|
|
279
|
-
recordId={recordId}
|
|
280
|
-
/>
|
|
281
|
-
);
|
|
282
|
-
}
|
|
283
|
-
const extra = disabled
|
|
284
|
-
? '当前角色不可修改此字段'
|
|
285
|
-
: field.kind === 'scope'
|
|
286
|
-
? '仅显示当前角色可管理的学院'
|
|
287
|
-
: field.key === 'instrumentAdminIds'
|
|
288
|
-
? '管理员选择结果将参与仪器数据权限判断'
|
|
289
|
-
: undefined;
|
|
290
|
-
return (
|
|
291
|
-
<Form.Item
|
|
292
|
-
className={
|
|
293
|
-
field.kind === 'textarea' || field.kind === 'file'
|
|
294
|
-
? 'oxa-field-wide'
|
|
295
|
-
: undefined
|
|
296
|
-
}
|
|
297
|
-
extra={extra}
|
|
298
|
-
label={field.label}
|
|
299
|
-
name={field.key}
|
|
300
|
-
rules={rules}
|
|
301
|
-
valuePropName={field.kind === 'boolean' ? 'checked' : 'value'}
|
|
302
|
-
>
|
|
303
|
-
{control}
|
|
304
|
-
</Form.Item>
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function FileValue({
|
|
309
|
-
value,
|
|
310
|
-
onChange,
|
|
311
|
-
fieldCode,
|
|
312
|
-
recordId,
|
|
313
|
-
disabled,
|
|
314
|
-
multiple,
|
|
315
|
-
maxCount = 1,
|
|
316
|
-
accept,
|
|
317
|
-
}: {
|
|
318
|
-
value?: DataFileRef | DataFileRef[];
|
|
319
|
-
onChange?: (value: DataFileRef | DataFileRef[] | undefined) => void;
|
|
320
|
-
fieldCode: 'image' | 'attachments';
|
|
321
|
-
recordId?: string;
|
|
322
|
-
disabled?: boolean;
|
|
323
|
-
multiple?: boolean;
|
|
324
|
-
maxCount?: number;
|
|
325
|
-
accept?: string;
|
|
326
|
-
}) {
|
|
327
|
-
const [uploading, setUploading] = useState(false);
|
|
328
|
-
const [uploadError, setUploadError] = useState('');
|
|
329
|
-
const refs = Array.isArray(value) ? value : value ? [value] : [];
|
|
330
|
-
const refsRef = useRef(refs);
|
|
331
|
-
useEffect(() => {
|
|
332
|
-
refsRef.current = refs;
|
|
333
|
-
}, [refs]);
|
|
334
|
-
const uploadProps = {
|
|
335
|
-
accept,
|
|
336
|
-
disabled,
|
|
337
|
-
maxCount,
|
|
338
|
-
multiple,
|
|
339
|
-
customRequest: async (options: {
|
|
340
|
-
file: string | Blob;
|
|
341
|
-
onError?: (error: Error) => void;
|
|
342
|
-
onProgress?: (event: { percent: number }) => void;
|
|
343
|
-
onSuccess?: (body: unknown) => void;
|
|
344
|
-
}) => {
|
|
345
|
-
setUploading(true);
|
|
346
|
-
setUploadError('');
|
|
347
|
-
try {
|
|
348
|
-
options.onProgress?.({ percent: 15 });
|
|
349
|
-
const uploaded = await instrumentDataApi.upload(
|
|
350
|
-
fieldCode,
|
|
351
|
-
options.file as File,
|
|
352
|
-
recordId
|
|
353
|
-
);
|
|
354
|
-
options.onProgress?.({ percent: 100 });
|
|
355
|
-
const next = multiple ? [...refsRef.current, uploaded] : uploaded;
|
|
356
|
-
refsRef.current = Array.isArray(next) ? next : [next];
|
|
357
|
-
onChange?.(next);
|
|
358
|
-
options.onSuccess?.(uploaded);
|
|
359
|
-
} catch (error) {
|
|
360
|
-
const failure =
|
|
361
|
-
error instanceof Error ? error : new Error(String(error));
|
|
362
|
-
setUploadError(failure.message);
|
|
363
|
-
options.onError?.(failure);
|
|
364
|
-
} finally {
|
|
365
|
-
setUploading(false);
|
|
366
|
-
}
|
|
367
|
-
},
|
|
368
|
-
showUploadList: false,
|
|
369
|
-
};
|
|
370
|
-
return (
|
|
371
|
-
<div className="oxa-file-field">
|
|
372
|
-
<Upload {...uploadProps} pastable>
|
|
373
|
-
<Button
|
|
374
|
-
disabled={disabled || uploading || refs.length >= maxCount}
|
|
375
|
-
icon={fieldCode === 'image' ? <PictureOutlined /> : <UploadOutlined />}
|
|
376
|
-
loading={uploading}
|
|
377
|
-
>
|
|
378
|
-
{fieldCode === 'image' ? '上传图片' : '上传文件'}
|
|
379
|
-
</Button>
|
|
380
|
-
</Upload>
|
|
381
|
-
<Upload.Dragger
|
|
382
|
-
{...uploadProps}
|
|
383
|
-
className="oxa-file-dropzone"
|
|
384
|
-
openFileDialogOnClick={false}
|
|
385
|
-
pastable
|
|
386
|
-
>
|
|
387
|
-
<span>
|
|
388
|
-
可将文件拖到这里,或在此区域按 <kbd>⌘ V</kbd> 粘贴
|
|
389
|
-
</span>
|
|
390
|
-
<small>
|
|
391
|
-
{multiple
|
|
392
|
-
? `最多 ${maxCount} 个文件,单个不超过 50MB`
|
|
393
|
-
: '仅保留一个文件,新上传会替换当前文件'}
|
|
394
|
-
</small>
|
|
395
|
-
</Upload.Dragger>
|
|
396
|
-
{uploadError && (
|
|
397
|
-
<div className="oxa-file-error">
|
|
398
|
-
<span>{uploadError}</span>
|
|
399
|
-
<Button
|
|
400
|
-
icon={<ReloadOutlined />}
|
|
401
|
-
onClick={() => setUploadError('')}
|
|
402
|
-
size="small"
|
|
403
|
-
type="text"
|
|
404
|
-
>
|
|
405
|
-
关闭
|
|
406
|
-
</Button>
|
|
407
|
-
</div>
|
|
408
|
-
)}
|
|
409
|
-
{refs.length ? (
|
|
410
|
-
<AttachmentFileList
|
|
411
|
-
files={refs}
|
|
412
|
-
onRemove={file => {
|
|
413
|
-
const next = refs.filter(item => item.id !== file.id);
|
|
414
|
-
refsRef.current = next;
|
|
415
|
-
onChange?.(multiple ? next : undefined);
|
|
416
|
-
}}
|
|
417
|
-
removable={!disabled}
|
|
418
|
-
/>
|
|
419
|
-
) : (
|
|
420
|
-
<div className="oxa-file-empty">尚未上传文件</div>
|
|
421
|
-
)}
|
|
422
|
-
</div>
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
214
|
function Detail({
|
|
427
215
|
record,
|
|
428
216
|
auditEntries,
|
|
429
217
|
auditError,
|
|
218
|
+
variant = 'desktop',
|
|
430
219
|
}: {
|
|
431
220
|
record: InstrumentRecord;
|
|
432
221
|
auditEntries: DataAuditEntry[];
|
|
433
222
|
auditError?: string;
|
|
223
|
+
variant?: 'desktop' | 'mobile';
|
|
434
224
|
}) {
|
|
435
225
|
const groups = Object.entries(sectionTitles)
|
|
436
226
|
.map(([section, title]) => ({
|
|
@@ -445,7 +235,7 @@ function Detail({
|
|
|
445
235
|
}))
|
|
446
236
|
.filter(group => group.fields.length > 0);
|
|
447
237
|
return (
|
|
448
|
-
<div className=
|
|
238
|
+
<div className={`oxa-detail-layout${variant === 'mobile' ? ' oxa-mobile-detail' : ''}`}>
|
|
449
239
|
<div className="oxa-detail-main">
|
|
450
240
|
{groups.map(group => (
|
|
451
241
|
<Card className="oxa-section-card" key={group.section} title={group.title}>
|
|
@@ -455,8 +245,49 @@ function Detail({
|
|
|
455
245
|
key: field.key,
|
|
456
246
|
label: field.label,
|
|
457
247
|
span:
|
|
458
|
-
field.
|
|
459
|
-
|
|
248
|
+
surfaceField(field.key).widget === 'textarea' ||
|
|
249
|
+
surfaceField(field.key).widget === 'file'
|
|
250
|
+
? 3
|
|
251
|
+
: 1,
|
|
252
|
+
children: (
|
|
253
|
+
<SurfaceFieldValue
|
|
254
|
+
field={surfaceField(field.key)}
|
|
255
|
+
renderers={{
|
|
256
|
+
renderValue: ({ field: surface, value }) => {
|
|
257
|
+
if (surface.widget === 'scope') {
|
|
258
|
+
return (
|
|
259
|
+
<ResolvedValueText
|
|
260
|
+
source="college"
|
|
261
|
+
value={String(value)}
|
|
262
|
+
/>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
if (
|
|
266
|
+
surface.key === 'instrumentStatus' ||
|
|
267
|
+
surface.key === 'usageStatus'
|
|
268
|
+
) {
|
|
269
|
+
const label =
|
|
270
|
+
surface.options?.find(
|
|
271
|
+
option => option.value === value
|
|
272
|
+
)?.label || String(value);
|
|
273
|
+
return (
|
|
274
|
+
<Tag
|
|
275
|
+
color={
|
|
276
|
+
value === 'normal' || value === 'active'
|
|
277
|
+
? 'green'
|
|
278
|
+
: 'default'
|
|
279
|
+
}
|
|
280
|
+
>
|
|
281
|
+
{label}
|
|
282
|
+
</Tag>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
return undefined;
|
|
286
|
+
},
|
|
287
|
+
}}
|
|
288
|
+
value={record[field.key]}
|
|
289
|
+
/>
|
|
290
|
+
),
|
|
460
291
|
}))}
|
|
461
292
|
/>
|
|
462
293
|
</Card>
|
|
@@ -476,11 +307,30 @@ function Detail({
|
|
|
476
307
|
{
|
|
477
308
|
key: 'status',
|
|
478
309
|
label: '仪器状态',
|
|
479
|
-
children:
|
|
480
|
-
|
|
481
|
-
field
|
|
482
|
-
|
|
483
|
-
|
|
310
|
+
children: (
|
|
311
|
+
<SurfaceFieldValue
|
|
312
|
+
field={surfaceField('instrumentStatus')}
|
|
313
|
+
renderers={{
|
|
314
|
+
renderValue: ({ field, value }) => {
|
|
315
|
+
const label =
|
|
316
|
+
field.options?.find(
|
|
317
|
+
option => option.value === value
|
|
318
|
+
)?.label || String(value);
|
|
319
|
+
return (
|
|
320
|
+
<Tag
|
|
321
|
+
color={
|
|
322
|
+
value === 'normal' || value === 'active'
|
|
323
|
+
? 'green'
|
|
324
|
+
: 'default'
|
|
325
|
+
}
|
|
326
|
+
>
|
|
327
|
+
{label}
|
|
328
|
+
</Tag>
|
|
329
|
+
);
|
|
330
|
+
},
|
|
331
|
+
}}
|
|
332
|
+
value={record.instrumentStatus}
|
|
333
|
+
/>
|
|
484
334
|
),
|
|
485
335
|
},
|
|
486
336
|
]}
|
|
@@ -512,54 +362,9 @@ function Detail({
|
|
|
512
362
|
);
|
|
513
363
|
}
|
|
514
364
|
|
|
515
|
-
function
|
|
516
|
-
|
|
517
|
-
return
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
function displayValue(field: InstrumentField, value: unknown) {
|
|
521
|
-
if (value === undefined || value === null || value === '') return '-';
|
|
522
|
-
if (field.kind === 'scope') {
|
|
523
|
-
return <ResolvedValueText source="college" value={String(value)} />;
|
|
524
|
-
}
|
|
525
|
-
if (field.kind === 'directory-user') {
|
|
526
|
-
return (
|
|
527
|
-
<ResolvedValueText
|
|
528
|
-
source="user"
|
|
529
|
-
value={Array.isArray(value) ? value.map(String) : String(value)}
|
|
530
|
-
/>
|
|
531
|
-
);
|
|
532
|
-
}
|
|
533
|
-
if (field.kind === 'directory-department') {
|
|
534
|
-
return <ResolvedValueText source="department" value={String(value)} />;
|
|
535
|
-
}
|
|
536
|
-
if (field.kind === 'boolean') {
|
|
537
|
-
return <Tag color={value ? 'green' : 'default'}>{value ? '是' : '否'}</Tag>;
|
|
538
|
-
}
|
|
539
|
-
if (field.kind === 'file') {
|
|
540
|
-
const values = (Array.isArray(value) ? value : [value]).filter(
|
|
541
|
-
(item): item is DataFileRef =>
|
|
542
|
-
Boolean(item && typeof item === 'object' && 'id' in item)
|
|
543
|
-
);
|
|
544
|
-
return <AttachmentReadOnly values={values} />;
|
|
545
|
-
}
|
|
546
|
-
if (Array.isArray(value)) {
|
|
547
|
-
return (
|
|
548
|
-
value
|
|
549
|
-
.map(
|
|
550
|
-
item =>
|
|
551
|
-
field.options?.find(option => option.value === item)?.label ||
|
|
552
|
-
String(item)
|
|
553
|
-
)
|
|
554
|
-
.join('、') || '-'
|
|
555
|
-
);
|
|
556
|
-
}
|
|
557
|
-
const label =
|
|
558
|
-
field.options?.find(option => option.value === value)?.label || String(value);
|
|
559
|
-
if (field.key === 'instrumentStatus' || field.key === 'usageStatus') {
|
|
560
|
-
return <Tag color={value === 'normal' || value === 'active' ? 'green' : 'default'}>{label}</Tag>;
|
|
561
|
-
}
|
|
562
|
-
return label;
|
|
365
|
+
function surfaceField(key: string): SurfaceField {
|
|
366
|
+
const field = instrumentSurface.fields[key];
|
|
367
|
+
return field ? { key, ...field } : { key, label: key, widget: 'text' };
|
|
563
368
|
}
|
|
564
369
|
|
|
565
370
|
function auditLabel(operation: DataAuditEntry['operation']) {
|
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
import { ArrowLeftOutlined, EyeOutlined } from '@ant-design/icons';
|
|
2
1
|
import { useCreate, useOne, useUpdate } from '@refinedev/core';
|
|
3
|
-
import {
|
|
2
|
+
import { App } from 'antd';
|
|
4
3
|
import { useNavigate, useParams } from 'react-router-dom';
|
|
4
|
+
import {
|
|
5
|
+
MobileResourceFormPage,
|
|
6
|
+
ResourceFormPage,
|
|
7
|
+
} from './components/resource/StandardResourcePages';
|
|
5
8
|
import { InstrumentForm } from './InstrumentForm';
|
|
6
|
-
import { Shell } from './Shell';
|
|
7
9
|
import { INSTRUMENT_CAPABILITIES, type InstrumentRecord } from './instrument';
|
|
8
|
-
import {
|
|
10
|
+
import { instrumentSurface } from '../../../platform/data/instrument-surface.js';
|
|
9
11
|
|
|
10
|
-
export function InstrumentFormPage({
|
|
12
|
+
export function InstrumentFormPage({
|
|
13
|
+
mode,
|
|
14
|
+
variant = 'desktop',
|
|
15
|
+
}: {
|
|
16
|
+
mode: 'create' | 'edit';
|
|
17
|
+
variant?: 'desktop' | 'mobile';
|
|
18
|
+
}) {
|
|
11
19
|
const { id = '' } = useParams();
|
|
12
20
|
const navigate = useNavigate();
|
|
13
|
-
const { hasCapability } = useRuntime();
|
|
14
21
|
const { message } = App.useApp();
|
|
15
22
|
const create = useCreate();
|
|
16
23
|
const update = useUpdate();
|
|
@@ -20,76 +27,34 @@ export function InstrumentFormPage({ mode }: { mode: 'create' | 'edit' }) {
|
|
|
20
27
|
queryOptions: { enabled: mode === 'edit' },
|
|
21
28
|
});
|
|
22
29
|
const record = query.result;
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
: hasCapability(INSTRUMENT_CAPABILITIES.update);
|
|
27
|
-
if (!allowed) {
|
|
28
|
-
return (
|
|
29
|
-
<Shell>
|
|
30
|
-
<Result status="403" title="当前平台用户无此操作权限" />
|
|
31
|
-
</Shell>
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
if (mode === 'edit' && query.query.isLoading) {
|
|
35
|
-
return (
|
|
36
|
-
<Shell>
|
|
37
|
-
<div className="oxa-page-loading">
|
|
38
|
-
<Spin />
|
|
39
|
-
</div>
|
|
40
|
-
</Shell>
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
if (mode === 'edit' && !record) {
|
|
44
|
-
return (
|
|
45
|
-
<Shell>
|
|
46
|
-
<Result status="404" title="仪器不存在或不在数据范围内" />
|
|
47
|
-
</Shell>
|
|
48
|
-
);
|
|
49
|
-
}
|
|
30
|
+
const Page = variant === 'mobile' ? MobileResourceFormPage : ResourceFormPage;
|
|
31
|
+
const listPath = variant === 'mobile' ? '/m/instruments' : '/instruments';
|
|
32
|
+
const detailPath = variant === 'mobile' ? `/m/instruments/${id}` : `/instruments/${id}`;
|
|
50
33
|
return (
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
{record.revision}
|
|
71
|
-
</Typography.Text>
|
|
72
|
-
)}
|
|
73
|
-
</div>
|
|
74
|
-
{record && (
|
|
75
|
-
<Button
|
|
76
|
-
icon={<EyeOutlined />}
|
|
77
|
-
onClick={() => navigate(`/instruments/${id}`)}
|
|
78
|
-
>
|
|
79
|
-
查看详情
|
|
80
|
-
</Button>
|
|
81
|
-
)}
|
|
82
|
-
</div>
|
|
83
|
-
<Alert
|
|
84
|
-
title="仅显示当前角色可编辑的字段;保存时将校验数据版本与权限范围。"
|
|
85
|
-
showIcon
|
|
86
|
-
type="info"
|
|
87
|
-
/>
|
|
88
|
-
</Card>
|
|
34
|
+
<Page
|
|
35
|
+
backLabel={mode === 'edit' ? '返回仪器详情' : '返回仪器资源'}
|
|
36
|
+
backPath={mode === 'edit' ? detailPath : listPath}
|
|
37
|
+
capability={
|
|
38
|
+
mode === 'create'
|
|
39
|
+
? INSTRUMENT_CAPABILITIES.create
|
|
40
|
+
: INSTRUMENT_CAPABILITIES.update
|
|
41
|
+
}
|
|
42
|
+
detailPath={record ? detailPath : undefined}
|
|
43
|
+
loading={mode === 'edit' && query.query.isLoading}
|
|
44
|
+
mode={mode}
|
|
45
|
+
notFound={mode === 'edit' && !record}
|
|
46
|
+
recordSummary={
|
|
47
|
+
record && `${record.chineseName} · ${record.instrumentCode} · 当前版本 ${record.revision}`
|
|
48
|
+
}
|
|
49
|
+
resource="instruments"
|
|
50
|
+
surface={instrumentSurface}
|
|
51
|
+
title="仪器"
|
|
52
|
+
>
|
|
89
53
|
<InstrumentForm
|
|
90
54
|
mode={mode}
|
|
55
|
+
variant={variant}
|
|
91
56
|
onCancel={() =>
|
|
92
|
-
navigate(mode === 'edit' ?
|
|
57
|
+
navigate(mode === 'edit' ? detailPath : listPath)
|
|
93
58
|
}
|
|
94
59
|
onSubmit={async values => {
|
|
95
60
|
if (mode === 'create') {
|
|
@@ -98,7 +63,7 @@ export function InstrumentFormPage({ mode }: { mode: 'create' | 'edit' }) {
|
|
|
98
63
|
values,
|
|
99
64
|
});
|
|
100
65
|
message.success('仪器已创建');
|
|
101
|
-
navigate(
|
|
66
|
+
navigate(listPath);
|
|
102
67
|
} else {
|
|
103
68
|
const result = await update.mutateAsync({
|
|
104
69
|
resource: 'instruments',
|
|
@@ -107,11 +72,11 @@ export function InstrumentFormPage({ mode }: { mode: 'create' | 'edit' }) {
|
|
|
107
72
|
meta: { expectedRevision: record!.revision },
|
|
108
73
|
});
|
|
109
74
|
message.success('修改已保存');
|
|
110
|
-
navigate(`/instruments/${result.data.id}`);
|
|
75
|
+
navigate(variant === 'mobile' ? `/m/instruments/${result.data.id}` : `/instruments/${result.data.id}`);
|
|
111
76
|
}
|
|
112
77
|
}}
|
|
113
78
|
record={record}
|
|
114
79
|
/>
|
|
115
|
-
</
|
|
80
|
+
</Page>
|
|
116
81
|
);
|
|
117
82
|
}
|