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.
@@ -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, identity } = useRuntime();
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
- const columns = useMemo<TableColumnsType<ResourceRecord>>(() => [
310
- ...fields.map(field => ({
311
- title: field.label,
312
- dataIndex: field.key,
313
- key: field.key,
314
- sorter: Boolean(field.sortable),
315
- render: (value: unknown, record: ResourceRecord) => (
316
- <SurfaceFieldValue field={field} value={value} renderers={{ renderValue: ({ value: current }) => field.key === fields[0]?.key ? <a onClick={() => navigate(`/${code}/${record.id}`)}>{String(current || '-')}</a> : undefined }} />
317
- ),
318
- })),
319
- {
320
- title: '更新时间',
321
- dataIndex: 'updated_at',
322
- render: value => value ? new Date(String(value)).toLocaleString('zh-CN') : '-',
323
- },
324
- {
325
- title: '操作',
326
- key: 'actions',
327
- render: (_: unknown, record: ResourceRecord) => (
328
- <Space>
329
- <Button aria-label="查看" icon={<EyeOutlined />} onClick={() => navigate(`/${code}/${record.id}`)} size="small" />
330
- {hasCapability(capabilities.update) && <Button aria-label="编辑" icon={<EditOutlined />} onClick={() => navigate(`/${code}/${record.id}/edit`)} size="small" />}
331
- {hasCapability(capabilities.delete) && <Popconfirm title={`确定删除${name}?`} onConfirm={() => remove.mutate({ resource: code, id: record.id, meta: { expectedRevision: record.revision } }, { onSuccess: () => { message.success('已删除'); void state.list.query.refetch(); } })}><Button aria-label="删除" danger icon={<DeleteOutlined />} size="small" /></Popconfirm>}
332
- </Space>
333
- ),
334
- },
335
- ], [capabilities.delete, capabilities.update, code, fields, hasCapability, message, name, navigate, remove, state.list.query]);
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
- <div className="oxa-filter-grid">
368
- <Select allowClear options={(surface.list?.searchableFields || []).map(key => ({ label: fieldFor(surface, key).label, value: key }))} placeholder="搜索字段" value={state.draft.searchField} onChange={value => state.setDraft(item => ({ ...item, searchField: value }))} />
369
- <Input allowClear prefix={<SearchOutlined />} placeholder="关键词" value={state.draft.keyword} onChange={event => state.setDraft(item => ({ ...item, keyword: event.target.value }))} />
370
- {(surface.list?.filterFields || []).map(key => {
371
- const field = fieldFor(surface, key);
372
- return <SurfaceFilterControl key={key} field={field} value={state.draft.filters?.[key]} onChange={value => state.setDraft(item => ({ ...item, filters: { ...item.filters, [key]: value } }))} />;
373
- })}
374
- <Space>
375
- <Button loading={state.list.query.isFetching} type="primary" icon={<SearchOutlined />} onClick={() => { state.setPage(1); state.setQuery(state.draft); }}>查询</Button>
376
- <Button onClick={() => { state.setDraft({}); state.setQuery({}); state.setPage(1); }}>重置</Button>
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} recordSummary={record ? `${name} · 当前版本 ${record.revision}` : undefined} resource={code} surface={surface} title={name}>
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) setAuditEntries(page.items || []);
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?.id]);
501
- const content = record ? <div className="oxa-detail-layout"> <div className="oxa-detail-main">{fieldsBySection(definition.surface).map(group => <Card className="oxa-section-card" key={group.section} title={group.section === 'default' ? '基本信息' : group.section}><Descriptions column={{ xs: 1, sm: 2, lg: 3 }} items={group.fields.map(field => ({ key: field.key, label: field.label, children: <SurfaceFieldValue field={field} value={record[field.key]} /> }))} /></Card>)}</div><aside className="oxa-detail-aside"><Card className="oxa-section-card" id={`${definition.code}-audit`} loading={auditLoading} title="最近变更">{auditError ? <Alert message="变更记录读取失败" description={auditError} type="error" showIcon /> : auditEntries.length ? auditEntries.slice(0, 5).map(entry => <AuditEntry entry={entry} key={entry.id} surface={definition.surface} />) : <Typography.Text type="secondary">暂无变更记录</Typography.Text>}</Card></aside></div> : null;
502
- const hero = record ? <div><Typography.Title level={variant === 'mobile' ? 3 : 2}>{definition.name}详情</Typography.Title><Typography.Text type="secondary">{record.id} · 版本 {record.revision}</Typography.Text></div> : null;
503
- const props = { backLabel: `返回${definition.name}`, backPath: `/${definition.code}`, editCapability: definition.capabilities.update, editPath: `/${definition.code}/${id}/edit`, error: query.query.isError || (!query.query.isFetching && !record) ? query.query.error?.message || '记录不存在' : undefined, hero, loading: query.query.isFetching, meta: record ? <><span>版本 {record.revision}</span><span>最近更新 {record.updated_at ? new Date(record.updated_at).toLocaleString('zh-CN') : '-'}</span></> : null, readCapability: definition.capabilities.read, resource: definition.code, surface: definition.surface, title: definition.name };
504
- return variant === 'mobile' ? <MobileResourceDetailPage {...props} auditTargetId={`${definition.code}-audit`}>{content}</MobileResourceDetailPage> : <ResourceDetailPage {...props} auditTargetId={`${definition.code}-audit`}>{content}</ResourceDetailPage>;
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
  }