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.
Files changed (39) hide show
  1. package/dist/create-workspace.d.ts.map +1 -1
  2. package/dist/create-workspace.js +1 -0
  3. package/dist/create-workspace.js.map +1 -1
  4. package/package.json +4 -4
  5. package/template/AGENTS.md +1 -1
  6. package/template/README.md +7 -7
  7. package/template/apps/server/Dockerfile +2 -1
  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/src/AuthoritativeSelector.tsx +298 -41
  15. package/template/apps/web/src/Shell.tsx +39 -211
  16. package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +1 -1
  17. package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +229 -16
  18. package/template/apps/web/src/components/resource/SurfaceFields.tsx +142 -2
  19. package/template/apps/web/src/data-provider.ts +23 -102
  20. package/template/apps/web/src/main.tsx +35 -83
  21. package/template/apps/web/src/platform-client.ts +112 -424
  22. package/template/apps/web/src/runtime-meta.ts +1 -1
  23. package/template/apps/web/src/styles.css +285 -3
  24. package/template/apps/web/test/contracts.test.ts +43 -769
  25. package/template/openxiangda.config.ts +14 -81
  26. package/template/package.json +3 -3
  27. package/template/packages/contracts/src/generated.ts +7 -804
  28. package/template/apps/server/src/instrument-context.controller.ts +0 -25
  29. package/template/apps/web/e2e/instruments.spec.ts +0 -1338
  30. package/template/apps/web/src/CollegePage.tsx +0 -181
  31. package/template/apps/web/src/InstrumentDetailPage.tsx +0 -176
  32. package/template/apps/web/src/InstrumentForm.tsx +0 -380
  33. package/template/apps/web/src/InstrumentFormPage.tsx +0 -82
  34. package/template/apps/web/src/InstrumentListPage.tsx +0 -457
  35. package/template/apps/web/src/fields.ts +0 -18
  36. package/template/apps/web/src/instrument.ts +0 -56
  37. package/template/platform/data/colleges.ts +0 -21
  38. package/template/platform/data/instrument-surface.ts +0 -125
  39. 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 { ResolvedValueText } from '../../AuthoritativeSelector';
25
- import { AttachmentFileList } from '../platform-fields/AttachmentFileList';
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 type { InstrumentListQuery, InstrumentRecord } from './instrument';
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 generatedDefinitions = resourceDefinitions as unknown as Record<string, GeneratedDefinition>;
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 generatedClient(resource: string) {
21
- const definition = generatedDefinitions[resource];
22
- if (!definition || definition.surface.generated === false) {
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 instrumentProvider = {
29
- getApiUrl: () =>
30
- `/service/openxiangda-api/v2/applications/${applicationCode()}/native/data`,
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 generatedClient(resource).list({
73
- ...query,
31
+ const result = await clientFor(resource).list({
74
32
  page: pagination?.currentPage || 1,
75
33
  pageSize: pagination?.pageSize || 20,
76
- sort: sorter
77
- ? { field: String(sorter.field), order: sorter.order || 'asc' }
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
- data: await generatedClient(resource).get(String(id)),
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
- create: async ({ resource, variables }) => ({
86
- data: await generatedClient(resource).create(variables as Record<string, unknown>),
44
+ deleteOne: async ({ resource, id, meta }: DeleteParams) => ({
45
+ data: await clientFor(resource).remove(String(id), Number(meta?.expectedRevision)),
87
46
  }),
88
- update: async ({ resource, id, variables, meta }) => ({
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 { InstrumentDetailPage } from './InstrumentDetailPage';
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
- import { resourceDefinitions } from '../../../packages/contracts/src/generated.js';
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="/instruments" />} />
55
- <Route path="/instruments" element={<InstrumentListPage />} />
56
- <Route
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
  );