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.
@@ -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 defaultSort = surface.list?.defaultSort || {
251
- field: Object.keys(surface.fields)[0] || 'id',
252
- order: 'asc' as const,
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(surface.list?.defaultPageSize || 20);
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: defaultSort.field, order: defaultSort.order || 'asc' as const });
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
- if (mode === 'list') return <GeneratedResourceListPage definition={definition} variant={variant} />;
282
- if (mode === 'detail') return <GeneratedResourceDetailPage definition={definition} variant={variant} />;
283
- return <GeneratedResourceFormPage definition={definition} mode={mode} variant={variant} />;
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, identity } = useRuntime();
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
- const columns = useMemo<TableColumnsType<ResourceRecord>>(() => [
303
- ...fields.map(field => ({
304
- title: field.label,
305
- dataIndex: field.key,
306
- key: field.key,
307
- sorter: Boolean(field.sortable),
308
- render: (value: unknown, record: ResourceRecord) => (
309
- <SurfaceFieldValue field={field} value={value} renderers={{ renderValue: ({ value: current }) => field.key === fields[0]?.key ? <a onClick={() => navigate(`/${code}/${record.id}`)}>{String(current || '-')}</a> : undefined }} />
310
- ),
311
- })),
312
- {
313
- title: '更新时间',
314
- dataIndex: 'updated_at',
315
- render: value => value ? new Date(String(value)).toLocaleString('zh-CN') : '-',
316
- },
317
- {
318
- title: '操作',
319
- key: 'actions',
320
- render: (_: unknown, record: ResourceRecord) => (
321
- <Space>
322
- <Button aria-label="查看" icon={<EyeOutlined />} onClick={() => navigate(`/${code}/${record.id}`)} size="small" />
323
- {hasCapability(capabilities.update) && <Button aria-label="编辑" icon={<EditOutlined />} onClick={() => navigate(`/${code}/${record.id}/edit`)} size="small" />}
324
- {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>}
325
- </Space>
326
- ),
327
- },
328
- ], [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
+ );
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
- <div className="oxa-filter-grid">
361
- <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 }))} />
362
- <Input allowClear prefix={<SearchOutlined />} placeholder="关键词" value={state.draft.keyword} onChange={event => state.setDraft(item => ({ ...item, keyword: event.target.value }))} />
363
- {(surface.list?.filterFields || []).map(key => {
364
- const field = fieldFor(surface, key);
365
- return <SurfaceFilterControl key={key} field={field} value={state.draft.filters?.[key]} onChange={value => state.setDraft(item => ({ ...item, filters: { ...item.filters, [key]: value } }))} />;
366
- })}
367
- <Space>
368
- <Button loading={state.list.query.isFetching} type="primary" icon={<SearchOutlined />} onClick={() => { state.setPage(1); state.setQuery(state.draft); }}>查询</Button>
369
- <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>
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} 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}>
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) setAuditEntries(page.items || []);
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?.id]);
494
- 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;
495
- 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;
496
- 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 };
497
- 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
+ );
498
865
  }