openxiangda-cli 2.0.0-alpha.66 → 2.0.0-alpha.68
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/package.json +1 -1
- package/template/apps/web/e2e/resources.spec.ts +11 -0
- package/template/apps/web/src/FilePreviewPage.tsx +3 -3
- package/template/apps/web/src/Shell.tsx +323 -33
- package/template/apps/web/src/appearance.tsx +147 -0
- package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +426 -59
- package/template/apps/web/src/components/resource/StandardResourcePages.tsx +13 -52
- package/template/apps/web/src/components/resource/SurfaceFields.tsx +1 -1
- package/template/apps/web/src/main.tsx +22 -21
- package/template/apps/web/src/platform-client.ts +16 -3
- package/template/apps/web/src/styles.css +425 -219
- package/template/apps/web/test/contracts.test.ts +42 -0
- package/template/package.json +1 -1
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ArrowRightOutlined,
|
|
3
|
+
ColumnHeightOutlined,
|
|
3
4
|
DeleteOutlined,
|
|
4
5
|
EditOutlined,
|
|
5
6
|
EyeOutlined,
|
|
7
|
+
ExportOutlined,
|
|
8
|
+
ImportOutlined,
|
|
6
9
|
PlusOutlined,
|
|
10
|
+
ReloadOutlined,
|
|
7
11
|
SearchOutlined,
|
|
12
|
+
SettingOutlined,
|
|
13
|
+
UpOutlined,
|
|
14
|
+
DownOutlined,
|
|
8
15
|
} from '@ant-design/icons';
|
|
9
16
|
import { useCreate, useDelete, useList, useOne, useUpdate } from '@refinedev/core';
|
|
10
17
|
import {
|
|
@@ -12,7 +19,9 @@ import {
|
|
|
12
19
|
Alert,
|
|
13
20
|
Button,
|
|
14
21
|
Card,
|
|
22
|
+
Collapse,
|
|
15
23
|
Descriptions,
|
|
24
|
+
Dropdown,
|
|
16
25
|
Empty,
|
|
17
26
|
Form,
|
|
18
27
|
Input,
|
|
@@ -22,7 +31,9 @@ import {
|
|
|
22
31
|
Space,
|
|
23
32
|
Spin,
|
|
24
33
|
Table,
|
|
34
|
+
Tooltip,
|
|
25
35
|
Typography,
|
|
36
|
+
type MenuProps,
|
|
26
37
|
type TableColumnsType,
|
|
27
38
|
} from 'antd';
|
|
28
39
|
import dayjs from 'dayjs';
|
|
@@ -247,15 +258,21 @@ function AuditEntry({ entry, surface }: { entry: DataAuditEntry; surface: DataRe
|
|
|
247
258
|
|
|
248
259
|
function useGeneratedList(definition: GeneratedDefinition) {
|
|
249
260
|
const { code, surface } = definition;
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
};
|
|
261
|
+
const defaultSortField = surface.list?.defaultSort?.field || Object.keys(surface.fields)[0] || 'id';
|
|
262
|
+
const defaultSortOrder = surface.list?.defaultSort?.order || 'asc';
|
|
263
|
+
const defaultPageSize = surface.list?.defaultPageSize || 20;
|
|
254
264
|
const [page, setPage] = useState(1);
|
|
255
|
-
const [pageSize, setPageSize] = useState(
|
|
265
|
+
const [pageSize, setPageSize] = useState(defaultPageSize);
|
|
256
266
|
const [draft, setDraft] = useState<Partial<GenericResourceQuery>>({});
|
|
257
267
|
const [query, setQuery] = useState<Partial<GenericResourceQuery>>({});
|
|
258
|
-
const [sort, setSort] = useState({ field:
|
|
268
|
+
const [sort, setSort] = useState({ field: defaultSortField, order: defaultSortOrder as 'asc' | 'desc' });
|
|
269
|
+
useEffect(() => {
|
|
270
|
+
setPage(1);
|
|
271
|
+
setPageSize(defaultPageSize);
|
|
272
|
+
setDraft({});
|
|
273
|
+
setQuery({});
|
|
274
|
+
setSort({ field: defaultSortField, order: defaultSortOrder as 'asc' | 'desc' });
|
|
275
|
+
}, [code, defaultPageSize, defaultSortField, defaultSortOrder]);
|
|
259
276
|
const list = useList<ResourceRecord>({
|
|
260
277
|
resource: code,
|
|
261
278
|
pagination: { currentPage: page, pageSize },
|
|
@@ -278,9 +295,10 @@ export function GeneratedResourcePage({
|
|
|
278
295
|
if (!definition || definition.surface.generated === false) {
|
|
279
296
|
return <Result status="404" title="未声明可生成的资源页面" />;
|
|
280
297
|
}
|
|
281
|
-
|
|
282
|
-
if (mode === '
|
|
283
|
-
return <
|
|
298
|
+
const pageKey = `${variant}:${mode}:${resourceCode}`;
|
|
299
|
+
if (mode === 'list') return <GeneratedResourceListPage key={pageKey} definition={definition} variant={variant} />;
|
|
300
|
+
if (mode === 'detail') return <GeneratedResourceDetailPage key={pageKey} definition={definition} variant={variant} />;
|
|
301
|
+
return <GeneratedResourceFormPage key={pageKey} definition={definition} mode={mode} variant={variant} />;
|
|
284
302
|
}
|
|
285
303
|
|
|
286
304
|
function GeneratedResourceListPage({ definition, variant }: { definition: GeneratedDefinition; variant: 'desktop' | 'mobile' }) {
|
|
@@ -292,40 +310,122 @@ function GeneratedResourceListPage({ definition, variant }: { definition: Genera
|
|
|
292
310
|
function GeneratedDesktopList({ definition }: { definition: GeneratedDefinition }) {
|
|
293
311
|
const navigate = useNavigate();
|
|
294
312
|
const { message } = App.useApp();
|
|
295
|
-
const { hasCapability
|
|
313
|
+
const { hasCapability } = useRuntime();
|
|
296
314
|
const state = useGeneratedList(definition);
|
|
297
315
|
const { code, name, surface, capabilities } = definition;
|
|
298
316
|
const fields = listSurfaceFields(surface.fields);
|
|
299
317
|
const [exporting, setExporting] = useState(false);
|
|
318
|
+
const [filtersExpanded, setFiltersExpanded] = useState(false);
|
|
319
|
+
const [density, setDensity] = useState<'small' | 'middle' | 'large'>('middle');
|
|
320
|
+
const [visibleColumnKeys, setVisibleColumnKeys] = useState<string[]>(() =>
|
|
321
|
+
fields.map(field => field.key),
|
|
322
|
+
);
|
|
300
323
|
const remove = useDelete();
|
|
301
324
|
const rows = (state.list.result.data || []) as unknown as ResourceRecord[];
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
325
|
+
useEffect(() => {
|
|
326
|
+
setVisibleColumnKeys(fields.map(field => field.key));
|
|
327
|
+
setFiltersExpanded(false);
|
|
328
|
+
setDensity('middle');
|
|
329
|
+
}, [code]);
|
|
330
|
+
const columns = useMemo<TableColumnsType<ResourceRecord>>(
|
|
331
|
+
() => [
|
|
332
|
+
...fields
|
|
333
|
+
.filter(field => visibleColumnKeys.includes(field.key))
|
|
334
|
+
.map(field => ({
|
|
335
|
+
title: field.label,
|
|
336
|
+
dataIndex: field.key,
|
|
337
|
+
key: field.key,
|
|
338
|
+
sorter: Boolean(field.sortable),
|
|
339
|
+
render: (value: unknown, record: ResourceRecord) => (
|
|
340
|
+
<SurfaceFieldValue
|
|
341
|
+
field={field}
|
|
342
|
+
value={value}
|
|
343
|
+
renderers={{
|
|
344
|
+
renderValue: ({ value: current }) =>
|
|
345
|
+
field.key === fields[0]?.key ? (
|
|
346
|
+
<a onClick={() => navigate(`/${code}/${record.id}`)}>
|
|
347
|
+
{String(current || '-')}
|
|
348
|
+
</a>
|
|
349
|
+
) : undefined,
|
|
350
|
+
}}
|
|
351
|
+
/>
|
|
352
|
+
),
|
|
353
|
+
})),
|
|
354
|
+
{
|
|
355
|
+
title: '更新时间',
|
|
356
|
+
dataIndex: 'updated_at',
|
|
357
|
+
key: 'updated_at',
|
|
358
|
+
render: value =>
|
|
359
|
+
value ? new Date(String(value)).toLocaleString('zh-CN') : '-',
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
title: '操作',
|
|
363
|
+
key: 'actions',
|
|
364
|
+
fixed: 'right' as const,
|
|
365
|
+
render: (_: unknown, record: ResourceRecord) => (
|
|
366
|
+
<Space size={4}>
|
|
367
|
+
<Button
|
|
368
|
+
aria-label="查看"
|
|
369
|
+
icon={<EyeOutlined />}
|
|
370
|
+
onClick={() => navigate(`/${code}/${record.id}`)}
|
|
371
|
+
size="small"
|
|
372
|
+
type="text"
|
|
373
|
+
/>
|
|
374
|
+
{hasCapability(capabilities.update) && (
|
|
375
|
+
<Button
|
|
376
|
+
aria-label="编辑"
|
|
377
|
+
icon={<EditOutlined />}
|
|
378
|
+
onClick={() => navigate(`/${code}/${record.id}/edit`)}
|
|
379
|
+
size="small"
|
|
380
|
+
type="text"
|
|
381
|
+
/>
|
|
382
|
+
)}
|
|
383
|
+
{hasCapability(capabilities.delete) && (
|
|
384
|
+
<Popconfirm
|
|
385
|
+
onConfirm={() =>
|
|
386
|
+
remove.mutate(
|
|
387
|
+
{
|
|
388
|
+
resource: code,
|
|
389
|
+
id: record.id,
|
|
390
|
+
meta: { expectedRevision: record.revision },
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
onSuccess: () => {
|
|
394
|
+
message.success('已删除');
|
|
395
|
+
void state.list.query.refetch();
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
title={`确定删除${name}?`}
|
|
401
|
+
>
|
|
402
|
+
<Button
|
|
403
|
+
aria-label="删除"
|
|
404
|
+
danger
|
|
405
|
+
icon={<DeleteOutlined />}
|
|
406
|
+
size="small"
|
|
407
|
+
type="text"
|
|
408
|
+
/>
|
|
409
|
+
</Popconfirm>
|
|
410
|
+
)}
|
|
411
|
+
</Space>
|
|
412
|
+
),
|
|
413
|
+
},
|
|
414
|
+
],
|
|
415
|
+
[
|
|
416
|
+
capabilities.delete,
|
|
417
|
+
capabilities.update,
|
|
418
|
+
code,
|
|
419
|
+
fields,
|
|
420
|
+
hasCapability,
|
|
421
|
+
message,
|
|
422
|
+
name,
|
|
423
|
+
navigate,
|
|
424
|
+
remove,
|
|
425
|
+
state.list.query,
|
|
426
|
+
visibleColumnKeys,
|
|
427
|
+
],
|
|
428
|
+
);
|
|
329
429
|
const handleExport = async () => {
|
|
330
430
|
setExporting(true);
|
|
331
431
|
try {
|
|
@@ -337,12 +437,34 @@ function GeneratedDesktopList({ definition }: { definition: GeneratedDefinition
|
|
|
337
437
|
setExporting(false);
|
|
338
438
|
}
|
|
339
439
|
};
|
|
440
|
+
const filterFields = surface.list?.filterFields || [];
|
|
441
|
+
const hasMoreFilters = filterFields.length > 2;
|
|
442
|
+
const columnMenu: MenuProps = {
|
|
443
|
+
items: fields.map(field => ({ key: field.key, label: field.label })),
|
|
444
|
+
multiple: true,
|
|
445
|
+
selectable: true,
|
|
446
|
+
selectedKeys: visibleColumnKeys,
|
|
447
|
+
onSelect: ({ selectedKeys }) =>
|
|
448
|
+
setVisibleColumnKeys(selectedKeys.map(String)),
|
|
449
|
+
onDeselect: ({ selectedKeys }) => {
|
|
450
|
+
if (selectedKeys.length) setVisibleColumnKeys(selectedKeys.map(String));
|
|
451
|
+
else message.info('至少保留一列');
|
|
452
|
+
},
|
|
453
|
+
};
|
|
454
|
+
const densityMenu: MenuProps = {
|
|
455
|
+
items: [
|
|
456
|
+
{ key: 'large', label: '宽松' },
|
|
457
|
+
{ key: 'middle', label: '标准' },
|
|
458
|
+
{ key: 'small', label: '紧凑' },
|
|
459
|
+
],
|
|
460
|
+
selectable: true,
|
|
461
|
+
selectedKeys: [density],
|
|
462
|
+
onClick: ({ key }) => setDensity(key as typeof density),
|
|
463
|
+
};
|
|
340
464
|
return (
|
|
341
465
|
<ResourceListPage
|
|
342
466
|
createCapability={capabilities.create}
|
|
343
467
|
createPath={`/${code}/new`}
|
|
344
|
-
description={`由 ${code} 资源 Surface 自动生成的标准 CRUD 页面`}
|
|
345
|
-
onExport={() => void handleExport()}
|
|
346
468
|
readCapability={capabilities.read}
|
|
347
469
|
resource={code}
|
|
348
470
|
surface={surface}
|
|
@@ -352,21 +474,144 @@ function GeneratedDesktopList({ definition }: { definition: GeneratedDefinition
|
|
|
352
474
|
<Alert
|
|
353
475
|
action={<Button onClick={() => void state.list.query.refetch()} size="small">重试</Button>}
|
|
354
476
|
description={state.list.query.error instanceof Error ? state.list.query.error.message : '请稍后重试或联系平台管理员'}
|
|
355
|
-
message="数据读取失败"
|
|
356
477
|
showIcon
|
|
478
|
+
title="数据读取失败"
|
|
357
479
|
type="error"
|
|
358
480
|
/>
|
|
359
481
|
)}
|
|
360
|
-
<
|
|
361
|
-
<
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
482
|
+
<section aria-label="数据筛选" className="oxa-search-panel">
|
|
483
|
+
<div className="oxa-filter-grid">
|
|
484
|
+
<div className="oxa-filter-item">
|
|
485
|
+
<Select
|
|
486
|
+
allowClear
|
|
487
|
+
options={(surface.list?.searchableFields || []).map(key => ({
|
|
488
|
+
label: fieldFor(surface, key).label,
|
|
489
|
+
value: key,
|
|
490
|
+
}))}
|
|
491
|
+
placeholder="搜索字段"
|
|
492
|
+
value={state.draft.searchField}
|
|
493
|
+
onChange={value =>
|
|
494
|
+
state.setDraft(item => ({ ...item, searchField: value }))
|
|
495
|
+
}
|
|
496
|
+
/>
|
|
497
|
+
</div>
|
|
498
|
+
<div className="oxa-filter-item">
|
|
499
|
+
<Input
|
|
500
|
+
allowClear
|
|
501
|
+
onChange={event =>
|
|
502
|
+
state.setDraft(item => ({
|
|
503
|
+
...item,
|
|
504
|
+
keyword: event.target.value,
|
|
505
|
+
}))
|
|
506
|
+
}
|
|
507
|
+
onPressEnter={() => {
|
|
508
|
+
state.setPage(1);
|
|
509
|
+
state.setQuery(state.draft);
|
|
510
|
+
}}
|
|
511
|
+
placeholder="关键词"
|
|
512
|
+
prefix={<SearchOutlined />}
|
|
513
|
+
value={state.draft.keyword}
|
|
514
|
+
/>
|
|
515
|
+
</div>
|
|
516
|
+
{filterFields.map((key, index) => {
|
|
517
|
+
const field = fieldFor(surface, key);
|
|
518
|
+
return (
|
|
519
|
+
<div
|
|
520
|
+
className={`oxa-filter-item${index >= 2 && !filtersExpanded ? ' is-collapsed' : ''}`}
|
|
521
|
+
key={key}
|
|
522
|
+
>
|
|
523
|
+
<SurfaceFilterControl
|
|
524
|
+
field={field}
|
|
525
|
+
onChange={value =>
|
|
526
|
+
state.setDraft(item => ({
|
|
527
|
+
...item,
|
|
528
|
+
filters: { ...item.filters, [key]: value },
|
|
529
|
+
}))
|
|
530
|
+
}
|
|
531
|
+
value={state.draft.filters?.[key]}
|
|
532
|
+
/>
|
|
533
|
+
</div>
|
|
534
|
+
);
|
|
535
|
+
})}
|
|
536
|
+
</div>
|
|
537
|
+
<div className="oxa-filter-actions">
|
|
538
|
+
<Button
|
|
539
|
+
icon={<SearchOutlined />}
|
|
540
|
+
loading={state.list.query.isFetching}
|
|
541
|
+
onClick={() => {
|
|
542
|
+
state.setPage(1);
|
|
543
|
+
state.setQuery(state.draft);
|
|
544
|
+
}}
|
|
545
|
+
type="primary"
|
|
546
|
+
>
|
|
547
|
+
查询
|
|
548
|
+
</Button>
|
|
549
|
+
<Button
|
|
550
|
+
onClick={() => {
|
|
551
|
+
state.setDraft({});
|
|
552
|
+
state.setQuery({});
|
|
553
|
+
state.setPage(1);
|
|
554
|
+
}}
|
|
555
|
+
>
|
|
556
|
+
重置
|
|
557
|
+
</Button>
|
|
558
|
+
{hasMoreFilters && (
|
|
559
|
+
<Button
|
|
560
|
+
icon={filtersExpanded ? <UpOutlined /> : <DownOutlined />}
|
|
561
|
+
onClick={() => setFiltersExpanded(value => !value)}
|
|
562
|
+
type="link"
|
|
563
|
+
>
|
|
564
|
+
{filtersExpanded ? '收起筛选' : `更多筛选(${filterFields.length - 2})`}
|
|
565
|
+
</Button>
|
|
566
|
+
)}
|
|
567
|
+
</div>
|
|
568
|
+
</section>
|
|
569
|
+
<div className="oxa-list-toolbar">
|
|
570
|
+
<Space className="oxa-list-toolbar-primary" wrap>
|
|
571
|
+
{hasCapability(capabilities.create) && (
|
|
572
|
+
<Button
|
|
573
|
+
icon={<PlusOutlined />}
|
|
574
|
+
onClick={() => navigate(`/${code}/new`)}
|
|
575
|
+
type="primary"
|
|
576
|
+
>
|
|
577
|
+
新增{name}
|
|
578
|
+
</Button>
|
|
579
|
+
)}
|
|
580
|
+
<Button
|
|
581
|
+
disabled
|
|
582
|
+
icon={<ImportOutlined />}
|
|
583
|
+
title="导入能力将在批量事务合同落地后启用"
|
|
584
|
+
>
|
|
585
|
+
导入
|
|
586
|
+
</Button>
|
|
587
|
+
<Button
|
|
588
|
+
icon={<ExportOutlined />}
|
|
589
|
+
loading={exporting}
|
|
590
|
+
onClick={() => void handleExport()}
|
|
591
|
+
>
|
|
592
|
+
导出
|
|
593
|
+
</Button>
|
|
594
|
+
</Space>
|
|
595
|
+
<Space className="oxa-list-toolbar-secondary" size={4}>
|
|
596
|
+
<Tooltip title="刷新">
|
|
597
|
+
<Button
|
|
598
|
+
aria-label="刷新数据"
|
|
599
|
+
icon={<ReloadOutlined />}
|
|
600
|
+
loading={state.list.query.isFetching}
|
|
601
|
+
onClick={() => void state.list.query.refetch()}
|
|
602
|
+
type="text"
|
|
603
|
+
/>
|
|
604
|
+
</Tooltip>
|
|
605
|
+
<Dropdown menu={columnMenu} trigger={['click']}>
|
|
606
|
+
<Button icon={<SettingOutlined />} type="text">
|
|
607
|
+
列设置
|
|
608
|
+
</Button>
|
|
609
|
+
</Dropdown>
|
|
610
|
+
<Dropdown menu={densityMenu} trigger={['click']}>
|
|
611
|
+
<Button icon={<ColumnHeightOutlined />} type="text">
|
|
612
|
+
密度
|
|
613
|
+
</Button>
|
|
614
|
+
</Dropdown>
|
|
370
615
|
</Space>
|
|
371
616
|
</div>
|
|
372
617
|
<Table<ResourceRecord>
|
|
@@ -375,6 +620,8 @@ function GeneratedDesktopList({ definition }: { definition: GeneratedDefinition
|
|
|
375
620
|
loading={state.list.query.isFetching}
|
|
376
621
|
locale={{ emptyText: <Empty description={`暂无${name}`} /> }}
|
|
377
622
|
rowKey="id"
|
|
623
|
+
scroll={{ x: 'max-content' }}
|
|
624
|
+
size={density}
|
|
378
625
|
onChange={(_, __, sorter) => { const item = Array.isArray(sorter) ? sorter[0] : sorter; if (item?.field && item.order) state.setSort({ field: String(item.field), order: item.order === 'ascend' ? 'asc' : 'desc' }); }}
|
|
379
626
|
pagination={{ current: state.page, pageSize: state.pageSize, total: state.list.result.total || 0, showSizeChanger: true, onChange: (next, size) => { state.setPage(next); state.setPageSize(size); } }}
|
|
380
627
|
/>
|
|
@@ -408,8 +655,8 @@ function GeneratedMobileList({ definition }: { definition: GeneratedDefinition }
|
|
|
408
655
|
<Alert
|
|
409
656
|
action={<Button onClick={() => void state.list.query.refetch()} size="small">重试</Button>}
|
|
410
657
|
description={state.list.query.error instanceof Error ? state.list.query.error.message : '请稍后重试或联系平台管理员'}
|
|
411
|
-
message="数据读取失败"
|
|
412
658
|
showIcon
|
|
659
|
+
title="数据读取失败"
|
|
413
660
|
type="error"
|
|
414
661
|
/>
|
|
415
662
|
)}
|
|
@@ -453,7 +700,7 @@ function GeneratedResourceFormPage({ definition, mode, variant }: { definition:
|
|
|
453
700
|
else { const result = await update.mutateAsync({ resource: code, id, values: next, meta: { expectedRevision: record?.revision } }); message.success('修改已保存'); navigate(`/${code}/${result.data.id}`); }
|
|
454
701
|
};
|
|
455
702
|
return (
|
|
456
|
-
<Page backLabel={mode === 'edit' ? `返回${name}详情` : `返回${name}`} backPath={mode === 'edit' ? detailPath : listPath} capability={mode === 'create' ? capabilities.create : capabilities.update} detailPath={record ? detailPath : undefined} loading={mode === 'edit' && query.query.isFetching} mode={mode} notFound={mode === 'edit' && !record}
|
|
703
|
+
<Page backLabel={mode === 'edit' ? `返回${name}详情` : `返回${name}`} backPath={mode === 'edit' ? detailPath : listPath} capability={mode === 'create' ? capabilities.create : capabilities.update} detailPath={record ? detailPath : undefined} loading={mode === 'edit' && query.query.isFetching} mode={mode} notFound={mode === 'edit' && !record} resource={code} surface={surface} title={name}>
|
|
457
704
|
<Form className={variant === 'mobile' ? 'oxa-mobile-form' : 'oxa-form-full'} form={form} initialValues={undefined} layout="vertical" onFinish={values => void submit(values as Record<string, unknown>)}>
|
|
458
705
|
<div className="oxa-sections">{fieldsBySection(surface).map(group => <Card className="oxa-section-card" key={group.section} title={group.section === 'default' ? '基本信息' : group.section}><div className="oxa-grid">{group.fields.map(field => <FieldControl key={field.key} disabled={!fieldWritable(field, mode, hasCapability, identity.isAppSuperAdmin)} field={field} operation={mode === 'create' ? 'create' : 'update'} recordId={record?.id} renderers={{ upload: (current, file, recordId) => createNativeResourceClient(code, surface).upload(current.key, file, recordId) }} />)}</div></Card>)}</div>
|
|
459
706
|
<div className="oxa-actions"><Button onClick={() => navigate(mode === 'edit' ? detailPath : listPath)}>取消</Button><Button htmlType="submit" loading={create.mutation.isPending || update.mutation.isPending} type="primary">{mode === 'create' ? '保存' : '保存修改'}</Button></div>
|
|
@@ -470,14 +717,19 @@ function GeneratedResourceDetailPage({ definition, variant }: { definition: Gene
|
|
|
470
717
|
const [auditEntries, setAuditEntries] = useState<DataAuditEntry[]>([]);
|
|
471
718
|
const [auditLoading, setAuditLoading] = useState(false);
|
|
472
719
|
const [auditError, setAuditError] = useState('');
|
|
720
|
+
const [auditExpanded, setAuditExpanded] = useState(false);
|
|
721
|
+
const [auditLoadedFor, setAuditLoadedFor] = useState('');
|
|
473
722
|
useEffect(() => {
|
|
474
|
-
if (!record) return;
|
|
723
|
+
if (!record || !auditExpanded || auditLoadedFor === record.id) return;
|
|
475
724
|
let active = true;
|
|
476
725
|
setAuditLoading(true);
|
|
477
726
|
setAuditError('');
|
|
478
727
|
void createNativeResourceClient(definition.code, definition.surface).audit(record.id)
|
|
479
728
|
.then(page => {
|
|
480
|
-
if (active)
|
|
729
|
+
if (active) {
|
|
730
|
+
setAuditEntries(page.items || []);
|
|
731
|
+
setAuditLoadedFor(record.id);
|
|
732
|
+
}
|
|
481
733
|
})
|
|
482
734
|
.catch(reason => {
|
|
483
735
|
if (!active) return;
|
|
@@ -490,9 +742,124 @@ function GeneratedResourceDetailPage({ definition, variant }: { definition: Gene
|
|
|
490
742
|
return () => {
|
|
491
743
|
active = false;
|
|
492
744
|
};
|
|
493
|
-
}, [definition.code, definition.surface, record
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
745
|
+
}, [auditExpanded, auditLoadedFor, definition.code, definition.surface, record]);
|
|
746
|
+
const auditContent = auditError ? (
|
|
747
|
+
<Alert
|
|
748
|
+
action={
|
|
749
|
+
<Button
|
|
750
|
+
onClick={() => {
|
|
751
|
+
setAuditError('');
|
|
752
|
+
setAuditLoadedFor('');
|
|
753
|
+
}}
|
|
754
|
+
size="small"
|
|
755
|
+
>
|
|
756
|
+
重试
|
|
757
|
+
</Button>
|
|
758
|
+
}
|
|
759
|
+
description={auditError}
|
|
760
|
+
showIcon
|
|
761
|
+
title="变更记录读取失败"
|
|
762
|
+
type="error"
|
|
763
|
+
/>
|
|
764
|
+
) : auditLoading ? (
|
|
765
|
+
<div className="oxa-audit-loading"><Spin size="small" /> 正在读取操作记录</div>
|
|
766
|
+
) : auditEntries.length ? (
|
|
767
|
+
auditEntries
|
|
768
|
+
.slice(0, 5)
|
|
769
|
+
.map(entry => (
|
|
770
|
+
<AuditEntry
|
|
771
|
+
entry={entry}
|
|
772
|
+
key={entry.id}
|
|
773
|
+
surface={definition.surface}
|
|
774
|
+
/>
|
|
775
|
+
))
|
|
776
|
+
) : (
|
|
777
|
+
<Typography.Text type="secondary">暂无变更记录</Typography.Text>
|
|
778
|
+
);
|
|
779
|
+
const content = record ? (
|
|
780
|
+
<div className="oxa-detail-layout">
|
|
781
|
+
<div className="oxa-detail-main">
|
|
782
|
+
{fieldsBySection(definition.surface).map(group => (
|
|
783
|
+
<Card
|
|
784
|
+
className="oxa-section-card"
|
|
785
|
+
key={group.section}
|
|
786
|
+
title={group.section === 'default' ? '基本信息' : group.section}
|
|
787
|
+
>
|
|
788
|
+
<Descriptions
|
|
789
|
+
column={{ xs: 1, sm: 2, lg: 3 }}
|
|
790
|
+
items={group.fields.map(field => ({
|
|
791
|
+
key: field.key,
|
|
792
|
+
label: field.label,
|
|
793
|
+
children: (
|
|
794
|
+
<SurfaceFieldValue field={field} value={record[field.key]} />
|
|
795
|
+
),
|
|
796
|
+
}))}
|
|
797
|
+
/>
|
|
798
|
+
</Card>
|
|
799
|
+
))}
|
|
800
|
+
<div id={`${definition.code}-audit`}>
|
|
801
|
+
<Collapse
|
|
802
|
+
activeKey={auditExpanded ? ['audit'] : []}
|
|
803
|
+
className="oxa-audit-collapse"
|
|
804
|
+
expandIconPlacement="end"
|
|
805
|
+
items={[
|
|
806
|
+
{
|
|
807
|
+
key: 'audit',
|
|
808
|
+
label: (
|
|
809
|
+
<div className="oxa-audit-collapse-heading">
|
|
810
|
+
<strong>操作记录</strong>
|
|
811
|
+
<span>查看创建、更新和删除的字段变化</span>
|
|
812
|
+
</div>
|
|
813
|
+
),
|
|
814
|
+
children: auditContent,
|
|
815
|
+
},
|
|
816
|
+
]}
|
|
817
|
+
onChange={keys =>
|
|
818
|
+
setAuditExpanded(
|
|
819
|
+
(Array.isArray(keys) ? keys : [keys]).includes('audit'),
|
|
820
|
+
)
|
|
821
|
+
}
|
|
822
|
+
/>
|
|
823
|
+
</div>
|
|
824
|
+
</div>
|
|
825
|
+
</div>
|
|
826
|
+
) : null;
|
|
827
|
+
const hero = record ? (
|
|
828
|
+
<div>
|
|
829
|
+
<Typography.Title level={variant === 'mobile' ? 3 : 2}>
|
|
830
|
+
{definition.name}详情
|
|
831
|
+
</Typography.Title>
|
|
832
|
+
</div>
|
|
833
|
+
) : null;
|
|
834
|
+
const props = {
|
|
835
|
+
auditTargetId: `${definition.code}-audit`,
|
|
836
|
+
backLabel: `返回${definition.name}`,
|
|
837
|
+
backPath: `/${definition.code}`,
|
|
838
|
+
editCapability: definition.capabilities.update,
|
|
839
|
+
editPath: `/${definition.code}/${id}/edit`,
|
|
840
|
+
error:
|
|
841
|
+
query.query.isError || (!query.query.isFetching && !record)
|
|
842
|
+
? query.query.error?.message || '记录不存在'
|
|
843
|
+
: undefined,
|
|
844
|
+
hero,
|
|
845
|
+
loading: query.query.isFetching,
|
|
846
|
+
meta: record ? (
|
|
847
|
+
<span>
|
|
848
|
+
最近更新{' '}
|
|
849
|
+
{record.updated_at
|
|
850
|
+
? new Date(record.updated_at).toLocaleString('zh-CN')
|
|
851
|
+
: '-'}
|
|
852
|
+
</span>
|
|
853
|
+
) : null,
|
|
854
|
+
onAuditOpen: () => setAuditExpanded(true),
|
|
855
|
+
readCapability: definition.capabilities.read,
|
|
856
|
+
resource: definition.code,
|
|
857
|
+
surface: definition.surface,
|
|
858
|
+
title: definition.name,
|
|
859
|
+
};
|
|
860
|
+
return variant === 'mobile' ? (
|
|
861
|
+
<MobileResourceDetailPage {...props}>{content}</MobileResourceDetailPage>
|
|
862
|
+
) : (
|
|
863
|
+
<ResourceDetailPage {...props}>{content}</ResourceDetailPage>
|
|
864
|
+
);
|
|
498
865
|
}
|