openxiangda-cli 2.0.0-alpha.89 → 2.0.0-alpha.91

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 (77) hide show
  1. package/README.md +2 -2
  2. package/bin/run.js +1 -0
  3. package/dist/commands/skill.d.ts +26 -0
  4. package/dist/commands/skill.d.ts.map +1 -0
  5. package/dist/commands/skill.js +43 -0
  6. package/dist/commands/skill.js.map +1 -0
  7. package/dist/create-workspace.d.ts.map +1 -1
  8. package/dist/create-workspace.js +3 -3
  9. package/dist/create-workspace.js.map +1 -1
  10. package/dist/toolchain-capsule.d.ts.map +1 -1
  11. package/dist/toolchain-capsule.js +4 -8
  12. package/dist/toolchain-capsule.js.map +1 -1
  13. package/package.json +6 -7
  14. package/template/AGENTS.md +2 -1
  15. package/template/README.md +3 -3
  16. package/template/apps/server/package.json +1 -1
  17. package/template/apps/server/src/app.module.ts +1 -1
  18. package/template/apps/server/src/main.ts +1 -1
  19. package/template/apps/web/e2e/client-fixture.ts +5 -0
  20. package/template/apps/web/e2e/data-api-live-fixture.tsx +19 -14
  21. package/template/apps/web/e2e/field-protocol-fixture.tsx +87 -20
  22. package/template/apps/web/e2e/field-protocol.spec.ts +86 -0
  23. package/template/apps/web/e2e/resources.spec.ts +20 -181
  24. package/template/apps/web/field-protocol.e2e.html +3 -0
  25. package/template/apps/web/package.json +2 -5
  26. package/template/apps/web/src/main.tsx +12 -60
  27. package/template/apps/web/test/contracts.test.ts +52 -104
  28. package/template/openxiangda.config.ts +1 -1
  29. package/template/package.json +1 -3
  30. package/template/packages/contracts/src/generated.ts +1 -0
  31. package/template/scripts/verify-template-budget.mjs +2 -2
  32. package/template/apps/web/src/AuthoritativeSelector.tsx +0 -661
  33. package/template/apps/web/src/FilePreviewPage.tsx +0 -324
  34. package/template/apps/web/src/RoleSubjectSelect.tsx +0 -128
  35. package/template/apps/web/src/Shell.tsx +0 -445
  36. package/template/apps/web/src/appearance.tsx +0 -147
  37. package/template/apps/web/src/components/platform-fields/AddressField.tsx +0 -338
  38. package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +0 -245
  39. package/template/apps/web/src/components/platform-fields/CascadeField.tsx +0 -49
  40. package/template/apps/web/src/components/platform-fields/DateTimeField.tsx +0 -148
  41. package/template/apps/web/src/components/platform-fields/JsonField.tsx +0 -77
  42. package/template/apps/web/src/components/platform-fields/LocationField.tsx +0 -164
  43. package/template/apps/web/src/components/platform-fields/PlatformDirectoryPicker.tsx +0 -673
  44. package/template/apps/web/src/components/platform-fields/ResourceReferenceField.tsx +0 -67
  45. package/template/apps/web/src/components/platform-fields/RichTextField.tsx +0 -196
  46. package/template/apps/web/src/components/platform-fields/SignatureField.tsx +0 -315
  47. package/template/apps/web/src/components/platform-fields/SubtableField.tsx +0 -553
  48. package/template/apps/web/src/components/platform-fields/address-value.ts +0 -80
  49. package/template/apps/web/src/components/platform-fields/cascade-value.ts +0 -66
  50. package/template/apps/web/src/components/platform-fields/directory-value.ts +0 -53
  51. package/template/apps/web/src/components/platform-fields/field-form-codec.ts +0 -98
  52. package/template/apps/web/src/components/platform-fields/location-value.ts +0 -87
  53. package/template/apps/web/src/components/platform-fields/resource-query.ts +0 -131
  54. package/template/apps/web/src/components/platform-fields/rich-text-value.ts +0 -59
  55. package/template/apps/web/src/components/platform-fields/subtable-value.ts +0 -187
  56. package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +0 -1184
  57. package/template/apps/web/src/components/resource/ResourceBatchActions.tsx +0 -326
  58. package/template/apps/web/src/components/resource/StandardResourcePages.tsx +0 -392
  59. package/template/apps/web/src/components/resource/SurfaceFields.tsx +0 -915
  60. package/template/apps/web/src/components/resource/generated-resource-definition.ts +0 -18
  61. package/template/apps/web/src/components/resource/resource-import.ts +0 -205
  62. package/template/apps/web/src/data-provider.ts +0 -72
  63. package/template/apps/web/src/platform-client.ts +0 -708
  64. package/template/apps/web/src/runtime-meta.ts +0 -75
  65. package/template/apps/web/src/runtime.tsx +0 -227
  66. package/template/apps/web/src/styles.css +0 -1941
  67. package/template/apps/web/test/address-field.test.ts +0 -66
  68. package/template/apps/web/test/cascade-value.test.ts +0 -68
  69. package/template/apps/web/test/directory-value.test.ts +0 -55
  70. package/template/apps/web/test/field-bounds.test.ts +0 -17
  71. package/template/apps/web/test/field-form-codec.test.ts +0 -85
  72. package/template/apps/web/test/location-field.test.ts +0 -83
  73. package/template/apps/web/test/resource-import.test.ts +0 -143
  74. package/template/apps/web/test/resource-query.test.ts +0 -144
  75. package/template/apps/web/test/rich-json-field.test.ts +0 -53
  76. package/template/apps/web/test/signature-serial-field.test.ts +0 -40
  77. package/template/apps/web/test/subtable-value.test.ts +0 -144
@@ -1,1184 +0,0 @@
1
- import {
2
- ArrowRightOutlined,
3
- ColumnHeightOutlined,
4
- DeleteOutlined,
5
- EditOutlined,
6
- EyeOutlined,
7
- ExportOutlined,
8
- PlusOutlined,
9
- ReloadOutlined,
10
- SearchOutlined,
11
- SettingOutlined,
12
- UpOutlined,
13
- DownOutlined,
14
- } from '@ant-design/icons';
15
- import { useCreate, useDelete, useList, useOne, useUpdate } from '@refinedev/core';
16
- import {
17
- App,
18
- Alert,
19
- Button,
20
- Card,
21
- Collapse,
22
- Descriptions,
23
- Dropdown,
24
- Empty,
25
- Form,
26
- Input,
27
- Popconfirm,
28
- Result,
29
- Select,
30
- Space,
31
- Spin,
32
- Table,
33
- Tooltip,
34
- Typography,
35
- type MenuProps,
36
- type TableColumnsType,
37
- } from 'antd';
38
- import type {
39
- DataAuditEntry,
40
- DataResourceSurface,
41
- DataTransactionOperation,
42
- SubjectProfile,
43
- UserReferenceValue,
44
- } from 'openxiangda-contracts/browser';
45
- import { useEffect, useMemo, useState } from 'react';
46
- import { useNavigate, useParams } from 'react-router-dom';
47
- import { resourceDefinitions } from '../../../../../packages/contracts/src/generated.js';
48
- import {
49
- MobileResourceDetailPage,
50
- MobileResourceFormPage,
51
- MobileResourceListPage,
52
- ResourceDetailPage,
53
- ResourceFormPage,
54
- ResourceListPage,
55
- } from './StandardResourcePages';
56
- import {
57
- listSurfaceFields,
58
- MobileSurfaceFieldControl,
59
- SurfaceFieldControl,
60
- SurfaceFieldValue,
61
- SurfaceFilterControl,
62
- type SurfaceField,
63
- } from './SurfaceFields';
64
- import {
65
- createNativeResourceClient,
66
- transactNativeData,
67
- type GenericResourceQuery,
68
- } from '../../platform-client';
69
- import { normalizeFormValues, normalizeRecordForForm } from '../platform-fields/field-form-codec';
70
- import { DirectoryIdentityText } from '../../AuthoritativeSelector';
71
- import { useRuntime } from '../../runtime';
72
- import { ResourceBatchActions, ResourceImportButton } from './ResourceBatchActions';
73
- import {
74
- SubtableField,
75
- SubtableValueDisplay,
76
- } from '../platform-fields/SubtableField';
77
- import {
78
- buildSubtableOperations,
79
- type SubtableDraftRow,
80
- } from '../platform-fields/subtable-value';
81
- import type { GeneratedResourceDefinition } from './generated-resource-definition';
82
-
83
- type ResourceRecord = Record<string, unknown> & {
84
- id: string;
85
- revision: number;
86
- updated_at?: string;
87
- };
88
-
89
- const definitions = resourceDefinitions as unknown as Record<
90
- string,
91
- GeneratedResourceDefinition
92
- >;
93
-
94
- function signatureSigner(profile: SubjectProfile | null): UserReferenceValue | undefined {
95
- if (!profile) return undefined;
96
- return {
97
- label: profile.displayName,
98
- value: profile.userId,
99
- ...(profile.avatarUrl ? { avatarUrl: profile.avatarUrl } : {}),
100
- ...(profile.jobNumber ? { employeeNo: profile.jobNumber } : {}),
101
- ...(profile.affiliatedDepartment
102
- ? {
103
- departments: [{
104
- label: profile.affiliatedDepartment.name,
105
- value: profile.affiliatedDepartment.id,
106
- }],
107
- }
108
- : {}),
109
- };
110
- }
111
-
112
- function definitionFor(resourceCode: string) {
113
- return definitions[resourceCode];
114
- }
115
-
116
- function fieldFor(surface: DataResourceSurface, key: string): SurfaceField {
117
- const field = surface.fields[key];
118
- return field
119
- ? { key, ...field }
120
- : {
121
- key,
122
- label: key,
123
- type: 'text.short',
124
- widget: 'text',
125
- readCapabilities: [],
126
- createCapabilities: [],
127
- updateCapabilities: [],
128
- };
129
- }
130
-
131
- function fieldsBySection(surface: DataResourceSurface) {
132
- const groups = new Map<string, SurfaceField[]>();
133
- for (const [key, field] of Object.entries(surface.fields)) {
134
- if (field.system) continue;
135
- const section = field.section || 'default';
136
- groups.set(section, [...(groups.get(section) || []), { key, ...field }]);
137
- }
138
- return [...groups.entries()].map(([section, fields]) => ({
139
- section,
140
- fields,
141
- }));
142
- }
143
-
144
- function fieldWriteAuthorized(field: SurfaceField, mode: 'create' | 'edit', hasCapability: (code: string) => boolean, isAppSuperAdmin: boolean) {
145
- if (isAppSuperAdmin) return true;
146
- const capabilities = mode === 'create' ? field.createCapabilities : field.updateCapabilities;
147
- return capabilities.length > 0 && capabilities.every(hasCapability);
148
- }
149
-
150
- function fieldWritable(field: SurfaceField, mode: 'create' | 'edit', hasCapability: (code: string) => boolean, isAppSuperAdmin: boolean) {
151
- return field.widget !== 'readonly' && fieldWriteAuthorized(field, mode, hasCapability, isAppSuperAdmin);
152
- }
153
-
154
- function fieldReadable(field: SurfaceField, hasCapability: (code: string) => boolean, isAppSuperAdmin: boolean) {
155
- return isAppSuperAdmin || (field.readCapabilities.length > 0 && field.readCapabilities.every(hasCapability));
156
- }
157
-
158
- function auditOperationLabel(operation: DataAuditEntry['operation']) {
159
- return operation === 'created' ? '创建' : operation === 'deleted' ? '删除' : '更新';
160
- }
161
-
162
- function auditValue(field: SurfaceField, value: unknown) {
163
- if (value === undefined || value === null || value === '') {
164
- return <Typography.Text type="secondary">-</Typography.Text>;
165
- }
166
- return <SurfaceFieldValue field={field} value={value} />;
167
- }
168
-
169
- function auditChangeFields(entry: DataAuditEntry, surface: DataResourceSurface) {
170
- const fields = fieldsBySection(surface).flatMap((group) => group.fields);
171
- const source = entry.changes || entry.record || entry.before || {};
172
- return fields.filter((field) => Object.prototype.hasOwnProperty.call(source, field.key));
173
- }
174
-
175
- async function exportResourceRows(
176
- code: string,
177
- name: string,
178
- surface: DataResourceSurface,
179
- fields: SurfaceField[],
180
- query: Partial<GenericResourceQuery>,
181
- sort: { field: string; order: 'asc' | 'desc' }
182
- ) {
183
- const client = createNativeResourceClient(code, surface);
184
- const exported = await client.exportCsv({
185
- page: 1,
186
- pageSize: 200,
187
- keyword: query.keyword,
188
- searchField: query.searchField,
189
- filters: query.filters,
190
- sort,
191
- }, fields.map((field) => field.key));
192
- const url = URL.createObjectURL(exported.blob);
193
- const anchor = document.createElement('a');
194
- anchor.href = url;
195
- anchor.download = exported.fileName || `${name}.csv`;
196
- anchor.click();
197
- window.setTimeout(() => URL.revokeObjectURL(url), 0);
198
- return { rowLimit: exported.rowLimit };
199
- }
200
-
201
- function AuditEntry({ entry, surface, readable }: { entry: DataAuditEntry; surface: DataResourceSurface; readable: (field: SurfaceField) => boolean }) {
202
- const fields = auditChangeFields(entry, surface).filter(readable);
203
- const changes = entry.changes || entry.record || entry.before || {};
204
- return (
205
- <div className="oxa-audit-entry">
206
- <div className="oxa-audit-heading">
207
- <strong>{auditOperationLabel(entry.operation)}</strong>
208
- <Typography.Text type="secondary">版本 {entry.revision}</Typography.Text>
209
- </div>
210
- <div className="oxa-audit-meta">
211
- <span>操作人</span>
212
- {entry.actor.principalType === 'application' ? (
213
- <Typography.Text>
214
- 应用服务
215
- {entry.actor.oauthClientId ? `(${entry.actor.oauthClientId})` : ''}
216
- </Typography.Text>
217
- ) : (
218
- <DirectoryIdentityText id={entry.actor.userId} source="user" />
219
- )}
220
- <span>时间</span>
221
- <Typography.Text>{new Date(entry.occurredAt).toLocaleString('zh-CN')}</Typography.Text>
222
- </div>
223
- {fields.length ? (
224
- <div className="oxa-audit-changes">
225
- {fields.map((field) => {
226
- const before = entry.operation === 'created' ? undefined : entry.before?.[field.key];
227
- const after = entry.operation === 'deleted' ? undefined : changes[field.key];
228
- return (
229
- <div className="oxa-audit-change" key={field.key}>
230
- <Typography.Text strong>{field.label}</Typography.Text>
231
- <div className="oxa-audit-change-values">
232
- {auditValue(field, before)}
233
- <ArrowRightOutlined />
234
- {auditValue(field, after)}
235
- </div>
236
- </div>
237
- );
238
- })}
239
- </div>
240
- ) : (
241
- <Typography.Text type="secondary">未发生字段变化</Typography.Text>
242
- )}
243
- </div>
244
- );
245
- }
246
-
247
- function useGeneratedList(
248
- definition: GeneratedResourceDefinition,
249
- readableFieldCodes: Set<string>
250
- ) {
251
- const { code, surface } = definition;
252
- const declaredDefaultSort = surface.list?.defaultSort?.field;
253
- const defaultSortField =
254
- (declaredDefaultSort && readableFieldCodes.has(declaredDefaultSort)
255
- ? declaredDefaultSort
256
- : Object.keys(surface.fields).find((field) => readableFieldCodes.has(field))) || 'id';
257
- const defaultSortOrder = surface.list?.defaultSort?.order || 'asc';
258
- const defaultPageSize = surface.list?.defaultPageSize || 20;
259
- const defaultSearchField = (surface.list?.searchableFields || []).find((field) =>
260
- readableFieldCodes.has(field)
261
- );
262
- const [page, setPage] = useState(1);
263
- const [pageSize, setPageSize] = useState(defaultPageSize);
264
- const [draft, setDraft] = useState<Partial<GenericResourceQuery>>({ searchField: defaultSearchField });
265
- const [query, setQuery] = useState<Partial<GenericResourceQuery>>({});
266
- const [sort, setSort] = useState({
267
- field: defaultSortField,
268
- order: defaultSortOrder as 'asc' | 'desc',
269
- });
270
- useEffect(() => {
271
- setPage(1);
272
- setPageSize(defaultPageSize);
273
- setDraft({ searchField: defaultSearchField });
274
- setQuery({});
275
- setSort({
276
- field: defaultSortField,
277
- order: defaultSortOrder as 'asc' | 'desc',
278
- });
279
- }, [code, defaultPageSize, defaultSearchField, defaultSortField, defaultSortOrder]);
280
- const list = useList<ResourceRecord>({
281
- resource: code,
282
- pagination: { currentPage: page, pageSize },
283
- sorters: [sort],
284
- meta: { query },
285
- });
286
- return {
287
- draft,
288
- list,
289
- page,
290
- pageSize,
291
- query,
292
- setDraft,
293
- setPage,
294
- setPageSize,
295
- setQuery,
296
- setSort,
297
- sort,
298
- };
299
- }
300
-
301
- export function GeneratedResourcePage({
302
- resourceCode,
303
- mode = 'list',
304
- variant = 'desktop',
305
- definition: providedDefinition,
306
- }: {
307
- resourceCode: string;
308
- mode?: 'list' | 'create' | 'edit' | 'detail';
309
- variant?: 'desktop' | 'mobile';
310
- definition?: GeneratedResourceDefinition;
311
- }) {
312
- const definition = providedDefinition || definitionFor(resourceCode);
313
- if (!definition || definition.surface.generated === false) {
314
- return <Result status="404" title="未声明可生成的资源页面" />;
315
- }
316
- const pageKey = `${variant}:${mode}:${resourceCode}`;
317
- if (mode === 'list') return <GeneratedResourceListPage key={pageKey} definition={definition} variant={variant} />;
318
- if (mode === 'detail') return <GeneratedResourceDetailPage key={pageKey} definition={definition} variant={variant} />;
319
- return <GeneratedResourceFormPage key={pageKey} definition={definition} mode={mode} variant={variant} />;
320
- }
321
-
322
- function GeneratedResourceListPage({ definition, variant }: { definition: GeneratedResourceDefinition; variant: 'desktop' | 'mobile' }) {
323
- return variant === 'mobile' ? <GeneratedMobileList definition={definition} /> : <GeneratedDesktopList definition={definition} />;
324
- }
325
-
326
- function GeneratedDesktopList({ definition }: { definition: GeneratedResourceDefinition }) {
327
- const navigate = useNavigate();
328
- const { message } = App.useApp();
329
- const { hasCapability, identity } = useRuntime();
330
- const { code, name, surface, capabilities } = definition;
331
- const fields = listSurfaceFields(surface.fields).filter((field) =>
332
- fieldReadable(field, hasCapability, identity.isAppSuperAdmin)
333
- );
334
- const readableFieldCodes = new Set(
335
- Object.entries(surface.fields)
336
- .filter(([, field]) => fieldReadable({ key: '', ...field }, hasCapability, identity.isAppSuperAdmin))
337
- .map(([key]) => key)
338
- );
339
- const state = useGeneratedList(definition, readableFieldCodes);
340
- const [exporting, setExporting] = useState(false);
341
- const [filtersExpanded, setFiltersExpanded] = useState(false);
342
- const [density, setDensity] = useState<'small' | 'middle' | 'large'>('middle');
343
- const [visibleColumnKeys, setVisibleColumnKeys] = useState<string[]>(() => fields.map((field) => field.key));
344
- const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
345
- const remove = useDelete();
346
- const rows = (state.list.result.data || []) as unknown as ResourceRecord[];
347
- useEffect(() => {
348
- setVisibleColumnKeys(fields.map((field) => field.key));
349
- setFiltersExpanded(false);
350
- setDensity('middle');
351
- setSelectedRowKeys([]);
352
- }, [code]);
353
- useEffect(() => {
354
- setSelectedRowKeys([]);
355
- }, [code, state.page, state.pageSize, state.query, state.sort.field, state.sort.order]);
356
- const formFields = fieldsBySection(surface).flatMap((group) => group.fields);
357
- const importWritableFieldCodes = formFields
358
- .filter((field) => field.widget !== 'readonly' && fieldWritable(field, 'create', hasCapability, identity.isAppSuperAdmin))
359
- .map((field) => field.key);
360
- const batchWritableFields = formFields.filter(
361
- (field) => !['file', 'readonly'].includes(field.widget || '') && fieldWritable(field, 'edit', hasCapability, identity.isAppSuperAdmin)
362
- );
363
- const selectedRows = rows.filter((row) => selectedRowKeys.includes(row.id));
364
- const canBatch = hasCapability(capabilities.update) || hasCapability(capabilities.delete);
365
- const columns = useMemo<TableColumnsType<ResourceRecord>>(
366
- () => [
367
- ...fields
368
- .filter((field) => visibleColumnKeys.includes(field.key))
369
- .map((field) => ({
370
- title: field.label,
371
- dataIndex: field.key,
372
- key: field.key,
373
- sorter: Boolean(field.sortable),
374
- render: (value: unknown, record: ResourceRecord) => (
375
- <SurfaceFieldValue
376
- field={field}
377
- resourceCode={code}
378
- value={value}
379
- renderers={{
380
- renderValue: ({ value: current }) =>
381
- field.key === fields[0]?.key ? (
382
- <a onClick={() => navigate(`/${code}/${record.id}`)}>{String(current || '-')}</a>
383
- ) : undefined,
384
- }}
385
- />
386
- ),
387
- })),
388
- {
389
- title: '更新时间',
390
- dataIndex: 'updated_at',
391
- key: 'updated_at',
392
- render: (value) => (value ? new Date(String(value)).toLocaleString('zh-CN') : '-'),
393
- },
394
- {
395
- title: '操作',
396
- key: 'actions',
397
- fixed: 'right' as const,
398
- render: (_: unknown, record: ResourceRecord) => (
399
- <Space size={4}>
400
- <Button aria-label="查看" icon={<EyeOutlined />} onClick={() => navigate(`/${code}/${record.id}`)} size="small" type="text" />
401
- {hasCapability(capabilities.update) && (
402
- <Button
403
- aria-label="编辑"
404
- icon={<EditOutlined />}
405
- onClick={() => navigate(`/${code}/${record.id}/edit`)}
406
- size="small"
407
- type="text"
408
- />
409
- )}
410
- {hasCapability(capabilities.delete) && (
411
- <Popconfirm
412
- onConfirm={() =>
413
- remove.mutate(
414
- {
415
- resource: code,
416
- id: record.id,
417
- meta: { expectedRevision: record.revision },
418
- },
419
- {
420
- onSuccess: () => {
421
- message.success('已删除');
422
- void state.list.query.refetch();
423
- },
424
- }
425
- )
426
- }
427
- title={`确定删除${name}?`}
428
- >
429
- <Button aria-label="删除" danger icon={<DeleteOutlined />} size="small" type="text" />
430
- </Popconfirm>
431
- )}
432
- </Space>
433
- ),
434
- },
435
- ],
436
- [
437
- capabilities.delete,
438
- capabilities.update,
439
- code,
440
- fields,
441
- hasCapability,
442
- message,
443
- name,
444
- navigate,
445
- remove,
446
- state.list.query,
447
- visibleColumnKeys,
448
- ]
449
- );
450
- const handleExport = async () => {
451
- setExporting(true);
452
- try {
453
- const result = await exportResourceRows(code, name, surface, fields, state.query, state.sort);
454
- message.success(`导出已生成(最多 ${result.rowLimit} 条)`);
455
- } catch (error) {
456
- message.error(error instanceof Error ? error.message : '导出失败');
457
- } finally {
458
- setExporting(false);
459
- }
460
- };
461
- const filterFields = (surface.list?.filterFields || []).filter((field) =>
462
- readableFieldCodes.has(field)
463
- );
464
- const searchableFields = (surface.list?.searchableFields || []).filter((field) =>
465
- readableFieldCodes.has(field)
466
- );
467
- const hasMoreFilters = filterFields.length > 2;
468
- const columnMenu: MenuProps = {
469
- items: fields.map((field) => ({ key: field.key, label: field.label })),
470
- multiple: true,
471
- selectable: true,
472
- selectedKeys: visibleColumnKeys,
473
- onSelect: ({ selectedKeys }) => setVisibleColumnKeys(selectedKeys.map(String)),
474
- onDeselect: ({ selectedKeys }) => {
475
- if (selectedKeys.length) setVisibleColumnKeys(selectedKeys.map(String));
476
- else message.info('至少保留一列');
477
- },
478
- };
479
- const densityMenu: MenuProps = {
480
- items: [
481
- { key: 'large', label: '宽松' },
482
- { key: 'middle', label: '标准' },
483
- { key: 'small', label: '紧凑' },
484
- ],
485
- selectable: true,
486
- selectedKeys: [density],
487
- onClick: ({ key }) => setDensity(key as typeof density),
488
- };
489
- return (
490
- <ResourceListPage
491
- createCapability={capabilities.create}
492
- createPath={`/${code}/new`}
493
- readCapability={capabilities.read}
494
- resource={code}
495
- surface={surface}
496
- title={name}
497
- >
498
- {state.list.query.isError && (
499
- <Alert
500
- action={
501
- <Button onClick={() => void state.list.query.refetch()} size="small">
502
- 重试
503
- </Button>
504
- }
505
- description={state.list.query.error instanceof Error ? state.list.query.error.message : '请稍后重试或联系平台管理员'}
506
- showIcon
507
- title="数据读取失败"
508
- type="error"
509
- />
510
- )}
511
- <section aria-label="数据筛选" className="oxa-search-panel">
512
- <div className="oxa-filter-grid">
513
- <div className="oxa-filter-item">
514
- <Select
515
- allowClear
516
- options={searchableFields.map((key) => ({
517
- label: fieldFor(surface, key).label,
518
- value: key,
519
- }))}
520
- placeholder="搜索字段"
521
- value={state.draft.searchField}
522
- onChange={(value) => state.setDraft((item) => ({ ...item, searchField: value }))}
523
- />
524
- </div>
525
- <div className="oxa-filter-item">
526
- <Input
527
- allowClear
528
- onChange={(event) =>
529
- state.setDraft((item) => ({
530
- ...item,
531
- keyword: event.target.value,
532
- }))
533
- }
534
- onPressEnter={() => {
535
- state.setPage(1);
536
- state.setQuery(state.draft);
537
- }}
538
- placeholder="关键词"
539
- prefix={<SearchOutlined />}
540
- value={state.draft.keyword}
541
- />
542
- </div>
543
- {filterFields.map((key, index) => {
544
- const field = fieldFor(surface, key);
545
- return (
546
- <div className={`oxa-filter-item${index >= 2 && !filtersExpanded ? ' is-collapsed' : ''}`} key={key}>
547
- <SurfaceFilterControl
548
- field={field}
549
- resourceCode={code}
550
- onChange={(value) =>
551
- state.setDraft((item) => ({
552
- ...item,
553
- filters: { ...item.filters, [key]: value },
554
- }))
555
- }
556
- value={state.draft.filters?.[key]}
557
- />
558
- </div>
559
- );
560
- })}
561
- </div>
562
- <div className="oxa-filter-actions">
563
- <Button
564
- icon={<SearchOutlined />}
565
- loading={state.list.query.isFetching}
566
- onClick={() => {
567
- state.setPage(1);
568
- state.setQuery(state.draft);
569
- }}
570
- type="primary"
571
- >
572
- 查询
573
- </Button>
574
- <Button
575
- onClick={() => {
576
- state.setDraft({});
577
- state.setQuery({});
578
- state.setPage(1);
579
- }}
580
- >
581
- 重置
582
- </Button>
583
- {hasMoreFilters && (
584
- <Button
585
- icon={filtersExpanded ? <UpOutlined /> : <DownOutlined />}
586
- onClick={() => setFiltersExpanded((value) => !value)}
587
- type="link"
588
- >
589
- {filtersExpanded ? '收起筛选' : `更多筛选(${filterFields.length - 2})`}
590
- </Button>
591
- )}
592
- </div>
593
- </section>
594
- <div className="oxa-list-toolbar">
595
- <Space className="oxa-list-toolbar-primary" wrap>
596
- {hasCapability(capabilities.create) && (
597
- <Button icon={<PlusOutlined />} onClick={() => navigate(`/${code}/new`)} type="primary">
598
- 新增{name}
599
- </Button>
600
- )}
601
- {hasCapability(capabilities.create) && (
602
- <ResourceImportButton
603
- code={code}
604
- name={name}
605
- surface={surface}
606
- writableFieldCodes={importWritableFieldCodes}
607
- onCompleted={() => state.list.query.refetch()}
608
- />
609
- )}
610
- <Button icon={<ExportOutlined />} loading={exporting} onClick={() => void handleExport()}>
611
- 导出
612
- </Button>
613
- </Space>
614
- <Space className="oxa-list-toolbar-secondary" size={4}>
615
- <Tooltip title="刷新">
616
- <Button
617
- aria-label="刷新数据"
618
- icon={<ReloadOutlined />}
619
- loading={state.list.query.isFetching}
620
- onClick={() => void state.list.query.refetch()}
621
- type="text"
622
- />
623
- </Tooltip>
624
- <Dropdown menu={columnMenu} trigger={['click']}>
625
- <Button icon={<SettingOutlined />} type="text">
626
- 列设置
627
- </Button>
628
- </Dropdown>
629
- <Dropdown menu={densityMenu} trigger={['click']}>
630
- <Button icon={<ColumnHeightOutlined />} type="text">
631
- 密度
632
- </Button>
633
- </Dropdown>
634
- </Space>
635
- </div>
636
- <ResourceBatchActions
637
- canDelete={hasCapability(capabilities.delete)}
638
- code={code}
639
- name={name}
640
- onClear={() => setSelectedRowKeys([])}
641
- onCompleted={() => state.list.query.refetch()}
642
- rows={selectedRows}
643
- surface={surface}
644
- writableFields={batchWritableFields}
645
- />
646
- <Table<ResourceRecord>
647
- columns={columns}
648
- dataSource={rows}
649
- loading={state.list.query.isFetching}
650
- locale={{ emptyText: <Empty description={`暂无${name}`} /> }}
651
- rowKey="id"
652
- rowSelection={
653
- canBatch
654
- ? {
655
- selectedRowKeys,
656
- onChange: (keys) => {
657
- const next = keys.map(String);
658
- if (next.length > 100) message.warning('单次最多选择 100 条记录');
659
- setSelectedRowKeys(next.slice(0, 100));
660
- },
661
- }
662
- : undefined
663
- }
664
- scroll={{ x: 'max-content' }}
665
- size={density}
666
- onChange={(_, __, sorter) => {
667
- const item = Array.isArray(sorter) ? sorter[0] : sorter;
668
- if (item?.field && item.order)
669
- state.setSort({
670
- field: String(item.field),
671
- order: item.order === 'ascend' ? 'asc' : 'desc',
672
- });
673
- }}
674
- pagination={{
675
- current: state.page,
676
- pageSize: state.pageSize,
677
- total: state.list.result.total || 0,
678
- showSizeChanger: true,
679
- onChange: (next, size) => {
680
- state.setPage(next);
681
- state.setPageSize(size);
682
- },
683
- }}
684
- />
685
- </ResourceListPage>
686
- );
687
- }
688
-
689
- function GeneratedMobileList({ definition }: { definition: GeneratedResourceDefinition }) {
690
- const navigate = useNavigate();
691
- const { hasCapability, identity } = useRuntime();
692
- const { message } = App.useApp();
693
- const { code, name, surface, capabilities } = definition;
694
- const readableFieldCodes = new Set(
695
- Object.entries(surface.fields)
696
- .filter(([, field]) => fieldReadable({ key: '', ...field }, hasCapability, identity.isAppSuperAdmin))
697
- .map(([key]) => key)
698
- );
699
- const state = useGeneratedList(definition, readableFieldCodes);
700
- const [exporting, setExporting] = useState(false);
701
- const rows = (state.list.result.data || []) as unknown as ResourceRecord[];
702
- const fields = listSurfaceFields(surface.fields)
703
- .filter((field) => fieldReadable(field, hasCapability, identity.isAppSuperAdmin))
704
- .slice(0, 4);
705
- const handleExport = async () => {
706
- setExporting(true);
707
- try {
708
- const result = await exportResourceRows(code, name, surface, fields, state.query, state.sort);
709
- message.success(`导出已生成(最多 ${result.rowLimit} 条)`);
710
- } catch (error) {
711
- message.error(error instanceof Error ? error.message : '导出失败');
712
- } finally {
713
- setExporting(false);
714
- }
715
- };
716
- return (
717
- <MobileResourceListPage
718
- createCapability={capabilities.create}
719
- createPath={`/${code}/new`}
720
- description={`${code} 资源`}
721
- onExport={() => void handleExport()}
722
- readCapability={capabilities.read}
723
- resource={code}
724
- surface={surface}
725
- title={name}
726
- >
727
- {state.list.query.isError && (
728
- <Alert
729
- action={
730
- <Button onClick={() => void state.list.query.refetch()} size="small">
731
- 重试
732
- </Button>
733
- }
734
- description={state.list.query.error instanceof Error ? state.list.query.error.message : '请稍后重试或联系平台管理员'}
735
- showIcon
736
- title="数据读取失败"
737
- type="error"
738
- />
739
- )}
740
- <div className="oxa-mobile-filter">
741
- <Input
742
- allowClear
743
- placeholder="关键词"
744
- value={state.draft.keyword}
745
- onChange={(event) => state.setDraft((item) => ({ ...item, keyword: event.target.value }))}
746
- onPressEnter={() => {
747
- state.setPage(1);
748
- state.setQuery(state.draft);
749
- }}
750
- />
751
- <Button
752
- loading={state.list.query.isFetching}
753
- icon={<SearchOutlined />}
754
- onClick={() => {
755
- state.setPage(1);
756
- state.setQuery(state.draft);
757
- }}
758
- type="primary"
759
- >
760
- 查询
761
- </Button>
762
- </div>
763
- {state.list.query.isFetching ? (
764
- <div className="oxa-page-loading">
765
- <Spin />
766
- </div>
767
- ) : rows.length ? (
768
- <div className="oxa-mobile-record-list">
769
- {rows.map((record) => (
770
- <Card className="oxa-mobile-record-card" key={record.id} size="small">
771
- <div className="oxa-mobile-record-heading">
772
- <a onClick={() => navigate(`/${code}/${record.id}`)}>{String(record[fields[0]?.key || 'id'] || '-')}</a>
773
- </div>
774
- <div className="oxa-mobile-record-fields">
775
- {fields.slice(1).map((field) => (
776
- <div key={field.key}>
777
- <Typography.Text type="secondary">{field.label}</Typography.Text>
778
- <div>
779
- <SurfaceFieldValue field={field} resourceCode={code} value={record[field.key]} />
780
- </div>
781
- </div>
782
- ))}
783
- </div>
784
- <Space>
785
- <Button onClick={() => navigate(`/${code}/${record.id}`)} size="small">
786
- 查看
787
- </Button>
788
- {hasCapability(capabilities.update) && (
789
- <Button onClick={() => navigate(`/${code}/${record.id}/edit`)} size="small">
790
- 编辑
791
- </Button>
792
- )}
793
- </Space>
794
- </Card>
795
- ))}
796
- </div>
797
- ) : (
798
- <Card>
799
- <Empty description={`暂无${name}`} />
800
- </Card>
801
- )}
802
- <div className="oxa-mobile-pagination">
803
- <Button disabled={state.page <= 1} onClick={() => state.setPage((item) => item - 1)}>
804
- 上一页
805
- </Button>
806
- <Typography.Text>第 {state.page} 页</Typography.Text>
807
- <Button disabled={rows.length < (surface.list?.defaultPageSize || 20)} onClick={() => state.setPage((item) => item + 1)}>
808
- 下一页
809
- </Button>
810
- </div>
811
- </MobileResourceListPage>
812
- );
813
- }
814
-
815
- function GeneratedResourceFormPage({
816
- definition,
817
- mode,
818
- variant,
819
- }: {
820
- definition: GeneratedResourceDefinition;
821
- mode: 'create' | 'edit';
822
- variant: 'desktop' | 'mobile';
823
- }) {
824
- const { id = '' } = useParams();
825
- const navigate = useNavigate();
826
- const { message } = App.useApp();
827
- const { hasCapability, identity } = useRuntime();
828
- const [form] = Form.useForm();
829
- const create = useCreate<ResourceRecord>();
830
- const update = useUpdate<ResourceRecord>();
831
- const [transactionPending, setTransactionPending] = useState(false);
832
- const query = useOne<ResourceRecord>({
833
- resource: definition.code,
834
- id,
835
- queryOptions: { enabled: mode === 'edit' },
836
- });
837
- const record = query.result;
838
- const { code, name, surface, capabilities } = definition;
839
- useEffect(() => {
840
- if (record) form.setFieldsValue(normalizeRecordForForm(record, surface));
841
- }, [form, record, surface]);
842
- const listPath = `/${code}`;
843
- const detailPath = `/${code}/${id}`;
844
- const Page = variant === 'mobile' ? MobileResourceFormPage : ResourceFormPage;
845
- const FieldControl = variant === 'mobile' ? MobileSurfaceFieldControl : SurfaceFieldControl;
846
- if (!hasCapability(mode === 'create' ? capabilities.create : capabilities.update))
847
- return <Result status="403" title="当前平台用户无此操作权限" />;
848
- const submit = async (values: Record<string, unknown>) => {
849
- const writableValues = Object.fromEntries(
850
- Object.entries(values).filter(([key]) =>
851
- fieldWritable(
852
- fieldFor(surface, key),
853
- mode,
854
- hasCapability,
855
- identity.isAppSuperAdmin
856
- )
857
- )
858
- );
859
- const subtableFields = Object.entries(surface.fields)
860
- .map(([key, field]) => ({ key, ...field }))
861
- .filter(
862
- field =>
863
- field.type === 'subtable' &&
864
- fieldWritable(
865
- field,
866
- mode,
867
- hasCapability,
868
- identity.isAppSuperAdmin
869
- )
870
- );
871
- const next = normalizeFormValues(
872
- Object.fromEntries(
873
- Object.entries(writableValues).filter(
874
- ([key]) => surface.fields[key]?.type !== 'subtable'
875
- )
876
- ),
877
- surface
878
- );
879
- if (subtableFields.length > 0) {
880
- if (mode === 'edit' && (!record?.id || !Number.isSafeInteger(record.revision))) {
881
- throw new Error('OPENXIANGDA_SUBTABLE_PARENT_REVISION_REQUIRED');
882
- }
883
- const operations: DataTransactionOperation[] = [
884
- mode === 'create'
885
- ? { operation: 'create', resourceCode: code, data: next }
886
- : {
887
- operation: 'update',
888
- resourceCode: code,
889
- id: record!.id,
890
- expectedRevision: record!.revision,
891
- data: next,
892
- },
893
- ];
894
- for (const subtableField of subtableFields) {
895
- const config = subtableField.subtable;
896
- const childDefinition = config
897
- ? definitionFor(config.resourceCode)
898
- : undefined;
899
- if (!config || !childDefinition) {
900
- throw new Error(`OPENXIANGDA_SUBTABLE_DEFINITION_MISSING:${subtableField.key}`);
901
- }
902
- operations.push(
903
- ...buildSubtableOperations({
904
- rows: Array.isArray(writableValues[subtableField.key])
905
- ? writableValues[subtableField.key] as SubtableDraftRow[]
906
- : [],
907
- childResourceCode: config.resourceCode,
908
- childSurface: childDefinition.surface,
909
- foreignKey: config.foreignKey,
910
- orderField: config.orderField,
911
- maxRows: config.maxRows,
912
- parent: mode === 'create'
913
- ? { operation: 'create', operationIndex: 0 }
914
- : { operation: 'update', id: record!.id },
915
- canWrite: (fieldCode, childField, childOperation) =>
916
- fieldWriteAuthorized(
917
- { key: fieldCode, ...childField },
918
- childOperation === 'create' ? 'create' : 'edit',
919
- hasCapability,
920
- identity.isAppSuperAdmin
921
- ),
922
- canDelete:
923
- identity.isAppSuperAdmin ||
924
- hasCapability(childDefinition.capabilities.delete),
925
- })
926
- );
927
- }
928
- if (operations.length > 100) {
929
- throw new Error('OPENXIANGDA_DATA_TRANSACTION_TOO_LARGE');
930
- }
931
- setTransactionPending(true);
932
- try {
933
- const result = await transactNativeData(operations);
934
- const parent = result.items[0];
935
- if (!parent || parent.operation === 'emitEvent') {
936
- throw new Error('OPENXIANGDA_SUBTABLE_PARENT_RESULT_MISSING');
937
- }
938
- message.success(mode === 'create' ? `${name}已创建` : '修改已保存');
939
- navigate(mode === 'create' ? listPath : `/${code}/${parent.id}`);
940
- } finally {
941
- setTransactionPending(false);
942
- }
943
- return;
944
- }
945
- if (mode === 'create') {
946
- await create.mutateAsync({ resource: code, values: next });
947
- message.success(`${name}已创建`);
948
- navigate(listPath);
949
- } else {
950
- const result = await update.mutateAsync({
951
- resource: code,
952
- id,
953
- values: next,
954
- meta: { expectedRevision: record?.revision },
955
- });
956
- message.success('修改已保存');
957
- navigate(`/${code}/${result.data.id}`);
958
- }
959
- };
960
- return (
961
- <Page
962
- backLabel={mode === 'edit' ? `返回${name}详情` : `返回${name}`}
963
- backPath={mode === 'edit' ? detailPath : listPath}
964
- capability={mode === 'create' ? capabilities.create : capabilities.update}
965
- detailPath={record ? detailPath : undefined}
966
- loading={mode === 'edit' && query.query.isFetching}
967
- mode={mode}
968
- notFound={mode === 'edit' && !record}
969
- resource={code}
970
- surface={surface}
971
- title={name}
972
- >
973
- <Form
974
- className={variant === 'mobile' ? 'oxa-mobile-form' : 'oxa-form-full'}
975
- form={form}
976
- initialValues={undefined}
977
- layout="vertical"
978
- onFinish={(values) => void submit(values as Record<string, unknown>)}
979
- >
980
- <div className="oxa-sections">
981
- {fieldsBySection(surface).map((group) => ({
982
- ...group,
983
- fields: group.fields.filter((field) =>
984
- fieldReadable(field, hasCapability, identity.isAppSuperAdmin) ||
985
- fieldWritable(field, mode, hasCapability, identity.isAppSuperAdmin)
986
- ),
987
- })).filter((group) => group.fields.length > 0).map((group) => (
988
- <Card className="oxa-section-card" key={group.section} title={group.section === 'default' ? '基本信息' : group.section}>
989
- <div className="oxa-grid">
990
- {group.fields.map((field) => (
991
- <FieldControl
992
- key={field.key}
993
- disabled={!fieldWritable(field, mode, hasCapability, identity.isAppSuperAdmin)}
994
- field={field}
995
- operation={mode === 'create' ? 'create' : 'update'}
996
- resourceCode={code}
997
- recordId={record?.id}
998
- renderers={{
999
- renderSubtable: (context) => (
1000
- <SubtableField
1001
- disabled={context.disabled}
1002
- field={context.field}
1003
- mobile={variant === 'mobile'}
1004
- operation={context.operation}
1005
- parentRecordId={context.recordId}
1006
- />
1007
- ),
1008
- signer: signatureSigner(identity.subjectProfile),
1009
- upload: (current, file, recordId) => createNativeResourceClient(code, surface).upload(current.key, file, recordId),
1010
- }}
1011
- />
1012
- ))}
1013
- </div>
1014
- </Card>
1015
- ))}
1016
- </div>
1017
- <div className="oxa-actions">
1018
- <Button onClick={() => navigate(mode === 'edit' ? detailPath : listPath)}>取消</Button>
1019
- <Button htmlType="submit" loading={create.mutation.isPending || update.mutation.isPending || transactionPending} type="primary">
1020
- {mode === 'create' ? '保存' : '保存修改'}
1021
- </Button>
1022
- </div>
1023
- </Form>
1024
- </Page>
1025
- );
1026
- }
1027
-
1028
- function GeneratedResourceDetailPage({ definition, variant }: { definition: GeneratedResourceDefinition; variant: 'desktop' | 'mobile' }) {
1029
- const { id = '' } = useParams();
1030
- const navigate = useNavigate();
1031
- const { hasCapability, identity } = useRuntime();
1032
- const query = useOne<ResourceRecord>({
1033
- resource: definition.code,
1034
- id,
1035
- queryOptions: { retry: false },
1036
- });
1037
- const record = query.result;
1038
- const [auditEntries, setAuditEntries] = useState<DataAuditEntry[]>([]);
1039
- const [auditLoading, setAuditLoading] = useState(false);
1040
- const [auditError, setAuditError] = useState('');
1041
- const [auditExpanded, setAuditExpanded] = useState(false);
1042
- const [auditLoadedFor, setAuditLoadedFor] = useState('');
1043
- useEffect(() => {
1044
- if (!record || !auditExpanded || auditLoadedFor === record.id) return;
1045
- let active = true;
1046
- setAuditLoading(true);
1047
- setAuditError('');
1048
- void createNativeResourceClient(definition.code, definition.surface)
1049
- .audit(record.id)
1050
- .then((page) => {
1051
- if (active) {
1052
- setAuditEntries(page.items || []);
1053
- setAuditLoadedFor(record.id);
1054
- }
1055
- })
1056
- .catch((reason) => {
1057
- if (!active) return;
1058
- setAuditEntries([]);
1059
- setAuditError(reason instanceof Error ? reason.message : String(reason));
1060
- })
1061
- .finally(() => {
1062
- if (active) setAuditLoading(false);
1063
- });
1064
- return () => {
1065
- active = false;
1066
- };
1067
- }, [auditExpanded, auditLoadedFor, definition.code, definition.surface, record]);
1068
- const auditContent = auditError ? (
1069
- <Alert
1070
- action={
1071
- <Button
1072
- onClick={() => {
1073
- setAuditError('');
1074
- setAuditLoadedFor('');
1075
- }}
1076
- size="small"
1077
- >
1078
- 重试
1079
- </Button>
1080
- }
1081
- description={auditError}
1082
- showIcon
1083
- title="变更记录读取失败"
1084
- type="error"
1085
- />
1086
- ) : auditLoading ? (
1087
- <div className="oxa-audit-loading">
1088
- <Spin size="small" /> 正在读取操作记录
1089
- </div>
1090
- ) : auditEntries.length ? (
1091
- auditEntries.slice(0, 5).map((entry) => (
1092
- <AuditEntry
1093
- entry={entry}
1094
- key={entry.id}
1095
- readable={(field) => fieldReadable(field, hasCapability, identity.isAppSuperAdmin)}
1096
- surface={definition.surface}
1097
- />
1098
- ))
1099
- ) : (
1100
- <Typography.Text type="secondary">暂无变更记录</Typography.Text>
1101
- );
1102
- const content = record ? (
1103
- <div className="oxa-detail-layout">
1104
- <div className="oxa-detail-main">
1105
- {fieldsBySection(definition.surface).map((group) => ({
1106
- ...group,
1107
- fields: group.fields.filter((field) =>
1108
- fieldReadable(field, hasCapability, identity.isAppSuperAdmin)
1109
- ),
1110
- })).filter((group) => group.fields.length > 0).map((group) => (
1111
- <Card className="oxa-section-card" key={group.section} title={group.section === 'default' ? '基本信息' : group.section}>
1112
- <Descriptions
1113
- column={{ xs: 1, sm: 2, lg: 3 }}
1114
- items={group.fields.map((field) => ({
1115
- key: field.key,
1116
- label: field.label,
1117
- span: field.type === 'subtable' ? 3 : 1,
1118
- children: field.type === 'subtable' ? (
1119
- <SubtableValueDisplay
1120
- field={field}
1121
- mobile={variant === 'mobile'}
1122
- parentRecordId={record.id}
1123
- />
1124
- ) : (
1125
- <SurfaceFieldValue
1126
- field={field}
1127
- resourceCode={definition.code}
1128
- value={record[field.key]}
1129
- />
1130
- ),
1131
- }))}
1132
- />
1133
- </Card>
1134
- ))}
1135
- <div id={`${definition.code}-audit`}>
1136
- <Collapse
1137
- activeKey={auditExpanded ? ['audit'] : []}
1138
- className="oxa-audit-collapse"
1139
- expandIconPlacement="end"
1140
- items={[
1141
- {
1142
- key: 'audit',
1143
- label: (
1144
- <div className="oxa-audit-collapse-heading">
1145
- <strong>操作记录</strong>
1146
- <span>查看创建、更新和删除的字段变化</span>
1147
- </div>
1148
- ),
1149
- children: auditContent,
1150
- },
1151
- ]}
1152
- onChange={(keys) => setAuditExpanded((Array.isArray(keys) ? keys : [keys]).includes('audit'))}
1153
- />
1154
- </div>
1155
- </div>
1156
- </div>
1157
- ) : null;
1158
- const hero = record ? (
1159
- <div>
1160
- <Typography.Title level={variant === 'mobile' ? 3 : 2}>{definition.name}详情</Typography.Title>
1161
- </div>
1162
- ) : null;
1163
- const props = {
1164
- auditTargetId: `${definition.code}-audit`,
1165
- backLabel: `返回${definition.name}`,
1166
- backPath: `/${definition.code}`,
1167
- editCapability: definition.capabilities.update,
1168
- editPath: `/${definition.code}/${id}/edit`,
1169
- error: query.query.isError || (!query.query.isFetching && !record) ? query.query.error?.message || '记录不存在' : undefined,
1170
- hero,
1171
- loading: query.query.isFetching,
1172
- meta: record ? <span>最近更新 {record.updated_at ? new Date(record.updated_at).toLocaleString('zh-CN') : '-'}</span> : null,
1173
- onAuditOpen: () => setAuditExpanded(true),
1174
- readCapability: definition.capabilities.read,
1175
- resource: definition.code,
1176
- surface: definition.surface,
1177
- title: definition.name,
1178
- };
1179
- return variant === 'mobile' ? (
1180
- <MobileResourceDetailPage {...props}>{content}</MobileResourceDetailPage>
1181
- ) : (
1182
- <ResourceDetailPage {...props}>{content}</ResourceDetailPage>
1183
- );
1184
- }