openxiangda-cli 2.0.0-alpha.60 → 2.0.0-alpha.62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/create-workspace.d.ts +1 -0
  2. package/dist/create-workspace.d.ts.map +1 -1
  3. package/dist/create-workspace.js +1 -0
  4. package/dist/create-workspace.js.map +1 -1
  5. package/package.json +4 -4
  6. package/template/AGENTS.md +1 -1
  7. package/template/README.md +7 -7
  8. package/template/apps/server/package.json +1 -1
  9. package/template/apps/server/src/app.module.ts +1 -3
  10. package/template/apps/server/test/smoke.test.ts +6 -23
  11. package/template/apps/web/e2e/resources.spec.ts +20 -0
  12. package/template/apps/web/index.html +1 -1
  13. package/template/apps/web/package.json +1 -1
  14. package/template/apps/web/scripts/check.mjs +5 -2
  15. package/template/apps/web/src/AuthoritativeSelector.tsx +38 -29
  16. package/template/apps/web/src/Shell.tsx +39 -211
  17. package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +1 -1
  18. package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +306 -0
  19. package/template/apps/web/src/components/resource/StandardResourcePages.tsx +431 -0
  20. package/template/apps/web/src/components/resource/SurfaceFields.tsx +500 -0
  21. package/template/apps/web/src/data-provider.ts +36 -31
  22. package/template/apps/web/src/main.tsx +24 -54
  23. package/template/apps/web/src/platform-client.ts +131 -382
  24. package/template/apps/web/src/runtime-meta.ts +1 -1
  25. package/template/apps/web/src/styles.css +100 -3
  26. package/template/apps/web/test/contracts.test.ts +31 -693
  27. package/template/openxiangda.config.ts +14 -81
  28. package/template/package.json +3 -3
  29. package/template/packages/contracts/src/generated.ts +7 -130
  30. package/template/apps/server/src/instrument-context.controller.ts +0 -25
  31. package/template/apps/web/e2e/instruments.spec.ts +0 -1338
  32. package/template/apps/web/src/CollegePage.tsx +0 -181
  33. package/template/apps/web/src/InstrumentDetailPage.tsx +0 -134
  34. package/template/apps/web/src/InstrumentForm.tsx +0 -575
  35. package/template/apps/web/src/InstrumentFormPage.tsx +0 -117
  36. package/template/apps/web/src/InstrumentListPage.tsx +0 -385
  37. package/template/apps/web/src/fields.ts +0 -83
  38. package/template/apps/web/src/instrument.ts +0 -55
  39. package/template/platform/data/colleges.ts +0 -21
  40. package/template/platform/data/instruments.ts +0 -81
@@ -1,575 +0,0 @@
1
- import {
2
- PictureOutlined,
3
- ReloadOutlined,
4
- UploadOutlined,
5
- } from '@ant-design/icons';
6
- import {
7
- App,
8
- Button,
9
- Card,
10
- DatePicker,
11
- Descriptions,
12
- Form,
13
- Input,
14
- InputNumber,
15
- Select,
16
- Switch,
17
- Tag,
18
- Timeline,
19
- Typography,
20
- Upload,
21
- } from 'antd';
22
- import dayjs from 'dayjs';
23
- import { useEffect, useRef, useState } from 'react';
24
- import type { DataAuditEntry, DataFileRef } from 'openxiangda-contracts/browser';
25
- import {
26
- AuthoritativeSelector,
27
- ResolvedValueText,
28
- } from './AuthoritativeSelector';
29
- import { AttachmentFileList } from './components/platform-fields/AttachmentFileList';
30
- import { PlatformDirectoryPicker } from './components/platform-fields/PlatformDirectoryPicker';
31
- import { instrumentFields, sectionTitles, type InstrumentField } from './fields';
32
- import type { InstrumentRecord } from './instrument';
33
- import { instrumentDataApi } from './platform-client';
34
- import { useRuntime } from './runtime';
35
-
36
- type Values = Record<string, unknown>;
37
- const createDefaults = {
38
- openToOutside: false,
39
- instrumentStatus: 'normal',
40
- usageStatus: 'active',
41
- } as const;
42
-
43
- export function instrumentFieldAllowed(
44
- field: InstrumentField,
45
- mode: 'create' | 'edit',
46
- hasCapability: (capability: string) => boolean
47
- ) {
48
- const capability =
49
- mode === 'create' ? field.createCapability : field.updateCapability;
50
- return !capability || hasCapability(capability);
51
- }
52
-
53
- export function sanitizeInstrumentValues(
54
- values: Values,
55
- mode: 'create' | 'edit',
56
- hasCapability: (capability: string) => boolean
57
- ): Values {
58
- return Object.fromEntries(
59
- instrumentFields
60
- .filter(
61
- field =>
62
- !field.system && instrumentFieldAllowed(field, mode, hasCapability)
63
- )
64
- .filter(field => Object.prototype.hasOwnProperty.call(values, field.key))
65
- .map(field => [field.key, values[field.key]])
66
- );
67
- }
68
-
69
- export function InstrumentForm({
70
- mode,
71
- record,
72
- onCancel,
73
- onSubmit,
74
- auditEntries = [],
75
- auditError,
76
- }: {
77
- mode: 'create' | 'edit' | 'detail';
78
- record?: InstrumentRecord;
79
- onCancel: () => void;
80
- onSubmit?: (values: Values) => Promise<void>;
81
- auditEntries?: DataAuditEntry[];
82
- auditError?: string;
83
- }) {
84
- const [form] = Form.useForm();
85
- const { message } = App.useApp();
86
- const { hasCapability } = useRuntime();
87
- const [submitting, setSubmitting] = useState(false);
88
- useEffect(() => {
89
- if (record) {
90
- form.setFieldsValue({
91
- ...record,
92
- enabledDate: record.enabledDate ? dayjs(record.enabledDate) : undefined,
93
- });
94
- }
95
- }, [form, record]);
96
- if (mode === 'detail' && record) {
97
- return (
98
- <Detail
99
- auditEntries={auditEntries}
100
- auditError={auditError}
101
- record={record}
102
- />
103
- );
104
- }
105
- const isCreate = mode === 'create';
106
- const writeMode = isCreate ? 'create' : 'edit';
107
- const submit = async (values: Values) => {
108
- if (submitting) return;
109
- const normalized = {
110
- ...(isCreate ? createDefaults : {}),
111
- ...values,
112
- enabledDate:
113
- values.enabledDate &&
114
- typeof values.enabledDate === 'object' &&
115
- 'format' in values.enabledDate
116
- ? (values.enabledDate as { format(pattern: string): string }).format(
117
- 'YYYY-MM-DD'
118
- )
119
- : values.enabledDate,
120
- };
121
- setSubmitting(true);
122
- try {
123
- await onSubmit?.(
124
- sanitizeInstrumentValues(normalized, writeMode, hasCapability)
125
- );
126
- } catch (error) {
127
- message.error(error instanceof Error ? error.message : String(error));
128
- } finally {
129
- setSubmitting(false);
130
- }
131
- };
132
- const groups = Object.entries(sectionTitles).map(([key, title]) => ({
133
- key,
134
- title,
135
- fields: instrumentFields
136
- .filter(field => field.section === key && !field.system)
137
- .map(field => field.key),
138
- }));
139
- return (
140
- <Form
141
- className="oxa-instrument-form oxa-form-full"
142
- form={form}
143
- initialValues={isCreate ? createDefaults : undefined}
144
- layout="vertical"
145
- onFinish={values => void submit(values)}
146
- scrollToFirstError={{ block: 'center' }}
147
- >
148
- <div className="oxa-sections">
149
- {groups.map(group => (
150
- <Card className="oxa-section-card" key={group.key} title={group.title}>
151
- <div className="oxa-grid">
152
- {group.fields
153
- .map(key => instrumentFields.find(field => field.key === key))
154
- .filter((field): field is InstrumentField => Boolean(field))
155
- .map(field => (
156
- <FieldControl
157
- disabled={
158
- !instrumentFieldAllowed(field, writeMode, hasCapability)
159
- }
160
- field={field}
161
- key={field.key}
162
- operation={isCreate ? 'create' : 'update'}
163
- recordId={record?.id}
164
- />
165
- ))}
166
- </div>
167
- </Card>
168
- ))}
169
- </div>
170
- <div className="oxa-actions">
171
- <Button disabled={submitting} onClick={onCancel}>
172
- 取消
173
- </Button>
174
- <Button
175
- data-testid="save-instrument"
176
- htmlType="submit"
177
- loading={submitting}
178
- type="primary"
179
- >
180
- {isCreate ? '保存' : '保存修改'}
181
- </Button>
182
- </div>
183
- </Form>
184
- );
185
- }
186
-
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
- function Detail({
427
- record,
428
- auditEntries,
429
- auditError,
430
- }: {
431
- record: InstrumentRecord;
432
- auditEntries: DataAuditEntry[];
433
- auditError?: string;
434
- }) {
435
- const groups = Object.entries(sectionTitles)
436
- .map(([section, title]) => ({
437
- section,
438
- title,
439
- fields: instrumentFields.filter(
440
- field =>
441
- field.section === section &&
442
- !field.system &&
443
- Object.prototype.hasOwnProperty.call(record, field.key)
444
- ),
445
- }))
446
- .filter(group => group.fields.length > 0);
447
- return (
448
- <div className="oxa-detail-layout">
449
- <div className="oxa-detail-main">
450
- {groups.map(group => (
451
- <Card className="oxa-section-card" key={group.section} title={group.title}>
452
- <Descriptions
453
- column={{ xs: 1, sm: 2, lg: 3 }}
454
- items={group.fields.map(field => ({
455
- key: field.key,
456
- label: field.label,
457
- span:
458
- field.kind === 'textarea' || field.kind === 'file' ? 3 : 1,
459
- children: displayValue(field, record[field.key]),
460
- }))}
461
- />
462
- </Card>
463
- ))}
464
- </div>
465
- <aside className="oxa-detail-aside">
466
- <Card className="oxa-section-card" title="记录信息">
467
- <Descriptions
468
- column={1}
469
- items={[
470
- { key: 'revision', label: '数据版本', children: record.revision },
471
- {
472
- key: 'updatedAt',
473
- label: '更新时间',
474
- children: formatDateTime(record.updated_at),
475
- },
476
- {
477
- key: 'status',
478
- label: '仪器状态',
479
- children: displayValue(
480
- instrumentFields.find(
481
- field => field.key === 'instrumentStatus'
482
- )!,
483
- record.instrumentStatus
484
- ),
485
- },
486
- ]}
487
- />
488
- </Card>
489
- <Card className="oxa-section-card" id="instrument-audit" title="最近变更">
490
- {auditError ? (
491
- <Typography.Text type="secondary">审计记录暂不可用</Typography.Text>
492
- ) : auditEntries.length ? (
493
- <Timeline
494
- items={auditEntries.slice(0, 5).map(entry => ({
495
- children: (
496
- <div className="oxa-audit-entry">
497
- <strong>{auditLabel(entry.operation)}</strong>
498
- <span>{formatDateTime(entry.occurredAt)}</span>
499
- {entry.actor.userId && (
500
- <ResolvedValueText source="user" value={entry.actor.userId} />
501
- )}
502
- </div>
503
- ),
504
- }))}
505
- />
506
- ) : (
507
- <Typography.Text type="secondary">暂无变更记录</Typography.Text>
508
- )}
509
- </Card>
510
- </aside>
511
- </div>
512
- );
513
- }
514
-
515
- function AttachmentReadOnly({ values }: { values: DataFileRef[] }) {
516
- if (!values.length) return <>-</>;
517
- return <AttachmentFileList files={values} />;
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;
563
- }
564
-
565
- function auditLabel(operation: DataAuditEntry['operation']) {
566
- return {
567
- created: '创建仪器档案',
568
- updated: '更新仪器信息',
569
- deleted: '删除仪器档案',
570
- }[operation];
571
- }
572
-
573
- function formatDateTime(value?: string) {
574
- return value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '-';
575
- }
@@ -1,117 +0,0 @@
1
- import { ArrowLeftOutlined, EyeOutlined } from '@ant-design/icons';
2
- import { useCreate, useOne, useUpdate } from '@refinedev/core';
3
- import { Alert, App, Button, Card, Result, Space, Spin, Typography } from 'antd';
4
- import { useNavigate, useParams } from 'react-router-dom';
5
- import { InstrumentForm } from './InstrumentForm';
6
- import { Shell } from './Shell';
7
- import { INSTRUMENT_CAPABILITIES, type InstrumentRecord } from './instrument';
8
- import { useRuntime } from './runtime';
9
-
10
- export function InstrumentFormPage({ mode }: { mode: 'create' | 'edit' }) {
11
- const { id = '' } = useParams();
12
- const navigate = useNavigate();
13
- const { hasCapability } = useRuntime();
14
- const { message } = App.useApp();
15
- const create = useCreate();
16
- const update = useUpdate();
17
- const query = useOne<InstrumentRecord>({
18
- resource: 'instruments',
19
- id,
20
- queryOptions: { enabled: mode === 'edit' },
21
- });
22
- const record = query.result;
23
- const allowed =
24
- mode === 'create'
25
- ? hasCapability(INSTRUMENT_CAPABILITIES.create)
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
- }
50
- return (
51
- <Shell>
52
- <Card className="oxa-edit-hero">
53
- <div className="oxa-edit-hero-title">
54
- <div>
55
- <Button
56
- icon={<ArrowLeftOutlined />}
57
- onClick={() =>
58
- navigate(mode === 'edit' ? `/instruments/${id}` : '/instruments')
59
- }
60
- type="link"
61
- >
62
- {mode === 'edit' ? '返回仪器详情' : '返回仪器资源'}
63
- </Button>
64
- <Typography.Title level={2}>
65
- {mode === 'create' ? '新增仪器' : '编辑仪器'}
66
- </Typography.Title>
67
- {record && (
68
- <Typography.Text type="secondary">
69
- {record.chineseName} · {record.instrumentCode} · 当前版本{' '}
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>
89
- <InstrumentForm
90
- mode={mode}
91
- onCancel={() =>
92
- navigate(mode === 'edit' ? `/instruments/${id}` : '/instruments')
93
- }
94
- onSubmit={async values => {
95
- if (mode === 'create') {
96
- await create.mutateAsync({
97
- resource: 'instruments',
98
- values,
99
- });
100
- message.success('仪器已创建');
101
- navigate('/instruments');
102
- } else {
103
- const result = await update.mutateAsync({
104
- resource: 'instruments',
105
- id,
106
- values,
107
- meta: { expectedRevision: record!.revision },
108
- });
109
- message.success('修改已保存');
110
- navigate(`/instruments/${result.data.id}`);
111
- }
112
- }}
113
- record={record}
114
- />
115
- </Shell>
116
- );
117
- }