openxiangda-cli 2.0.0-alpha.57 → 2.0.0-alpha.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/commands/create.d.ts.map +1 -1
  2. package/dist/commands/create.js +13 -5
  3. package/dist/commands/create.js.map +1 -1
  4. package/dist/create-workspace.d.ts +6 -1
  5. package/dist/create-workspace.d.ts.map +1 -1
  6. package/dist/create-workspace.js +12 -9
  7. package/dist/create-workspace.js.map +1 -1
  8. package/package.json +4 -4
  9. package/template/README.md +3 -1
  10. package/template/apps/server/package.json +1 -1
  11. package/template/apps/web/e2e/instruments.spec.ts +1197 -9
  12. package/template/apps/web/package.json +4 -2
  13. package/template/apps/web/scripts/check.mjs +2 -2
  14. package/template/apps/web/scripts/verify-build.mjs +14 -2
  15. package/template/apps/web/src/AuthoritativeSelector.tsx +371 -0
  16. package/template/apps/web/src/CollegePage.tsx +181 -0
  17. package/template/apps/web/src/FilePreviewPage.tsx +303 -0
  18. package/template/apps/web/src/InstrumentDetailPage.tsx +94 -20
  19. package/template/apps/web/src/InstrumentForm.tsx +347 -94
  20. package/template/apps/web/src/InstrumentFormPage.tsx +49 -9
  21. package/template/apps/web/src/InstrumentListPage.tsx +30 -35
  22. package/template/apps/web/src/Shell.tsx +229 -41
  23. package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +226 -0
  24. package/template/apps/web/src/components/platform-fields/PlatformDirectoryPicker.tsx +698 -0
  25. package/template/apps/web/src/data-provider.ts +1 -1
  26. package/template/apps/web/src/fields.ts +17 -6
  27. package/template/apps/web/src/instrument.ts +1 -1
  28. package/template/apps/web/src/main.tsx +21 -1
  29. package/template/apps/web/src/platform-client.ts +248 -2
  30. package/template/apps/web/src/runtime-meta.ts +15 -0
  31. package/template/apps/web/src/runtime.tsx +4 -2
  32. package/template/apps/web/src/styles.css +1007 -28
  33. package/template/apps/web/test/contracts.test.ts +473 -10
  34. package/template/openxiangda.config.ts +24 -5
  35. package/template/package.json +3 -3
  36. package/template/packages/contracts/src/generated.ts +22 -0
  37. package/template/platform/data/colleges.ts +21 -0
  38. package/template/platform/data/instruments.ts +1 -1
  39. package/template/scripts/verify-template-budget.mjs +2 -2
@@ -1,4 +1,10 @@
1
1
  import {
2
+ PictureOutlined,
3
+ ReloadOutlined,
4
+ UploadOutlined,
5
+ } from '@ant-design/icons';
6
+ import {
7
+ App,
2
8
  Button,
3
9
  Card,
4
10
  DatePicker,
@@ -8,19 +14,31 @@ import {
8
14
  InputNumber,
9
15
  Select,
10
16
  Switch,
17
+ Tag,
18
+ Timeline,
19
+ Typography,
11
20
  Upload,
12
- type UploadFile,
13
21
  } from 'antd';
14
- import { UploadOutlined } from '@ant-design/icons';
15
22
  import dayjs from 'dayjs';
16
- import { useEffect } from 'react';
17
- import type { DataFileRef } from 'openxiangda-contracts/browser';
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';
18
31
  import { instrumentFields, sectionTitles, type InstrumentField } from './fields';
19
32
  import type { InstrumentRecord } from './instrument';
20
33
  import { instrumentDataApi } from './platform-client';
21
34
  import { useRuntime } from './runtime';
22
35
 
23
36
  type Values = Record<string, unknown>;
37
+ const createDefaults = {
38
+ openToOutside: false,
39
+ instrumentStatus: 'normal',
40
+ usageStatus: 'active',
41
+ } as const;
24
42
 
25
43
  export function instrumentFieldAllowed(
26
44
  field: InstrumentField,
@@ -53,14 +71,20 @@ export function InstrumentForm({
53
71
  record,
54
72
  onCancel,
55
73
  onSubmit,
74
+ auditEntries = [],
75
+ auditError,
56
76
  }: {
57
77
  mode: 'create' | 'edit' | 'detail';
58
78
  record?: InstrumentRecord;
59
79
  onCancel: () => void;
60
80
  onSubmit?: (values: Values) => Promise<void>;
81
+ auditEntries?: DataAuditEntry[];
82
+ auditError?: string;
61
83
  }) {
62
84
  const [form] = Form.useForm();
85
+ const { message } = App.useApp();
63
86
  const { hasCapability } = useRuntime();
87
+ const [submitting, setSubmitting] = useState(false);
64
88
  useEffect(() => {
65
89
  if (record) {
66
90
  form.setFieldsValue({
@@ -69,12 +93,21 @@ export function InstrumentForm({
69
93
  });
70
94
  }
71
95
  }, [form, record]);
72
- if (mode === 'detail' && record) return <Detail record={record} />;
96
+ if (mode === 'detail' && record) {
97
+ return (
98
+ <Detail
99
+ auditEntries={auditEntries}
100
+ auditError={auditError}
101
+ record={record}
102
+ />
103
+ );
104
+ }
73
105
  const isCreate = mode === 'create';
74
106
  const writeMode = isCreate ? 'create' : 'edit';
75
- const submit = async () => {
76
- const values = await form.validateFields();
107
+ const submit = async (values: Values) => {
108
+ if (submitting) return;
77
109
  const normalized = {
110
+ ...(isCreate ? createDefaults : {}),
78
111
  ...values,
79
112
  enabledDate:
80
113
  values.enabledDate &&
@@ -85,37 +118,48 @@ export function InstrumentForm({
85
118
  )
86
119
  : values.enabledDate,
87
120
  };
88
- await onSubmit?.(
89
- sanitizeInstrumentValues(normalized, writeMode, hasCapability)
90
- );
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
+ }
91
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
+ }));
92
139
  return (
93
140
  <Form
141
+ className="oxa-instrument-form oxa-form-full"
94
142
  form={form}
143
+ initialValues={isCreate ? createDefaults : undefined}
95
144
  layout="vertical"
96
- initialValues={
97
- isCreate
98
- ? {
99
- openToOutside: false,
100
- instrumentStatus: 'normal',
101
- usageStatus: 'active',
102
- }
103
- : undefined
104
- }
145
+ onFinish={values => void submit(values)}
146
+ scrollToFirstError={{ block: 'center' }}
105
147
  >
106
148
  <div className="oxa-sections">
107
- {Object.entries(sectionTitles).map(([section, title]) => (
108
- <Card key={section} title={title}>
149
+ {groups.map(group => (
150
+ <Card className="oxa-section-card" key={group.key} title={group.title}>
109
151
  <div className="oxa-grid">
110
- {instrumentFields
111
- .filter(field => field.section === section && !field.system)
152
+ {group.fields
153
+ .map(key => instrumentFields.find(field => field.key === key))
154
+ .filter((field): field is InstrumentField => Boolean(field))
112
155
  .map(field => (
113
156
  <FieldControl
114
- field={field}
115
- key={field.key}
116
157
  disabled={
117
158
  !instrumentFieldAllowed(field, writeMode, hasCapability)
118
159
  }
160
+ field={field}
161
+ key={field.key}
162
+ operation={isCreate ? 'create' : 'update'}
119
163
  recordId={record?.id}
120
164
  />
121
165
  ))}
@@ -124,13 +168,16 @@ export function InstrumentForm({
124
168
  ))}
125
169
  </div>
126
170
  <div className="oxa-actions">
127
- <Button onClick={onCancel}>取消</Button>
171
+ <Button disabled={submitting} onClick={onCancel}>
172
+ 取消
173
+ </Button>
128
174
  <Button
129
175
  data-testid="save-instrument"
176
+ htmlType="submit"
177
+ loading={submitting}
130
178
  type="primary"
131
- onClick={() => void submit()}
132
179
  >
133
- {isCreate ? '创建仪器' : '保存修改'}
180
+ {isCreate ? '保存' : '保存修改'}
134
181
  </Button>
135
182
  </div>
136
183
  </Form>
@@ -140,10 +187,12 @@ export function InstrumentForm({
140
187
  function FieldControl({
141
188
  field,
142
189
  disabled,
190
+ operation,
143
191
  recordId,
144
192
  }: {
145
193
  field: InstrumentField;
146
194
  disabled: boolean;
195
+ operation: 'create' | 'update';
147
196
  recordId?: string;
148
197
  }) {
149
198
  const rules = [
@@ -159,21 +208,25 @@ function FieldControl({
159
208
  ];
160
209
  const common = { disabled, placeholder: `请输入${field.label}` };
161
210
  let control = <Input {...common} />;
162
- if (field.kind === 'textarea')
211
+ if (field.kind === 'textarea') {
163
212
  control = <Input.TextArea {...common} rows={3} />;
164
- if (field.kind === 'number')
213
+ }
214
+ if (field.kind === 'number') {
165
215
  control = <InputNumber {...common} min={0} style={{ width: '100%' }} />;
166
- if (field.kind === 'date')
216
+ }
217
+ if (field.kind === 'date') {
167
218
  control = <DatePicker disabled={disabled} style={{ width: '100%' }} />;
168
- if (field.kind === 'boolean')
219
+ }
220
+ if (field.kind === 'boolean') {
169
221
  control = (
170
222
  <Switch
171
- disabled={disabled}
172
223
  checkedChildren="是"
224
+ disabled={disabled}
173
225
  unCheckedChildren="否"
174
226
  />
175
227
  );
176
- if (field.kind === 'select')
228
+ }
229
+ if (field.kind === 'select') {
177
230
  control = (
178
231
  <Select
179
232
  disabled={disabled}
@@ -181,7 +234,8 @@ function FieldControl({
181
234
  placeholder={`请选择${field.label}`}
182
235
  />
183
236
  );
184
- if (field.kind === 'multi')
237
+ }
238
+ if (field.kind === 'multi') {
185
239
  control = (
186
240
  <Select
187
241
  disabled={disabled}
@@ -190,7 +244,31 @@ function FieldControl({
190
244
  placeholder={`请选择${field.label}`}
191
245
  />
192
246
  );
193
- if (field.kind === 'file')
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') {
194
272
  control = (
195
273
  <FileValue
196
274
  accept={field.accept}
@@ -201,8 +279,22 @@ function FieldControl({
201
279
  recordId={recordId}
202
280
  />
203
281
  );
282
+ }
283
+ const extra = disabled
284
+ ? '当前角色不可修改此字段'
285
+ : field.kind === 'scope'
286
+ ? '仅显示当前角色可管理的学院'
287
+ : field.key === 'instrumentAdminIds'
288
+ ? '管理员选择结果将参与仪器数据权限判断'
289
+ : undefined;
204
290
  return (
205
291
  <Form.Item
292
+ className={
293
+ field.kind === 'textarea' || field.kind === 'file'
294
+ ? 'oxa-field-wide'
295
+ : undefined
296
+ }
297
+ extra={extra}
206
298
  label={field.label}
207
299
  name={field.key}
208
300
  rules={rules}
@@ -232,80 +324,226 @@ function FileValue({
232
324
  maxCount?: number;
233
325
  accept?: string;
234
326
  }) {
327
+ const [uploading, setUploading] = useState(false);
328
+ const [uploadError, setUploadError] = useState('');
235
329
  const refs = Array.isArray(value) ? value : value ? [value] : [];
236
- const files: UploadFile[] = refs.map(file => ({
237
- uid: file.id,
238
- name: file.name,
239
- status: 'done',
240
- }));
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
+ };
241
370
  return (
242
- <Upload
243
- accept={accept}
244
- disabled={disabled}
245
- fileList={files}
246
- maxCount={maxCount}
247
- multiple={multiple}
248
- customRequest={async options => {
249
- try {
250
- const uploaded = await instrumentDataApi.upload(
251
- fieldCode,
252
- options.file as File,
253
- recordId
254
- );
255
- onChange?.(multiple ? [...refs, uploaded] : uploaded);
256
- options.onSuccess?.(uploaded);
257
- } catch (error) {
258
- options.onError?.(
259
- error instanceof Error ? error : new Error(String(error))
260
- );
261
- }
262
- }}
263
- onRemove={file => {
264
- const next = refs.filter(item => item.id !== file.uid);
265
- onChange?.(multiple ? next : undefined);
266
- return true;
267
- }}
268
- >
269
- <Button icon={<UploadOutlined />}>上传</Button>
270
- </Upload>
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>
271
423
  );
272
424
  }
273
425
 
274
- function Detail({ record }: { record: InstrumentRecord }) {
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);
275
447
  return (
276
- <div className="oxa-sections">
277
- {Object.entries(sectionTitles).map(([section, title]) => (
278
- <Card key={section} title={title}>
279
- <Descriptions
280
- column={2}
281
- items={instrumentFields
282
- .filter(field => field.section === section)
283
- .map(field => ({
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 => ({
284
455
  key: field.key,
285
456
  label: field.label,
457
+ span:
458
+ field.kind === 'textarea' || field.kind === 'file' ? 3 : 1,
286
459
  children: displayValue(field, record[field.key]),
287
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
+ ]}
288
487
  />
289
488
  </Card>
290
- ))}
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>
291
511
  </div>
292
512
  );
293
513
  }
294
514
 
515
+ function AttachmentReadOnly({ values }: { values: DataFileRef[] }) {
516
+ if (!values.length) return <>-</>;
517
+ return <AttachmentFileList files={values} />;
518
+ }
519
+
295
520
  function displayValue(field: InstrumentField, value: unknown) {
296
521
  if (value === undefined || value === null || value === '') return '-';
297
- if (field.kind === 'boolean') return value ? '是' : '否';
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
+ }
298
539
  if (field.kind === 'file') {
299
- const values = Array.isArray(value) ? value : [value];
300
- return values
301
- .map(item =>
302
- item && typeof item === 'object' && 'name' in item
303
- ? String(item.name)
304
- : String(item)
305
- )
306
- .join('、');
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} />;
307
545
  }
308
- if (Array.isArray(value))
546
+ if (Array.isArray(value)) {
309
547
  return (
310
548
  value
311
549
  .map(
@@ -315,8 +553,23 @@ function displayValue(field: InstrumentField, value: unknown) {
315
553
  )
316
554
  .join('、') || '-'
317
555
  );
318
- return (
319
- field.options?.find(option => option.value === value)?.label ||
320
- String(value)
321
- );
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 }) : '-';
322
575
  }
@@ -1,5 +1,6 @@
1
+ import { ArrowLeftOutlined, EyeOutlined } from '@ant-design/icons';
1
2
  import { useCreate, useOne, useUpdate } from '@refinedev/core';
2
- import { App, Result, Spin, Typography } from 'antd';
3
+ import { Alert, App, Button, Card, Result, Space, Spin, Typography } from 'antd';
3
4
  import { useNavigate, useParams } from 'react-router-dom';
4
5
  import { InstrumentForm } from './InstrumentForm';
5
6
  import { Shell } from './Shell';
@@ -23,32 +24,70 @@ export function InstrumentFormPage({ mode }: { mode: 'create' | 'edit' }) {
23
24
  mode === 'create'
24
25
  ? hasCapability(INSTRUMENT_CAPABILITIES.create)
25
26
  : hasCapability(INSTRUMENT_CAPABILITIES.update);
26
- if (!allowed)
27
+ if (!allowed) {
27
28
  return (
28
29
  <Shell>
29
30
  <Result status="403" title="当前平台用户无此操作权限" />
30
31
  </Shell>
31
32
  );
32
- if (mode === 'edit' && query.query.isLoading)
33
+ }
34
+ if (mode === 'edit' && query.query.isLoading) {
33
35
  return (
34
36
  <Shell>
35
- <Spin />
37
+ <div className="oxa-page-loading">
38
+ <Spin />
39
+ </div>
36
40
  </Shell>
37
41
  );
38
- if (mode === 'edit' && !record)
42
+ }
43
+ if (mode === 'edit' && !record) {
39
44
  return (
40
45
  <Shell>
41
46
  <Result status="404" title="仪器不存在或不在数据范围内" />
42
47
  </Shell>
43
48
  );
49
+ }
44
50
  return (
45
51
  <Shell>
46
- <Typography.Title level={3}>
47
- {mode === 'create' ? '新增仪器' : '编辑仪器'}
48
- </Typography.Title>
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>
49
89
  <InstrumentForm
50
90
  mode={mode}
51
- record={record}
52
91
  onCancel={() =>
53
92
  navigate(mode === 'edit' ? `/instruments/${id}` : '/instruments')
54
93
  }
@@ -71,6 +110,7 @@ export function InstrumentFormPage({ mode }: { mode: 'create' | 'edit' }) {
71
110
  navigate(`/instruments/${result.data.id}`);
72
111
  }
73
112
  }}
113
+ record={record}
74
114
  />
75
115
  </Shell>
76
116
  );