openxiangda-cli 2.0.0-alpha.61 → 2.0.0-alpha.66
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.map +1 -1
- package/dist/create-workspace.js +1 -0
- package/dist/create-workspace.js.map +1 -1
- package/package.json +4 -4
- package/template/AGENTS.md +1 -1
- package/template/README.md +7 -7
- package/template/apps/server/Dockerfile +2 -1
- package/template/apps/server/package.json +1 -1
- package/template/apps/server/src/app.module.ts +1 -3
- package/template/apps/server/test/smoke.test.ts +6 -23
- package/template/apps/web/e2e/resources.spec.ts +20 -0
- package/template/apps/web/index.html +1 -1
- package/template/apps/web/package.json +1 -1
- package/template/apps/web/src/AuthoritativeSelector.tsx +298 -41
- package/template/apps/web/src/Shell.tsx +39 -211
- package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +1 -1
- package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +229 -16
- package/template/apps/web/src/components/resource/SurfaceFields.tsx +142 -2
- package/template/apps/web/src/data-provider.ts +23 -102
- package/template/apps/web/src/main.tsx +35 -83
- package/template/apps/web/src/platform-client.ts +112 -424
- package/template/apps/web/src/runtime-meta.ts +1 -1
- package/template/apps/web/src/styles.css +285 -3
- package/template/apps/web/test/contracts.test.ts +43 -769
- package/template/openxiangda.config.ts +14 -81
- package/template/package.json +3 -3
- package/template/packages/contracts/src/generated.ts +7 -804
- package/template/apps/server/src/instrument-context.controller.ts +0 -25
- package/template/apps/web/e2e/instruments.spec.ts +0 -1338
- package/template/apps/web/src/CollegePage.tsx +0 -181
- package/template/apps/web/src/InstrumentDetailPage.tsx +0 -176
- package/template/apps/web/src/InstrumentForm.tsx +0 -380
- package/template/apps/web/src/InstrumentFormPage.tsx +0 -82
- package/template/apps/web/src/InstrumentListPage.tsx +0 -457
- package/template/apps/web/src/fields.ts +0 -18
- package/template/apps/web/src/instrument.ts +0 -56
- package/template/platform/data/colleges.ts +0 -21
- package/template/platform/data/instrument-surface.ts +0 -125
- package/template/platform/data/instruments.ts +0 -83
|
@@ -21,8 +21,12 @@ import type {
|
|
|
21
21
|
DataFileRef,
|
|
22
22
|
} from 'openxiangda-contracts/browser';
|
|
23
23
|
import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
|
24
|
-
import {
|
|
25
|
-
|
|
24
|
+
import {
|
|
25
|
+
AuthoritativeSelector,
|
|
26
|
+
MobileAuthoritativeSelector,
|
|
27
|
+
ResolvedValueText,
|
|
28
|
+
} from '../../AuthoritativeSelector';
|
|
29
|
+
import { AttachmentFileList, formatManagedFileSize } from '../platform-fields/AttachmentFileList';
|
|
26
30
|
import { PlatformDirectoryPicker } from '../platform-fields/PlatformDirectoryPicker';
|
|
27
31
|
|
|
28
32
|
export type SurfaceField = DataFieldSurface & { key: string };
|
|
@@ -124,6 +128,8 @@ export function SurfaceFilterControl({
|
|
|
124
128
|
case 'directory-user':
|
|
125
129
|
case 'directory-department':
|
|
126
130
|
return <PlatformDirectoryPicker kind={field.widget === 'directory-user' ? 'user' : 'department'} multiple={field.multiple} onChange={onChange as (value: string | string[] | undefined) => void} placeholder={placeholder} value={value as string | string[] | undefined} />;
|
|
131
|
+
case 'resource':
|
|
132
|
+
return <AuthoritativeSelector source="resource" resourceCode={field.reference?.kind === 'resource' ? field.reference.resourceCode : undefined} labelField={field.reference?.kind === 'resource' ? field.reference.labelField : undefined} multiple={field.multiple} onChange={onChange as (value: string | string[] | undefined) => void} placeholder={placeholder} value={value as string | string[] | undefined} operation="update" />;
|
|
127
133
|
default:
|
|
128
134
|
return <Input allowClear value={value as string | undefined} onChange={event => onChange(event.target.value)} placeholder={placeholder} />;
|
|
129
135
|
}
|
|
@@ -193,6 +199,19 @@ export function SurfaceFieldControl({
|
|
|
193
199
|
/>
|
|
194
200
|
);
|
|
195
201
|
break;
|
|
202
|
+
case 'resource':
|
|
203
|
+
control = (
|
|
204
|
+
<AuthoritativeSelector
|
|
205
|
+
disabled={disabled}
|
|
206
|
+
labelField={field.reference?.kind === 'resource' ? field.reference.labelField : undefined}
|
|
207
|
+
multiple={field.multiple}
|
|
208
|
+
operation={operation}
|
|
209
|
+
placeholder={`搜索并选择${field.label}`}
|
|
210
|
+
resourceCode={field.reference?.kind === 'resource' ? field.reference.resourceCode : undefined}
|
|
211
|
+
source="resource"
|
|
212
|
+
/>
|
|
213
|
+
);
|
|
214
|
+
break;
|
|
196
215
|
case 'file':
|
|
197
216
|
control = (
|
|
198
217
|
<ManagedFileField
|
|
@@ -304,6 +323,19 @@ export function MobileSurfaceFieldControl({
|
|
|
304
323
|
/>
|
|
305
324
|
);
|
|
306
325
|
break;
|
|
326
|
+
case 'resource':
|
|
327
|
+
control = (
|
|
328
|
+
<MobileAuthoritativeSelector
|
|
329
|
+
disabled={disabled}
|
|
330
|
+
labelField={field.reference?.kind === 'resource' ? field.reference.labelField : undefined}
|
|
331
|
+
multiple={field.multiple}
|
|
332
|
+
operation={operation}
|
|
333
|
+
placeholder={`搜索并选择${field.label}`}
|
|
334
|
+
resourceCode={field.reference?.kind === 'resource' ? field.reference.resourceCode : undefined}
|
|
335
|
+
source="resource"
|
|
336
|
+
/>
|
|
337
|
+
);
|
|
338
|
+
break;
|
|
307
339
|
case 'file':
|
|
308
340
|
control = (
|
|
309
341
|
<ManagedFileField
|
|
@@ -312,6 +344,7 @@ export function MobileSurfaceFieldControl({
|
|
|
312
344
|
field={field}
|
|
313
345
|
maxCount={field.maxCount}
|
|
314
346
|
multiple={field.multiple}
|
|
347
|
+
mobile
|
|
315
348
|
onUpload={renderers?.upload}
|
|
316
349
|
recordId={recordId}
|
|
317
350
|
/>
|
|
@@ -351,6 +384,9 @@ export function SurfaceFieldValue({
|
|
|
351
384
|
/>
|
|
352
385
|
);
|
|
353
386
|
}
|
|
387
|
+
if (field.widget === 'resource' && field.reference?.kind === 'resource') {
|
|
388
|
+
return <ResolvedValueText labelField={field.reference.labelField} resourceCode={field.reference.resourceCode} source="resource" value={Array.isArray(value) ? value.map(String) : String(value)} />;
|
|
389
|
+
}
|
|
354
390
|
if (field.widget === 'file') {
|
|
355
391
|
const values = (Array.isArray(value) ? value : [value]).filter(isFileRef);
|
|
356
392
|
return values.length ? <AttachmentFileList files={values} /> : <>-</>;
|
|
@@ -376,6 +412,20 @@ function isFileRef(value: unknown): value is DataFileRef {
|
|
|
376
412
|
return Boolean(value && typeof value === 'object' && 'id' in value);
|
|
377
413
|
}
|
|
378
414
|
|
|
415
|
+
type MobilePendingUpload = {
|
|
416
|
+
id: string;
|
|
417
|
+
file: File;
|
|
418
|
+
status: 'uploading' | 'error';
|
|
419
|
+
error?: string;
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
let mobileUploadSequence = 0;
|
|
423
|
+
|
|
424
|
+
function createMobileUploadId() {
|
|
425
|
+
mobileUploadSequence += 1;
|
|
426
|
+
return `mobile-upload-${Date.now()}-${mobileUploadSequence}`;
|
|
427
|
+
}
|
|
428
|
+
|
|
379
429
|
function ManagedFileField({
|
|
380
430
|
field,
|
|
381
431
|
value,
|
|
@@ -386,6 +436,7 @@ function ManagedFileField({
|
|
|
386
436
|
maxCount = 1,
|
|
387
437
|
accept,
|
|
388
438
|
onUpload,
|
|
439
|
+
mobile = false,
|
|
389
440
|
}: {
|
|
390
441
|
field: SurfaceField;
|
|
391
442
|
value?: DataFileRef | DataFileRef[];
|
|
@@ -396,9 +447,13 @@ function ManagedFileField({
|
|
|
396
447
|
maxCount?: number;
|
|
397
448
|
accept?: string | string[];
|
|
398
449
|
onUpload?: SurfaceFieldRenderers['upload'];
|
|
450
|
+
mobile?: boolean;
|
|
399
451
|
}) {
|
|
400
452
|
const [uploading, setUploading] = useState(false);
|
|
401
453
|
const [uploadError, setUploadError] = useState('');
|
|
454
|
+
const [mobilePending, setMobilePending] = useState<MobilePendingUpload[]>([]);
|
|
455
|
+
const mobileInputRef = useRef<HTMLInputElement | null>(null);
|
|
456
|
+
const mobileActiveUploads = useRef(0);
|
|
402
457
|
const refs = useMemo(() => (Array.isArray(value) ? value : value ? [value] : []), [value]);
|
|
403
458
|
const refsRef = useRef(refs);
|
|
404
459
|
useEffect(() => {
|
|
@@ -407,6 +462,91 @@ function ManagedFileField({
|
|
|
407
462
|
if (!onUpload) {
|
|
408
463
|
return <Alert type="info" showIcon message="该文件字段尚未配置上传能力" />;
|
|
409
464
|
}
|
|
465
|
+
if (mobile) {
|
|
466
|
+
const acceptValue = Array.isArray(accept) ? accept.join(',') : accept;
|
|
467
|
+
const uploadOne = async (item: MobilePendingUpload) => {
|
|
468
|
+
setMobilePending(current => current.map(entry => entry.id === item.id ? { ...entry, status: 'uploading', error: undefined } : entry));
|
|
469
|
+
mobileActiveUploads.current += 1;
|
|
470
|
+
setUploading(true);
|
|
471
|
+
try {
|
|
472
|
+
const uploaded = await onUpload(field, item.file, recordId);
|
|
473
|
+
const next = multiple ? [...refsRef.current, uploaded].slice(0, maxCount) : uploaded;
|
|
474
|
+
refsRef.current = Array.isArray(next) ? next : next ? [next] : [];
|
|
475
|
+
onChange?.(next);
|
|
476
|
+
setMobilePending(current => current.filter(entry => entry.id !== item.id));
|
|
477
|
+
} catch (error) {
|
|
478
|
+
setMobilePending(current => current.map(entry => entry.id === item.id ? { ...entry, status: 'error', error: error instanceof Error ? error.message : String(error) } : entry));
|
|
479
|
+
} finally {
|
|
480
|
+
mobileActiveUploads.current = Math.max(0, mobileActiveUploads.current - 1);
|
|
481
|
+
setUploading(mobileActiveUploads.current > 0);
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
const selectFiles = (files: File[]) => {
|
|
485
|
+
const pendingCount = mobilePending.length;
|
|
486
|
+
const available = Math.max(0, maxCount - refsRef.current.length - pendingCount);
|
|
487
|
+
const selected = multiple ? files.slice(0, available) : files.slice(0, 1);
|
|
488
|
+
if (!selected.length) return;
|
|
489
|
+
const items = selected.map(file => ({ id: createMobileUploadId(), file, status: 'uploading' as const }));
|
|
490
|
+
setMobilePending(current => multiple ? [...current, ...items] : items);
|
|
491
|
+
items.forEach(item => void uploadOne(item));
|
|
492
|
+
};
|
|
493
|
+
return (
|
|
494
|
+
<div className="oxa-file-field oxa-mobile-file-field">
|
|
495
|
+
<input
|
|
496
|
+
ref={mobileInputRef}
|
|
497
|
+
accept={acceptValue}
|
|
498
|
+
disabled={disabled || uploading || mobilePending.some(item => item.status === 'uploading') || refs.length + mobilePending.length >= maxCount}
|
|
499
|
+
multiple={multiple}
|
|
500
|
+
onChange={event => {
|
|
501
|
+
selectFiles(Array.from(event.target.files || []));
|
|
502
|
+
event.currentTarget.value = '';
|
|
503
|
+
}}
|
|
504
|
+
style={{ display: 'none' }}
|
|
505
|
+
type="file"
|
|
506
|
+
/>
|
|
507
|
+
{(!maxCount || refs.length + mobilePending.length < maxCount) && (
|
|
508
|
+
<Button
|
|
509
|
+
className="oxa-mobile-file-button"
|
|
510
|
+
disabled={disabled || uploading || mobilePending.some(item => item.status === 'uploading')}
|
|
511
|
+
icon={<UploadOutlined />}
|
|
512
|
+
onClick={() => mobileInputRef.current?.click()}
|
|
513
|
+
>
|
|
514
|
+
上传文件
|
|
515
|
+
</Button>
|
|
516
|
+
)}
|
|
517
|
+
{mobilePending.length > 0 && (
|
|
518
|
+
<div className="oxa-mobile-pending-files">
|
|
519
|
+
{mobilePending.map(item => (
|
|
520
|
+
<div className="oxa-mobile-pending-file" key={item.id}>
|
|
521
|
+
<span className="oxa-mobile-pending-file-copy">
|
|
522
|
+
<strong className="oxa-mobile-pending-file-name" title={item.file.name}>{item.file.name}</strong>
|
|
523
|
+
<small>{formatManagedFileSize(item.file.size)} · {item.status === 'error' ? '上传失败' : '上传中…'}</small>
|
|
524
|
+
</span>
|
|
525
|
+
{item.status === 'error' ? (
|
|
526
|
+
<>
|
|
527
|
+
<span className="oxa-mobile-pending-error">{item.error || '上传失败'}</span>
|
|
528
|
+
<Button onClick={() => void uploadOne(item)} size="small" type="link">重试</Button>
|
|
529
|
+
<Button onClick={() => setMobilePending(current => current.filter(entry => entry.id !== item.id))} size="small" type="link">移除</Button>
|
|
530
|
+
</>
|
|
531
|
+
) : (
|
|
532
|
+
<span className="oxa-mobile-pending-status">上传中…</span>
|
|
533
|
+
)}
|
|
534
|
+
</div>
|
|
535
|
+
))}
|
|
536
|
+
</div>
|
|
537
|
+
)}
|
|
538
|
+
{refs.length ? (
|
|
539
|
+
<AttachmentFileList files={refs} onRemove={file => {
|
|
540
|
+
const next = refs.filter(item => item.id !== file.id);
|
|
541
|
+
refsRef.current = next;
|
|
542
|
+
onChange?.(multiple ? next : undefined);
|
|
543
|
+
}} removable={!disabled} />
|
|
544
|
+
) : (
|
|
545
|
+
<div className="oxa-file-empty">尚未上传文件</div>
|
|
546
|
+
)}
|
|
547
|
+
</div>
|
|
548
|
+
);
|
|
549
|
+
}
|
|
410
550
|
const uploadProps = {
|
|
411
551
|
accept: Array.isArray(accept) ? accept.join(',') : accept,
|
|
412
552
|
disabled,
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import type { DataProvider } from '@refinedev/core';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
createNativeResourceClient,
|
|
5
|
-
instrumentDataApi,
|
|
6
|
-
type GenericResourceQuery,
|
|
7
|
-
} from './platform-client';
|
|
2
|
+
import { createNativeResourceClient, type GenericResourceQuery } from './platform-client';
|
|
8
3
|
import { applicationCode } from './runtime-meta';
|
|
9
4
|
import { resourceDefinitions } from '../../../packages/contracts/src/generated.js';
|
|
10
5
|
|
|
@@ -15,112 +10,38 @@ type GeneratedDefinition = {
|
|
|
15
10
|
surface: import('openxiangda-contracts/browser').DataResourceSurface;
|
|
16
11
|
};
|
|
17
12
|
|
|
18
|
-
const
|
|
13
|
+
const definitions = resourceDefinitions as unknown as Record<string, GeneratedDefinition>;
|
|
14
|
+
type ListParams = Parameters<NonNullable<DataProvider['getList']>>[0];
|
|
15
|
+
type OneParams = Parameters<NonNullable<DataProvider['getOne']>>[0];
|
|
16
|
+
type CreateParams = Parameters<NonNullable<DataProvider['create']>>[0];
|
|
17
|
+
type UpdateParams = Parameters<NonNullable<DataProvider['update']>>[0];
|
|
18
|
+
type DeleteParams = Parameters<NonNullable<DataProvider['deleteOne']>>[0];
|
|
19
19
|
|
|
20
|
-
function
|
|
21
|
-
const definition =
|
|
22
|
-
if (!definition
|
|
23
|
-
throw new Error(`OPENXIANGDA_GENERATED_RESOURCE_NOT_DECLARED:${resource}`);
|
|
24
|
-
}
|
|
20
|
+
function clientFor(resource: string) {
|
|
21
|
+
const definition = definitions[resource];
|
|
22
|
+
if (!definition) throw new Error(`OPENXIANGDA_GENERATED_RESOURCE_NOT_DECLARED:${resource}`);
|
|
25
23
|
return createNativeResourceClient(resource, definition.surface);
|
|
26
24
|
}
|
|
27
25
|
|
|
28
|
-
export const
|
|
29
|
-
getApiUrl: () =>
|
|
30
|
-
|
|
31
|
-
getList: async ({ pagination, sorters, meta }) => {
|
|
32
|
-
const filters = (meta?.query || {}) as Partial<InstrumentListQuery>;
|
|
33
|
-
const sorter = sorters?.[0];
|
|
34
|
-
const result = await instrumentDataApi.list({
|
|
35
|
-
...filters,
|
|
36
|
-
page: pagination?.currentPage || 1,
|
|
37
|
-
pageSize: pagination?.pageSize || 20,
|
|
38
|
-
sort: sorter
|
|
39
|
-
? {
|
|
40
|
-
field: String(sorter.field),
|
|
41
|
-
order: sorter.order,
|
|
42
|
-
}
|
|
43
|
-
: { field: 'instrumentCode', order: 'asc' },
|
|
44
|
-
});
|
|
45
|
-
return { data: result.rows, total: result.total };
|
|
46
|
-
},
|
|
47
|
-
getOne: async ({ id }) => ({ data: await instrumentDataApi.get(String(id)) }),
|
|
48
|
-
create: async ({ variables }) => ({
|
|
49
|
-
data: await instrumentDataApi.create(variables as Partial<InstrumentRecord>),
|
|
50
|
-
}),
|
|
51
|
-
update: async ({ id, variables, meta }) => ({
|
|
52
|
-
data: await instrumentDataApi.update(
|
|
53
|
-
String(id),
|
|
54
|
-
Number(meta?.expectedRevision),
|
|
55
|
-
variables as Partial<InstrumentRecord>
|
|
56
|
-
),
|
|
57
|
-
}),
|
|
58
|
-
deleteOne: async ({ id, meta }) => ({
|
|
59
|
-
data: await instrumentDataApi.remove(
|
|
60
|
-
String(id),
|
|
61
|
-
Number(meta?.expectedRevision)
|
|
62
|
-
),
|
|
63
|
-
}),
|
|
64
|
-
} as DataProvider;
|
|
65
|
-
|
|
66
|
-
const generatedProvider = {
|
|
67
|
-
getApiUrl: () =>
|
|
68
|
-
`/service/openxiangda-api/v2/applications/${applicationCode()}/native/data`,
|
|
69
|
-
getList: async ({ resource, pagination, sorters, meta }) => {
|
|
26
|
+
export const applicationProvider = {
|
|
27
|
+
getApiUrl: () => `/service/openxiangda-api/v2/applications/${applicationCode()}/native/data`,
|
|
28
|
+
getList: async ({ resource, pagination, sorters, meta }: ListParams) => {
|
|
70
29
|
const query = (meta?.query || {}) as Partial<GenericResourceQuery>;
|
|
71
30
|
const sorter = sorters?.[0];
|
|
72
|
-
const result = await
|
|
73
|
-
...query,
|
|
31
|
+
const result = await clientFor(resource).list({
|
|
74
32
|
page: pagination?.currentPage || 1,
|
|
75
33
|
pageSize: pagination?.pageSize || 20,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
: query.sort,
|
|
34
|
+
...query,
|
|
35
|
+
...(sorter ? { sort: { field: String(sorter.field), order: (sorter.order || 'asc') as 'asc' | 'desc' } } : {}),
|
|
79
36
|
});
|
|
80
37
|
return { data: result.rows, total: result.total };
|
|
81
38
|
},
|
|
82
|
-
getOne: async ({ resource, id }) => ({
|
|
83
|
-
|
|
39
|
+
getOne: async ({ resource, id }: OneParams) => ({ data: await clientFor(resource).get(String(id)) }),
|
|
40
|
+
create: async ({ resource, variables }: CreateParams) => ({ data: await clientFor(resource).create(variables as Record<string, unknown>) }),
|
|
41
|
+
update: async ({ resource, id, variables, meta }: UpdateParams) => ({
|
|
42
|
+
data: await clientFor(resource).update(String(id), Number(meta?.expectedRevision), variables as Record<string, unknown>),
|
|
84
43
|
}),
|
|
85
|
-
|
|
86
|
-
data: await
|
|
44
|
+
deleteOne: async ({ resource, id, meta }: DeleteParams) => ({
|
|
45
|
+
data: await clientFor(resource).remove(String(id), Number(meta?.expectedRevision)),
|
|
87
46
|
}),
|
|
88
|
-
|
|
89
|
-
data: await generatedClient(resource).update(
|
|
90
|
-
String(id),
|
|
91
|
-
Number(meta?.expectedRevision),
|
|
92
|
-
variables as Record<string, unknown>
|
|
93
|
-
),
|
|
94
|
-
}),
|
|
95
|
-
deleteOne: async ({ resource, id, meta }) => ({
|
|
96
|
-
data: await generatedClient(resource).remove(
|
|
97
|
-
String(id),
|
|
98
|
-
Number(meta?.expectedRevision)
|
|
99
|
-
),
|
|
100
|
-
}),
|
|
101
|
-
} as DataProvider;
|
|
102
|
-
|
|
103
|
-
/** One Refine provider: golden modules may override a resource, simple resources use generated CRUD. */
|
|
104
|
-
export const applicationProvider = {
|
|
105
|
-
getApiUrl: instrumentProvider.getApiUrl,
|
|
106
|
-
getList: (args: Parameters<NonNullable<DataProvider['getList']>>[0]) =>
|
|
107
|
-
args.resource === 'instruments'
|
|
108
|
-
? instrumentProvider.getList!(args)
|
|
109
|
-
: generatedProvider.getList!(args),
|
|
110
|
-
getOne: (args: Parameters<NonNullable<DataProvider['getOne']>>[0]) =>
|
|
111
|
-
args.resource === 'instruments'
|
|
112
|
-
? instrumentProvider.getOne!(args)
|
|
113
|
-
: generatedProvider.getOne!(args),
|
|
114
|
-
create: (args: Parameters<NonNullable<DataProvider['create']>>[0]) =>
|
|
115
|
-
args.resource === 'instruments'
|
|
116
|
-
? instrumentProvider.create!(args)
|
|
117
|
-
: generatedProvider.create!(args),
|
|
118
|
-
update: (args: Parameters<NonNullable<DataProvider['update']>>[0]) =>
|
|
119
|
-
args.resource === 'instruments'
|
|
120
|
-
? instrumentProvider.update!(args)
|
|
121
|
-
: generatedProvider.update!(args),
|
|
122
|
-
deleteOne: (args: Parameters<NonNullable<DataProvider['deleteOne']>>[0]) =>
|
|
123
|
-
args.resource === 'instruments'
|
|
124
|
-
? instrumentProvider.deleteOne!(args)
|
|
125
|
-
: generatedProvider.deleteOne!(args),
|
|
126
|
-
} as DataProvider;
|
|
47
|
+
} as unknown as DataProvider;
|
|
@@ -1,108 +1,60 @@
|
|
|
1
1
|
import { Refine } from '@refinedev/core';
|
|
2
|
-
import { App as AntdApp, ConfigProvider } from 'antd';
|
|
2
|
+
import { App as AntdApp, ConfigProvider, Spin } from 'antd';
|
|
3
3
|
import zhCN from 'antd/locale/zh_CN';
|
|
4
4
|
import React from 'react';
|
|
5
5
|
import ReactDOM from 'react-dom/client';
|
|
6
6
|
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
|
7
|
+
import { resourceDefinitions } from '../../../packages/contracts/src/generated.js';
|
|
7
8
|
import { applicationProvider } from './data-provider';
|
|
8
|
-
import { CollegePage } from './CollegePage';
|
|
9
9
|
import { GeneratedResourcePage } from './components/resource/GeneratedResourceCrud';
|
|
10
10
|
import { FilePreviewPage } from './FilePreviewPage';
|
|
11
|
-
import {
|
|
12
|
-
import { InstrumentFormPage } from './InstrumentFormPage';
|
|
13
|
-
import { InstrumentListPage } from './InstrumentListPage';
|
|
11
|
+
import { EmptyApplicationPage } from './Shell';
|
|
14
12
|
import { RuntimeBoundary } from './runtime';
|
|
15
13
|
import { applicationBasename } from './runtime-meta';
|
|
14
|
+
import { useGlobalRequestLoading } from './platform-client';
|
|
16
15
|
import './styles.css';
|
|
17
|
-
|
|
16
|
+
|
|
17
|
+
const codes = Object.keys(resourceDefinitions);
|
|
18
|
+
const firstPath = codes[0] ? `/${codes[0]}` : '/';
|
|
19
|
+
|
|
20
|
+
function GlobalRequestLoading() {
|
|
21
|
+
const loading = useGlobalRequestLoading();
|
|
22
|
+
return loading ? (
|
|
23
|
+
<div aria-live="polite" className="oxa-global-request-loading">
|
|
24
|
+
<Spin size="small" />
|
|
25
|
+
<span>正在加载…</span>
|
|
26
|
+
</div>
|
|
27
|
+
) : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const resourceRoutes = codes.flatMap(resourceCode => [
|
|
31
|
+
<Route key={`${resourceCode}-list`} path={`/${resourceCode}`} element={<GeneratedResourcePage resourceCode={resourceCode} />} />,
|
|
32
|
+
<Route key={`${resourceCode}-new`} path={`/${resourceCode}/new`} element={<GeneratedResourcePage mode="create" resourceCode={resourceCode} />} />,
|
|
33
|
+
<Route key={`${resourceCode}-detail`} path={`/${resourceCode}/:id`} element={<GeneratedResourcePage mode="detail" resourceCode={resourceCode} />} />,
|
|
34
|
+
<Route key={`${resourceCode}-edit`} path={`/${resourceCode}/:id/edit`} element={<GeneratedResourcePage mode="edit" resourceCode={resourceCode} />} />,
|
|
35
|
+
<Route key={`${resourceCode}-mobile-list`} path={`/m/${resourceCode}`} element={<GeneratedResourcePage resourceCode={resourceCode} variant="mobile" />} />,
|
|
36
|
+
<Route key={`${resourceCode}-mobile-new`} path={`/m/${resourceCode}/new`} element={<GeneratedResourcePage mode="create" resourceCode={resourceCode} variant="mobile" />} />,
|
|
37
|
+
<Route key={`${resourceCode}-mobile-detail`} path={`/m/${resourceCode}/:id`} element={<GeneratedResourcePage mode="detail" resourceCode={resourceCode} variant="mobile" />} />,
|
|
38
|
+
<Route key={`${resourceCode}-mobile-edit`} path={`/m/${resourceCode}/:id/edit`} element={<GeneratedResourcePage mode="edit" resourceCode={resourceCode} variant="mobile" />} />,
|
|
39
|
+
]);
|
|
18
40
|
|
|
19
41
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
20
42
|
<React.StrictMode>
|
|
21
|
-
<ConfigProvider
|
|
22
|
-
locale={zhCN}
|
|
23
|
-
theme={{
|
|
24
|
-
token: {
|
|
25
|
-
borderRadius: 8,
|
|
26
|
-
colorBgLayout: '#f3f7fd',
|
|
27
|
-
colorPrimary: '#1677ff',
|
|
28
|
-
colorText: '#172033',
|
|
29
|
-
fontSize: 14,
|
|
30
|
-
},
|
|
31
|
-
components: {
|
|
32
|
-
Card: { headerFontSize: 16 },
|
|
33
|
-
Layout: { bodyBg: '#f3f7fd', headerBg: '#ffffff', siderBg: '#ffffff' },
|
|
34
|
-
Menu: { itemBorderRadius: 8, itemSelectedBg: '#eaf3ff' },
|
|
35
|
-
},
|
|
36
|
-
}}
|
|
37
|
-
>
|
|
43
|
+
<ConfigProvider locale={zhCN} theme={{ token: { borderRadius: 8, colorBgLayout: '#f3f7fd', colorPrimary: '#1677ff', colorText: '#172033', fontSize: 14 }, components: { Card: { headerFontSize: 16 }, Layout: { bodyBg: '#f3f7fd', headerBg: '#ffffff', siderBg: '#ffffff' }, Menu: { itemBorderRadius: 8, itemSelectedBg: '#eaf3ff' } } }}>
|
|
38
44
|
<AntdApp>
|
|
45
|
+
<GlobalRequestLoading />
|
|
39
46
|
<RuntimeBoundary>
|
|
40
47
|
<BrowserRouter basename={applicationBasename()}>
|
|
41
|
-
<Refine
|
|
42
|
-
dataProvider={applicationProvider}
|
|
43
|
-
resources={[
|
|
44
|
-
{
|
|
45
|
-
name: 'instruments',
|
|
46
|
-
list: '/instruments',
|
|
47
|
-
create: '/instruments/new',
|
|
48
|
-
edit: '/instruments/:id/edit',
|
|
49
|
-
show: '/instruments/:id',
|
|
50
|
-
},
|
|
51
|
-
]}
|
|
52
|
-
>
|
|
48
|
+
<Refine dataProvider={applicationProvider} resources={codes.map(code => ({ name: code, list: `/${code}`, create: `/${code}/new`, edit: `/${code}/:id/edit`, show: `/${code}/:id` }))}>
|
|
53
49
|
<Routes>
|
|
54
|
-
<Route path="/" element={<Navigate replace to=
|
|
55
|
-
<Route path="/
|
|
56
|
-
|
|
57
|
-
path="/m/instruments"
|
|
58
|
-
element={<InstrumentListPage variant="mobile" />}
|
|
59
|
-
/>
|
|
60
|
-
<Route path="/colleges" element={<CollegePage />} />
|
|
61
|
-
<Route
|
|
62
|
-
path="/files/:resourceCode/:fileId/preview"
|
|
63
|
-
element={<FilePreviewPage />}
|
|
64
|
-
/>
|
|
65
|
-
<Route
|
|
66
|
-
path="/instruments/new"
|
|
67
|
-
element={<InstrumentFormPage mode="create" />}
|
|
68
|
-
/>
|
|
69
|
-
<Route
|
|
70
|
-
path="/instruments/:id/edit"
|
|
71
|
-
element={<InstrumentFormPage mode="edit" />}
|
|
72
|
-
/>
|
|
73
|
-
<Route
|
|
74
|
-
path="/instruments/:id"
|
|
75
|
-
element={<InstrumentDetailPage />}
|
|
76
|
-
/>
|
|
77
|
-
<Route
|
|
78
|
-
path="/m/instruments/:id"
|
|
79
|
-
element={<InstrumentDetailPage variant="mobile" />}
|
|
80
|
-
/>
|
|
81
|
-
<Route
|
|
82
|
-
path="/m/instruments/new"
|
|
83
|
-
element={<InstrumentFormPage mode="create" variant="mobile" />}
|
|
84
|
-
/>
|
|
85
|
-
<Route
|
|
86
|
-
path="/m/instruments/:id/edit"
|
|
87
|
-
element={<InstrumentFormPage mode="edit" variant="mobile" />}
|
|
88
|
-
/>
|
|
89
|
-
{Object.entries(resourceDefinitions)
|
|
90
|
-
.filter(([, definition]) => definition.surface.generated !== false)
|
|
91
|
-
.flatMap(([resourceCode]) => [
|
|
92
|
-
<Route key={`${resourceCode}-list`} path={`/${resourceCode}`} element={<GeneratedResourcePage resourceCode={resourceCode} />} />,
|
|
93
|
-
<Route key={`${resourceCode}-new`} path={`/${resourceCode}/new`} element={<GeneratedResourcePage mode="create" resourceCode={resourceCode} />} />,
|
|
94
|
-
<Route key={`${resourceCode}-detail`} path={`/${resourceCode}/:id`} element={<GeneratedResourcePage mode="detail" resourceCode={resourceCode} />} />,
|
|
95
|
-
<Route key={`${resourceCode}-edit`} path={`/${resourceCode}/:id/edit`} element={<GeneratedResourcePage mode="edit" resourceCode={resourceCode} />} />,
|
|
96
|
-
<Route key={`${resourceCode}-mobile-list`} path={`/m/${resourceCode}`} element={<GeneratedResourcePage resourceCode={resourceCode} variant="mobile" />} />,
|
|
97
|
-
<Route key={`${resourceCode}-mobile-new`} path={`/m/${resourceCode}/new`} element={<GeneratedResourcePage mode="create" resourceCode={resourceCode} variant="mobile" />} />,
|
|
98
|
-
<Route key={`${resourceCode}-mobile-detail`} path={`/m/${resourceCode}/:id`} element={<GeneratedResourcePage mode="detail" resourceCode={resourceCode} variant="mobile" />} />,
|
|
99
|
-
<Route key={`${resourceCode}-mobile-edit`} path={`/m/${resourceCode}/:id/edit`} element={<GeneratedResourcePage mode="edit" resourceCode={resourceCode} variant="mobile" />} />,
|
|
100
|
-
])}
|
|
50
|
+
<Route path="/" element={codes.length ? <Navigate replace to={firstPath} /> : <EmptyApplicationPage />} />
|
|
51
|
+
<Route path="/files/:resourceCode/:fileId/preview" element={<FilePreviewPage />} />
|
|
52
|
+
{resourceRoutes}
|
|
101
53
|
</Routes>
|
|
102
54
|
</Refine>
|
|
103
55
|
</BrowserRouter>
|
|
104
56
|
</RuntimeBoundary>
|
|
105
57
|
</AntdApp>
|
|
106
58
|
</ConfigProvider>
|
|
107
|
-
</React.StrictMode
|
|
59
|
+
</React.StrictMode>,
|
|
108
60
|
);
|