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.
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +13 -5
- package/dist/commands/create.js.map +1 -1
- package/dist/create-workspace.d.ts +6 -1
- package/dist/create-workspace.d.ts.map +1 -1
- package/dist/create-workspace.js +12 -9
- package/dist/create-workspace.js.map +1 -1
- package/package.json +4 -4
- package/template/README.md +3 -1
- package/template/apps/server/package.json +1 -1
- package/template/apps/web/e2e/instruments.spec.ts +1197 -9
- package/template/apps/web/package.json +4 -2
- package/template/apps/web/scripts/check.mjs +2 -2
- package/template/apps/web/scripts/verify-build.mjs +14 -2
- package/template/apps/web/src/AuthoritativeSelector.tsx +371 -0
- package/template/apps/web/src/CollegePage.tsx +181 -0
- package/template/apps/web/src/FilePreviewPage.tsx +303 -0
- package/template/apps/web/src/InstrumentDetailPage.tsx +94 -20
- package/template/apps/web/src/InstrumentForm.tsx +347 -94
- package/template/apps/web/src/InstrumentFormPage.tsx +49 -9
- package/template/apps/web/src/InstrumentListPage.tsx +30 -35
- package/template/apps/web/src/Shell.tsx +229 -41
- package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +226 -0
- package/template/apps/web/src/components/platform-fields/PlatformDirectoryPicker.tsx +698 -0
- package/template/apps/web/src/data-provider.ts +1 -1
- package/template/apps/web/src/fields.ts +17 -6
- package/template/apps/web/src/instrument.ts +1 -1
- package/template/apps/web/src/main.tsx +21 -1
- package/template/apps/web/src/platform-client.ts +248 -2
- package/template/apps/web/src/runtime-meta.ts +15 -0
- package/template/apps/web/src/runtime.tsx +4 -2
- package/template/apps/web/src/styles.css +1007 -28
- package/template/apps/web/test/contracts.test.ts +473 -10
- package/template/openxiangda.config.ts +24 -5
- package/template/package.json +3 -3
- package/template/packages/contracts/src/generated.ts +22 -0
- package/template/platform/data/colleges.ts +21 -0
- package/template/platform/data/instruments.ts +1 -1
- 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)
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
{
|
|
108
|
-
<Card key={
|
|
149
|
+
{groups.map(group => (
|
|
150
|
+
<Card className="oxa-section-card" key={group.key} title={group.title}>
|
|
109
151
|
<div className="oxa-grid">
|
|
110
|
-
{
|
|
111
|
-
.
|
|
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}
|
|
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
|
-
|
|
213
|
+
}
|
|
214
|
+
if (field.kind === 'number') {
|
|
165
215
|
control = <InputNumber {...common} min={0} style={{ width: '100%' }} />;
|
|
166
|
-
|
|
216
|
+
}
|
|
217
|
+
if (field.kind === 'date') {
|
|
167
218
|
control = <DatePicker disabled={disabled} style={{ width: '100%' }} />;
|
|
168
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
<
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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({
|
|
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-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
<
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
.
|
|
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 === '
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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
|
-
|
|
33
|
+
}
|
|
34
|
+
if (mode === 'edit' && query.query.isLoading) {
|
|
33
35
|
return (
|
|
34
36
|
<Shell>
|
|
35
|
-
<
|
|
37
|
+
<div className="oxa-page-loading">
|
|
38
|
+
<Spin />
|
|
39
|
+
</div>
|
|
36
40
|
</Shell>
|
|
37
41
|
);
|
|
38
|
-
|
|
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
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
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
|
);
|