openxiangda-cli 2.0.0-alpha.67 → 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 +410 -50
- 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 +7 -0
- package/template/apps/web/src/styles.css +425 -219
- package/template/apps/web/test/contracts.test.ts +37 -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';
|
|
@@ -299,40 +310,122 @@ function GeneratedResourceListPage({ definition, variant }: { definition: Genera
|
|
|
299
310
|
function GeneratedDesktopList({ definition }: { definition: GeneratedDefinition }) {
|
|
300
311
|
const navigate = useNavigate();
|
|
301
312
|
const { message } = App.useApp();
|
|
302
|
-
const { hasCapability
|
|
313
|
+
const { hasCapability } = useRuntime();
|
|
303
314
|
const state = useGeneratedList(definition);
|
|
304
315
|
const { code, name, surface, capabilities } = definition;
|
|
305
316
|
const fields = listSurfaceFields(surface.fields);
|
|
306
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
|
+
);
|
|
307
323
|
const remove = useDelete();
|
|
308
324
|
const rows = (state.list.result.data || []) as unknown as ResourceRecord[];
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
+
);
|
|
336
429
|
const handleExport = async () => {
|
|
337
430
|
setExporting(true);
|
|
338
431
|
try {
|
|
@@ -344,12 +437,34 @@ function GeneratedDesktopList({ definition }: { definition: GeneratedDefinition
|
|
|
344
437
|
setExporting(false);
|
|
345
438
|
}
|
|
346
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
|
+
};
|
|
347
464
|
return (
|
|
348
465
|
<ResourceListPage
|
|
349
466
|
createCapability={capabilities.create}
|
|
350
467
|
createPath={`/${code}/new`}
|
|
351
|
-
description={`由 ${code} 资源 Surface 自动生成的标准 CRUD 页面`}
|
|
352
|
-
onExport={() => void handleExport()}
|
|
353
468
|
readCapability={capabilities.read}
|
|
354
469
|
resource={code}
|
|
355
470
|
surface={surface}
|
|
@@ -359,21 +474,144 @@ function GeneratedDesktopList({ definition }: { definition: GeneratedDefinition
|
|
|
359
474
|
<Alert
|
|
360
475
|
action={<Button onClick={() => void state.list.query.refetch()} size="small">重试</Button>}
|
|
361
476
|
description={state.list.query.error instanceof Error ? state.list.query.error.message : '请稍后重试或联系平台管理员'}
|
|
362
|
-
message="数据读取失败"
|
|
363
477
|
showIcon
|
|
478
|
+
title="数据读取失败"
|
|
364
479
|
type="error"
|
|
365
480
|
/>
|
|
366
481
|
)}
|
|
367
|
-
<
|
|
368
|
-
<
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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>
|
|
377
615
|
</Space>
|
|
378
616
|
</div>
|
|
379
617
|
<Table<ResourceRecord>
|
|
@@ -382,6 +620,8 @@ function GeneratedDesktopList({ definition }: { definition: GeneratedDefinition
|
|
|
382
620
|
loading={state.list.query.isFetching}
|
|
383
621
|
locale={{ emptyText: <Empty description={`暂无${name}`} /> }}
|
|
384
622
|
rowKey="id"
|
|
623
|
+
scroll={{ x: 'max-content' }}
|
|
624
|
+
size={density}
|
|
385
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' }); }}
|
|
386
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); } }}
|
|
387
627
|
/>
|
|
@@ -415,8 +655,8 @@ function GeneratedMobileList({ definition }: { definition: GeneratedDefinition }
|
|
|
415
655
|
<Alert
|
|
416
656
|
action={<Button onClick={() => void state.list.query.refetch()} size="small">重试</Button>}
|
|
417
657
|
description={state.list.query.error instanceof Error ? state.list.query.error.message : '请稍后重试或联系平台管理员'}
|
|
418
|
-
message="数据读取失败"
|
|
419
658
|
showIcon
|
|
659
|
+
title="数据读取失败"
|
|
420
660
|
type="error"
|
|
421
661
|
/>
|
|
422
662
|
)}
|
|
@@ -460,7 +700,7 @@ function GeneratedResourceFormPage({ definition, mode, variant }: { definition:
|
|
|
460
700
|
else { const result = await update.mutateAsync({ resource: code, id, values: next, meta: { expectedRevision: record?.revision } }); message.success('修改已保存'); navigate(`/${code}/${result.data.id}`); }
|
|
461
701
|
};
|
|
462
702
|
return (
|
|
463
|
-
<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}>
|
|
464
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>)}>
|
|
465
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>
|
|
466
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>
|
|
@@ -477,14 +717,19 @@ function GeneratedResourceDetailPage({ definition, variant }: { definition: Gene
|
|
|
477
717
|
const [auditEntries, setAuditEntries] = useState<DataAuditEntry[]>([]);
|
|
478
718
|
const [auditLoading, setAuditLoading] = useState(false);
|
|
479
719
|
const [auditError, setAuditError] = useState('');
|
|
720
|
+
const [auditExpanded, setAuditExpanded] = useState(false);
|
|
721
|
+
const [auditLoadedFor, setAuditLoadedFor] = useState('');
|
|
480
722
|
useEffect(() => {
|
|
481
|
-
if (!record) return;
|
|
723
|
+
if (!record || !auditExpanded || auditLoadedFor === record.id) return;
|
|
482
724
|
let active = true;
|
|
483
725
|
setAuditLoading(true);
|
|
484
726
|
setAuditError('');
|
|
485
727
|
void createNativeResourceClient(definition.code, definition.surface).audit(record.id)
|
|
486
728
|
.then(page => {
|
|
487
|
-
if (active)
|
|
729
|
+
if (active) {
|
|
730
|
+
setAuditEntries(page.items || []);
|
|
731
|
+
setAuditLoadedFor(record.id);
|
|
732
|
+
}
|
|
488
733
|
})
|
|
489
734
|
.catch(reason => {
|
|
490
735
|
if (!active) return;
|
|
@@ -497,9 +742,124 @@ function GeneratedResourceDetailPage({ definition, variant }: { definition: Gene
|
|
|
497
742
|
return () => {
|
|
498
743
|
active = false;
|
|
499
744
|
};
|
|
500
|
-
}, [definition.code, definition.surface, record
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
+
);
|
|
505
865
|
}
|