openxiangda-cli 2.0.0-alpha.68 → 2.0.0-alpha.70

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.
@@ -5,7 +5,6 @@ import {
5
5
  EditOutlined,
6
6
  EyeOutlined,
7
7
  ExportOutlined,
8
- ImportOutlined,
9
8
  PlusOutlined,
10
9
  ReloadOutlined,
11
10
  SearchOutlined,
@@ -60,6 +59,7 @@ import {
60
59
  import { createNativeResourceClient, type GenericResourceQuery } from '../../platform-client';
61
60
  import { ResolvedValueText } from '../../AuthoritativeSelector';
62
61
  import { useRuntime } from '../../runtime';
62
+ import { ResourceBatchActions, ResourceImportButton } from './ResourceBatchActions';
63
63
 
64
64
  type ResourceRecord = Record<string, unknown> & {
65
65
  id: string;
@@ -70,7 +70,12 @@ type ResourceRecord = Record<string, unknown> & {
70
70
  type GeneratedDefinition = {
71
71
  code: string;
72
72
  name: string;
73
- capabilities: { read: string; create: string; update: string; delete: string };
73
+ capabilities: {
74
+ read: string;
75
+ create: string;
76
+ update: string;
77
+ delete: string;
78
+ };
74
79
  surface: DataResourceSurface;
75
80
  };
76
81
 
@@ -92,16 +97,17 @@ function fieldsBySection(surface: DataResourceSurface) {
92
97
  const section = field.section || 'default';
93
98
  groups.set(section, [...(groups.get(section) || []), { key, ...field }]);
94
99
  }
95
- return [...groups.entries()].map(([section, fields]) => ({ section, fields }));
100
+ return [...groups.entries()].map(([section, fields]) => ({
101
+ section,
102
+ fields,
103
+ }));
96
104
  }
97
105
 
98
106
  function normalizeRecordForForm(record: ResourceRecord, surface: DataResourceSurface) {
99
107
  return Object.fromEntries(
100
108
  Object.entries(record).map(([key, value]) => {
101
109
  const widget = surface.fields[key]?.widget;
102
- return widget === 'date' || widget === 'datetime'
103
- ? [key, value ? dayjs(String(value)) : undefined]
104
- : [key, value];
110
+ return widget === 'date' || widget === 'datetime' ? [key, value ? dayjs(String(value)) : undefined] : [key, value];
105
111
  })
106
112
  );
107
113
  }
@@ -111,22 +117,20 @@ function normalizeFormValues(values: Record<string, unknown>, surface: DataResou
111
117
  Object.entries(values).map(([key, value]) => {
112
118
  const widget = surface.fields[key]?.widget;
113
119
  return widget === 'date' || widget === 'datetime'
114
- ? [key, value && typeof value === 'object' && 'format' in value
115
- ? (widget === 'datetime'
116
- ? dayjs(value as never).toISOString()
117
- : (value as { format: (pattern: string) => string }).format('YYYY-MM-DD'))
118
- : value]
120
+ ? [
121
+ key,
122
+ value && typeof value === 'object' && 'format' in value
123
+ ? widget === 'datetime'
124
+ ? dayjs(value as never).toISOString()
125
+ : (value as { format: (pattern: string) => string }).format('YYYY-MM-DD')
126
+ : value,
127
+ ]
119
128
  : [key, value];
120
129
  })
121
130
  );
122
131
  }
123
132
 
124
- function fieldWritable(
125
- field: SurfaceField,
126
- mode: 'create' | 'edit',
127
- hasCapability: (code: string) => boolean,
128
- isAppSuperAdmin: boolean
129
- ) {
133
+ function fieldWritable(field: SurfaceField, mode: 'create' | 'edit', hasCapability: (code: string) => boolean, isAppSuperAdmin: boolean) {
130
134
  const capability = mode === 'create' ? field.createCapability : field.updateCapability;
131
135
  if (isAppSuperAdmin) return true;
132
136
  if (capability) return hasCapability(capability);
@@ -134,11 +138,7 @@ function fieldWritable(
134
138
  }
135
139
 
136
140
  function auditOperationLabel(operation: DataAuditEntry['operation']) {
137
- return operation === 'created'
138
- ? '创建'
139
- : operation === 'deleted'
140
- ? '删除'
141
- : '更新';
141
+ return operation === 'created' ? '创建' : operation === 'deleted' ? '删除' : '更新';
142
142
  }
143
143
 
144
144
  function auditValue(field: SurfaceField, value: unknown) {
@@ -149,19 +149,19 @@ function auditValue(field: SurfaceField, value: unknown) {
149
149
  }
150
150
 
151
151
  function auditChangeFields(entry: DataAuditEntry, surface: DataResourceSurface) {
152
- const fields = fieldsBySection(surface).flatMap(group => group.fields);
152
+ const fields = fieldsBySection(surface).flatMap((group) => group.fields);
153
153
  const source = entry.changes || entry.record || entry.before || {};
154
- return fields.filter(field => Object.prototype.hasOwnProperty.call(source, field.key));
154
+ return fields.filter((field) => Object.prototype.hasOwnProperty.call(source, field.key));
155
155
  }
156
156
 
157
157
  function exportCellValue(field: SurfaceField, value: unknown) {
158
158
  if (value === undefined || value === null || value === '') return '';
159
159
  if (field.widget === 'boolean') return value ? '是' : '否';
160
160
  if (field.options && !Array.isArray(value)) {
161
- return field.options.find(option => option.value === value)?.label || String(value);
161
+ return field.options.find((option) => option.value === value)?.label || String(value);
162
162
  }
163
163
  if (Array.isArray(value)) {
164
- return value.map(item => field.options?.find(option => option.value === item)?.label || String(item)).join('、');
164
+ return value.map((item) => field.options?.find((option) => option.value === item)?.label || String(item)).join('、');
165
165
  }
166
166
  if (field.widget === 'date' || field.widget === 'datetime') {
167
167
  const parsed = new Date(String(value));
@@ -180,7 +180,7 @@ async function exportResourceRows(
180
180
  name: string,
181
181
  surface: DataResourceSurface,
182
182
  query: Partial<GenericResourceQuery>,
183
- sort: { field: string; order: 'asc' | 'desc' },
183
+ sort: { field: string; order: 'asc' | 'desc' }
184
184
  ) {
185
185
  const client = createNativeResourceClient(code, surface);
186
186
  const fields = listSurfaceFields(surface.fields);
@@ -201,9 +201,11 @@ async function exportResourceRows(
201
201
  total = result.total;
202
202
  page += 1;
203
203
  } while (rows.length < total && rows.length < 10000 && page <= 20);
204
- const header = fields.map(field => csvCell(field.label)).join(',');
205
- const body = rows.map(row => fields.map(field => csvCell(exportCellValue(field, row[field.key]))).join(','));
206
- const blob = new Blob([`\uFEFF${[header, ...body].join('\r\n')}`], { type: 'text/csv;charset=utf-8' });
204
+ const header = fields.map((field) => csvCell(field.label)).join(',');
205
+ const body = rows.map((row) => fields.map((field) => csvCell(exportCellValue(field, row[field.key]))).join(','));
206
+ const blob = new Blob([`\uFEFF${[header, ...body].join('\r\n')}`], {
207
+ type: 'text/csv;charset=utf-8',
208
+ });
207
209
  const url = URL.createObjectURL(blob);
208
210
  const anchor = document.createElement('a');
209
211
  anchor.href = url;
@@ -225,7 +227,10 @@ function AuditEntry({ entry, surface }: { entry: DataAuditEntry; surface: DataRe
225
227
  <div className="oxa-audit-meta">
226
228
  <span>操作人</span>
227
229
  {entry.actor.principalType === 'application' ? (
228
- <Typography.Text>应用服务{entry.actor.oauthClientId ? `(${entry.actor.oauthClientId})` : ''}</Typography.Text>
230
+ <Typography.Text>
231
+ 应用服务
232
+ {entry.actor.oauthClientId ? `(${entry.actor.oauthClientId})` : ''}
233
+ </Typography.Text>
229
234
  ) : (
230
235
  <ResolvedValueText source="user" value={entry.actor.userId} />
231
236
  )}
@@ -234,7 +239,7 @@ function AuditEntry({ entry, surface }: { entry: DataAuditEntry; surface: DataRe
234
239
  </div>
235
240
  {fields.length ? (
236
241
  <div className="oxa-audit-changes">
237
- {fields.map(field => {
242
+ {fields.map((field) => {
238
243
  const before = entry.operation === 'created' ? undefined : entry.before?.[field.key];
239
244
  const after = entry.operation === 'deleted' ? undefined : changes[field.key];
240
245
  return (
@@ -265,13 +270,19 @@ function useGeneratedList(definition: GeneratedDefinition) {
265
270
  const [pageSize, setPageSize] = useState(defaultPageSize);
266
271
  const [draft, setDraft] = useState<Partial<GenericResourceQuery>>({});
267
272
  const [query, setQuery] = useState<Partial<GenericResourceQuery>>({});
268
- const [sort, setSort] = useState({ field: defaultSortField, order: defaultSortOrder as 'asc' | 'desc' });
273
+ const [sort, setSort] = useState({
274
+ field: defaultSortField,
275
+ order: defaultSortOrder as 'asc' | 'desc',
276
+ });
269
277
  useEffect(() => {
270
278
  setPage(1);
271
279
  setPageSize(defaultPageSize);
272
280
  setDraft({});
273
281
  setQuery({});
274
- setSort({ field: defaultSortField, order: defaultSortOrder as 'asc' | 'desc' });
282
+ setSort({
283
+ field: defaultSortField,
284
+ order: defaultSortOrder as 'asc' | 'desc',
285
+ });
275
286
  }, [code, defaultPageSize, defaultSortField, defaultSortOrder]);
276
287
  const list = useList<ResourceRecord>({
277
288
  resource: code,
@@ -279,7 +290,19 @@ function useGeneratedList(definition: GeneratedDefinition) {
279
290
  sorters: [sort],
280
291
  meta: { query },
281
292
  });
282
- return { draft, list, page, pageSize, query, setDraft, setPage, setPageSize, setQuery, setSort, sort };
293
+ return {
294
+ draft,
295
+ list,
296
+ page,
297
+ pageSize,
298
+ query,
299
+ setDraft,
300
+ setPage,
301
+ setPageSize,
302
+ setQuery,
303
+ setSort,
304
+ sort,
305
+ };
283
306
  }
284
307
 
285
308
  export function GeneratedResourcePage({
@@ -302,36 +325,46 @@ export function GeneratedResourcePage({
302
325
  }
303
326
 
304
327
  function GeneratedResourceListPage({ definition, variant }: { definition: GeneratedDefinition; variant: 'desktop' | 'mobile' }) {
305
- return variant === 'mobile'
306
- ? <GeneratedMobileList definition={definition} />
307
- : <GeneratedDesktopList definition={definition} />;
328
+ return variant === 'mobile' ? <GeneratedMobileList definition={definition} /> : <GeneratedDesktopList definition={definition} />;
308
329
  }
309
330
 
310
331
  function GeneratedDesktopList({ definition }: { definition: GeneratedDefinition }) {
311
332
  const navigate = useNavigate();
312
333
  const { message } = App.useApp();
313
- const { hasCapability } = useRuntime();
334
+ const { hasCapability, identity } = useRuntime();
314
335
  const state = useGeneratedList(definition);
315
336
  const { code, name, surface, capabilities } = definition;
316
337
  const fields = listSurfaceFields(surface.fields);
317
338
  const [exporting, setExporting] = useState(false);
318
339
  const [filtersExpanded, setFiltersExpanded] = useState(false);
319
340
  const [density, setDensity] = useState<'small' | 'middle' | 'large'>('middle');
320
- const [visibleColumnKeys, setVisibleColumnKeys] = useState<string[]>(() =>
321
- fields.map(field => field.key),
322
- );
341
+ const [visibleColumnKeys, setVisibleColumnKeys] = useState<string[]>(() => fields.map((field) => field.key));
342
+ const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
323
343
  const remove = useDelete();
324
344
  const rows = (state.list.result.data || []) as unknown as ResourceRecord[];
325
345
  useEffect(() => {
326
- setVisibleColumnKeys(fields.map(field => field.key));
346
+ setVisibleColumnKeys(fields.map((field) => field.key));
327
347
  setFiltersExpanded(false);
328
348
  setDensity('middle');
349
+ setSelectedRowKeys([]);
329
350
  }, [code]);
351
+ useEffect(() => {
352
+ setSelectedRowKeys([]);
353
+ }, [code, state.page, state.pageSize, state.query, state.sort.field, state.sort.order]);
354
+ const formFields = fieldsBySection(surface).flatMap((group) => group.fields);
355
+ const importWritableFieldCodes = formFields
356
+ .filter((field) => field.widget !== 'readonly' && fieldWritable(field, 'create', hasCapability, identity.isAppSuperAdmin))
357
+ .map((field) => field.key);
358
+ const batchWritableFields = formFields.filter(
359
+ (field) => !['file', 'readonly'].includes(field.widget || '') && fieldWritable(field, 'edit', hasCapability, identity.isAppSuperAdmin)
360
+ );
361
+ const selectedRows = rows.filter((row) => selectedRowKeys.includes(row.id));
362
+ const canBatch = hasCapability(capabilities.update) || hasCapability(capabilities.delete);
330
363
  const columns = useMemo<TableColumnsType<ResourceRecord>>(
331
364
  () => [
332
365
  ...fields
333
- .filter(field => visibleColumnKeys.includes(field.key))
334
- .map(field => ({
366
+ .filter((field) => visibleColumnKeys.includes(field.key))
367
+ .map((field) => ({
335
368
  title: field.label,
336
369
  dataIndex: field.key,
337
370
  key: field.key,
@@ -343,9 +376,7 @@ function GeneratedDesktopList({ definition }: { definition: GeneratedDefinition
343
376
  renderers={{
344
377
  renderValue: ({ value: current }) =>
345
378
  field.key === fields[0]?.key ? (
346
- <a onClick={() => navigate(`/${code}/${record.id}`)}>
347
- {String(current || '-')}
348
- </a>
379
+ <a onClick={() => navigate(`/${code}/${record.id}`)}>{String(current || '-')}</a>
349
380
  ) : undefined,
350
381
  }}
351
382
  />
@@ -355,8 +386,7 @@ function GeneratedDesktopList({ definition }: { definition: GeneratedDefinition
355
386
  title: '更新时间',
356
387
  dataIndex: 'updated_at',
357
388
  key: 'updated_at',
358
- render: value =>
359
- value ? new Date(String(value)).toLocaleString('zh-CN') : '-',
389
+ render: (value) => (value ? new Date(String(value)).toLocaleString('zh-CN') : '-'),
360
390
  },
361
391
  {
362
392
  title: '操作',
@@ -364,13 +394,7 @@ function GeneratedDesktopList({ definition }: { definition: GeneratedDefinition
364
394
  fixed: 'right' as const,
365
395
  render: (_: unknown, record: ResourceRecord) => (
366
396
  <Space size={4}>
367
- <Button
368
- aria-label="查看"
369
- icon={<EyeOutlined />}
370
- onClick={() => navigate(`/${code}/${record.id}`)}
371
- size="small"
372
- type="text"
373
- />
397
+ <Button aria-label="查看" icon={<EyeOutlined />} onClick={() => navigate(`/${code}/${record.id}`)} size="small" type="text" />
374
398
  {hasCapability(capabilities.update) && (
375
399
  <Button
376
400
  aria-label="编辑"
@@ -394,18 +418,12 @@ function GeneratedDesktopList({ definition }: { definition: GeneratedDefinition
394
418
  message.success('已删除');
395
419
  void state.list.query.refetch();
396
420
  },
397
- },
421
+ }
398
422
  )
399
423
  }
400
424
  title={`确定删除${name}?`}
401
425
  >
402
- <Button
403
- aria-label="删除"
404
- danger
405
- icon={<DeleteOutlined />}
406
- size="small"
407
- type="text"
408
- />
426
+ <Button aria-label="删除" danger icon={<DeleteOutlined />} size="small" type="text" />
409
427
  </Popconfirm>
410
428
  )}
411
429
  </Space>
@@ -424,7 +442,7 @@ function GeneratedDesktopList({ definition }: { definition: GeneratedDefinition
424
442
  remove,
425
443
  state.list.query,
426
444
  visibleColumnKeys,
427
- ],
445
+ ]
428
446
  );
429
447
  const handleExport = async () => {
430
448
  setExporting(true);
@@ -440,12 +458,11 @@ function GeneratedDesktopList({ definition }: { definition: GeneratedDefinition
440
458
  const filterFields = surface.list?.filterFields || [];
441
459
  const hasMoreFilters = filterFields.length > 2;
442
460
  const columnMenu: MenuProps = {
443
- items: fields.map(field => ({ key: field.key, label: field.label })),
461
+ items: fields.map((field) => ({ key: field.key, label: field.label })),
444
462
  multiple: true,
445
463
  selectable: true,
446
464
  selectedKeys: visibleColumnKeys,
447
- onSelect: ({ selectedKeys }) =>
448
- setVisibleColumnKeys(selectedKeys.map(String)),
465
+ onSelect: ({ selectedKeys }) => setVisibleColumnKeys(selectedKeys.map(String)),
449
466
  onDeselect: ({ selectedKeys }) => {
450
467
  if (selectedKeys.length) setVisibleColumnKeys(selectedKeys.map(String));
451
468
  else message.info('至少保留一列');
@@ -472,7 +489,11 @@ function GeneratedDesktopList({ definition }: { definition: GeneratedDefinition
472
489
  >
473
490
  {state.list.query.isError && (
474
491
  <Alert
475
- action={<Button onClick={() => void state.list.query.refetch()} size="small">重试</Button>}
492
+ action={
493
+ <Button onClick={() => void state.list.query.refetch()} size="small">
494
+ 重试
495
+ </Button>
496
+ }
476
497
  description={state.list.query.error instanceof Error ? state.list.query.error.message : '请稍后重试或联系平台管理员'}
477
498
  showIcon
478
499
  title="数据读取失败"
@@ -484,22 +505,20 @@ function GeneratedDesktopList({ definition }: { definition: GeneratedDefinition
484
505
  <div className="oxa-filter-item">
485
506
  <Select
486
507
  allowClear
487
- options={(surface.list?.searchableFields || []).map(key => ({
508
+ options={(surface.list?.searchableFields || []).map((key) => ({
488
509
  label: fieldFor(surface, key).label,
489
510
  value: key,
490
511
  }))}
491
512
  placeholder="搜索字段"
492
513
  value={state.draft.searchField}
493
- onChange={value =>
494
- state.setDraft(item => ({ ...item, searchField: value }))
495
- }
514
+ onChange={(value) => state.setDraft((item) => ({ ...item, searchField: value }))}
496
515
  />
497
516
  </div>
498
517
  <div className="oxa-filter-item">
499
518
  <Input
500
519
  allowClear
501
- onChange={event =>
502
- state.setDraft(item => ({
520
+ onChange={(event) =>
521
+ state.setDraft((item) => ({
503
522
  ...item,
504
523
  keyword: event.target.value,
505
524
  }))
@@ -516,14 +535,11 @@ function GeneratedDesktopList({ definition }: { definition: GeneratedDefinition
516
535
  {filterFields.map((key, index) => {
517
536
  const field = fieldFor(surface, key);
518
537
  return (
519
- <div
520
- className={`oxa-filter-item${index >= 2 && !filtersExpanded ? ' is-collapsed' : ''}`}
521
- key={key}
522
- >
538
+ <div className={`oxa-filter-item${index >= 2 && !filtersExpanded ? ' is-collapsed' : ''}`} key={key}>
523
539
  <SurfaceFilterControl
524
540
  field={field}
525
- onChange={value =>
526
- state.setDraft(item => ({
541
+ onChange={(value) =>
542
+ state.setDraft((item) => ({
527
543
  ...item,
528
544
  filters: { ...item.filters, [key]: value },
529
545
  }))
@@ -558,7 +574,7 @@ function GeneratedDesktopList({ definition }: { definition: GeneratedDefinition
558
574
  {hasMoreFilters && (
559
575
  <Button
560
576
  icon={filtersExpanded ? <UpOutlined /> : <DownOutlined />}
561
- onClick={() => setFiltersExpanded(value => !value)}
577
+ onClick={() => setFiltersExpanded((value) => !value)}
562
578
  type="link"
563
579
  >
564
580
  {filtersExpanded ? '收起筛选' : `更多筛选(${filterFields.length - 2})`}
@@ -569,26 +585,20 @@ function GeneratedDesktopList({ definition }: { definition: GeneratedDefinition
569
585
  <div className="oxa-list-toolbar">
570
586
  <Space className="oxa-list-toolbar-primary" wrap>
571
587
  {hasCapability(capabilities.create) && (
572
- <Button
573
- icon={<PlusOutlined />}
574
- onClick={() => navigate(`/${code}/new`)}
575
- type="primary"
576
- >
588
+ <Button icon={<PlusOutlined />} onClick={() => navigate(`/${code}/new`)} type="primary">
577
589
  新增{name}
578
590
  </Button>
579
591
  )}
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
+ {hasCapability(capabilities.create) && (
593
+ <ResourceImportButton
594
+ code={code}
595
+ name={name}
596
+ surface={surface}
597
+ writableFieldCodes={importWritableFieldCodes}
598
+ onCompleted={() => state.list.query.refetch()}
599
+ />
600
+ )}
601
+ <Button icon={<ExportOutlined />} loading={exporting} onClick={() => void handleExport()}>
592
602
  导出
593
603
  </Button>
594
604
  </Space>
@@ -614,16 +624,54 @@ function GeneratedDesktopList({ definition }: { definition: GeneratedDefinition
614
624
  </Dropdown>
615
625
  </Space>
616
626
  </div>
627
+ <ResourceBatchActions
628
+ canDelete={hasCapability(capabilities.delete)}
629
+ code={code}
630
+ name={name}
631
+ onClear={() => setSelectedRowKeys([])}
632
+ onCompleted={() => state.list.query.refetch()}
633
+ rows={selectedRows}
634
+ surface={surface}
635
+ writableFields={batchWritableFields}
636
+ />
617
637
  <Table<ResourceRecord>
618
638
  columns={columns}
619
639
  dataSource={rows}
620
640
  loading={state.list.query.isFetching}
621
641
  locale={{ emptyText: <Empty description={`暂无${name}`} /> }}
622
642
  rowKey="id"
643
+ rowSelection={
644
+ canBatch
645
+ ? {
646
+ selectedRowKeys,
647
+ onChange: (keys) => {
648
+ const next = keys.map(String);
649
+ if (next.length > 100) message.warning('单次最多选择 100 条记录');
650
+ setSelectedRowKeys(next.slice(0, 100));
651
+ },
652
+ }
653
+ : undefined
654
+ }
623
655
  scroll={{ x: 'max-content' }}
624
656
  size={density}
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' }); }}
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); } }}
657
+ onChange={(_, __, sorter) => {
658
+ const item = Array.isArray(sorter) ? sorter[0] : sorter;
659
+ if (item?.field && item.order)
660
+ state.setSort({
661
+ field: String(item.field),
662
+ order: item.order === 'ascend' ? 'asc' : 'desc',
663
+ });
664
+ }}
665
+ pagination={{
666
+ current: state.page,
667
+ pageSize: state.pageSize,
668
+ total: state.list.result.total || 0,
669
+ showSizeChanger: true,
670
+ onChange: (next, size) => {
671
+ state.setPage(next);
672
+ state.setPageSize(size);
673
+ },
674
+ }}
627
675
  />
628
676
  </ResourceListPage>
629
677
  );
@@ -650,10 +698,23 @@ function GeneratedMobileList({ definition }: { definition: GeneratedDefinition }
650
698
  }
651
699
  };
652
700
  return (
653
- <MobileResourceListPage createCapability={capabilities.create} createPath={`/${code}/new`} description={`${code} 资源`} onExport={() => void handleExport()} readCapability={capabilities.read} resource={code} surface={surface} title={name}>
701
+ <MobileResourceListPage
702
+ createCapability={capabilities.create}
703
+ createPath={`/${code}/new`}
704
+ description={`${code} 资源`}
705
+ onExport={() => void handleExport()}
706
+ readCapability={capabilities.read}
707
+ resource={code}
708
+ surface={surface}
709
+ title={name}
710
+ >
654
711
  {state.list.query.isError && (
655
712
  <Alert
656
- action={<Button onClick={() => void state.list.query.refetch()} size="small">重试</Button>}
713
+ action={
714
+ <Button onClick={() => void state.list.query.refetch()} size="small">
715
+ 重试
716
+ </Button>
717
+ }
657
718
  description={state.list.query.error instanceof Error ? state.list.query.error.message : '请稍后重试或联系平台管理员'}
658
719
  showIcon
659
720
  title="数据读取失败"
@@ -661,16 +722,89 @@ function GeneratedMobileList({ definition }: { definition: GeneratedDefinition }
661
722
  />
662
723
  )}
663
724
  <div className="oxa-mobile-filter">
664
- <Input allowClear placeholder="关键词" value={state.draft.keyword} onChange={event => state.setDraft(item => ({ ...item, keyword: event.target.value }))} onPressEnter={() => { state.setPage(1); state.setQuery(state.draft); }} />
665
- <Button loading={state.list.query.isFetching} icon={<SearchOutlined />} onClick={() => { state.setPage(1); state.setQuery(state.draft); }} type="primary">查询</Button>
725
+ <Input
726
+ allowClear
727
+ placeholder="关键词"
728
+ value={state.draft.keyword}
729
+ onChange={(event) => state.setDraft((item) => ({ ...item, keyword: event.target.value }))}
730
+ onPressEnter={() => {
731
+ state.setPage(1);
732
+ state.setQuery(state.draft);
733
+ }}
734
+ />
735
+ <Button
736
+ loading={state.list.query.isFetching}
737
+ icon={<SearchOutlined />}
738
+ onClick={() => {
739
+ state.setPage(1);
740
+ state.setQuery(state.draft);
741
+ }}
742
+ type="primary"
743
+ >
744
+ 查询
745
+ </Button>
746
+ </div>
747
+ {state.list.query.isFetching ? (
748
+ <div className="oxa-page-loading">
749
+ <Spin />
750
+ </div>
751
+ ) : rows.length ? (
752
+ <div className="oxa-mobile-record-list">
753
+ {rows.map((record) => (
754
+ <Card className="oxa-mobile-record-card" key={record.id} size="small">
755
+ <div className="oxa-mobile-record-heading">
756
+ <a onClick={() => navigate(`/${code}/${record.id}`)}>{String(record[fields[0]?.key || 'id'] || '-')}</a>
757
+ </div>
758
+ <div className="oxa-mobile-record-fields">
759
+ {fields.slice(1).map((field) => (
760
+ <div key={field.key}>
761
+ <Typography.Text type="secondary">{field.label}</Typography.Text>
762
+ <div>
763
+ <SurfaceFieldValue field={field} value={record[field.key]} />
764
+ </div>
765
+ </div>
766
+ ))}
767
+ </div>
768
+ <Space>
769
+ <Button onClick={() => navigate(`/${code}/${record.id}`)} size="small">
770
+ 查看
771
+ </Button>
772
+ {hasCapability(capabilities.update) && (
773
+ <Button onClick={() => navigate(`/${code}/${record.id}/edit`)} size="small">
774
+ 编辑
775
+ </Button>
776
+ )}
777
+ </Space>
778
+ </Card>
779
+ ))}
780
+ </div>
781
+ ) : (
782
+ <Card>
783
+ <Empty description={`暂无${name}`} />
784
+ </Card>
785
+ )}
786
+ <div className="oxa-mobile-pagination">
787
+ <Button disabled={state.page <= 1} onClick={() => state.setPage((item) => item - 1)}>
788
+ 上一页
789
+ </Button>
790
+ <Typography.Text>第 {state.page} 页</Typography.Text>
791
+ <Button disabled={rows.length < (surface.list?.defaultPageSize || 20)} onClick={() => state.setPage((item) => item + 1)}>
792
+ 下一页
793
+ </Button>
666
794
  </div>
667
- {state.list.query.isFetching ? <div className="oxa-page-loading"><Spin /></div> : rows.length ? <div className="oxa-mobile-record-list">{rows.map(record => <Card className="oxa-mobile-record-card" key={record.id} size="small"><div className="oxa-mobile-record-heading"><a onClick={() => navigate(`/${code}/${record.id}`)}>{String(record[fields[0]?.key || 'id'] || '-')}</a></div><div className="oxa-mobile-record-fields">{fields.slice(1).map(field => <div key={field.key}><Typography.Text type="secondary">{field.label}</Typography.Text><div><SurfaceFieldValue field={field} value={record[field.key]} /></div></div>)}</div><Space><Button onClick={() => navigate(`/${code}/${record.id}`)} size="small">查看</Button>{hasCapability(capabilities.update) && <Button onClick={() => navigate(`/${code}/${record.id}/edit`)} size="small">编辑</Button>}</Space></Card>)}</div> : <Card><Empty description={`暂无${name}`} /></Card>}
668
- <div className="oxa-mobile-pagination"><Button disabled={state.page <= 1} onClick={() => state.setPage(item => item - 1)}>上一页</Button><Typography.Text>第 {state.page} 页</Typography.Text><Button disabled={rows.length < (surface.list?.defaultPageSize || 20)} onClick={() => state.setPage(item => item + 1)}>下一页</Button></div>
669
795
  </MobileResourceListPage>
670
796
  );
671
797
  }
672
798
 
673
- function GeneratedResourceFormPage({ definition, mode, variant }: { definition: GeneratedDefinition; mode: 'create' | 'edit'; variant: 'desktop' | 'mobile' }) {
799
+ function GeneratedResourceFormPage({
800
+ definition,
801
+ mode,
802
+ variant,
803
+ }: {
804
+ definition: GeneratedDefinition;
805
+ mode: 'create' | 'edit';
806
+ variant: 'desktop' | 'mobile';
807
+ }) {
674
808
  const { id = '' } = useParams();
675
809
  const navigate = useNavigate();
676
810
  const { message } = App.useApp();
@@ -678,32 +812,97 @@ function GeneratedResourceFormPage({ definition, mode, variant }: { definition:
678
812
  const [form] = Form.useForm();
679
813
  const create = useCreate<ResourceRecord>();
680
814
  const update = useUpdate<ResourceRecord>();
681
- const query = useOne<ResourceRecord>({ resource: definition.code, id, queryOptions: { enabled: mode === 'edit' } });
815
+ const query = useOne<ResourceRecord>({
816
+ resource: definition.code,
817
+ id,
818
+ queryOptions: { enabled: mode === 'edit' },
819
+ });
682
820
  const record = query.result;
683
821
  const { code, name, surface, capabilities } = definition;
684
- useEffect(() => { if (record) form.setFieldsValue(normalizeRecordForForm(record, surface)); }, [form, record, surface]);
822
+ useEffect(() => {
823
+ if (record) form.setFieldsValue(normalizeRecordForForm(record, surface));
824
+ }, [form, record, surface]);
685
825
  const listPath = `/${code}`;
686
826
  const detailPath = `/${code}/${id}`;
687
827
  const Page = variant === 'mobile' ? MobileResourceFormPage : ResourceFormPage;
688
828
  const FieldControl = variant === 'mobile' ? MobileSurfaceFieldControl : SurfaceFieldControl;
689
- if (!hasCapability(mode === 'create' ? capabilities.create : capabilities.update)) return <Result status="403" title="当前平台用户无此操作权限" />;
829
+ if (!hasCapability(mode === 'create' ? capabilities.create : capabilities.update))
830
+ return <Result status="403" title="当前平台用户无此操作权限" />;
690
831
  const submit = async (values: Record<string, unknown>) => {
691
832
  const next = normalizeFormValues(
692
833
  Object.fromEntries(
693
834
  Object.entries(values).filter(([key]) =>
694
- fieldWritable(surface.fields[key] ? { key, ...surface.fields[key] } : { key, label: key, widget: 'text' }, mode, hasCapability, identity.isAppSuperAdmin)
835
+ fieldWritable(
836
+ surface.fields[key] ? { key, ...surface.fields[key] } : { key, label: key, widget: 'text' },
837
+ mode,
838
+ hasCapability,
839
+ identity.isAppSuperAdmin
840
+ )
695
841
  )
696
842
  ),
697
843
  surface
698
844
  );
699
- if (mode === 'create') { await create.mutateAsync({ resource: code, values: next }); message.success(`${name}已创建`); navigate(listPath); }
700
- else { const result = await update.mutateAsync({ resource: code, id, values: next, meta: { expectedRevision: record?.revision } }); message.success('修改已保存'); navigate(`/${code}/${result.data.id}`); }
845
+ if (mode === 'create') {
846
+ await create.mutateAsync({ resource: code, values: next });
847
+ message.success(`${name}已创建`);
848
+ navigate(listPath);
849
+ } else {
850
+ const result = await update.mutateAsync({
851
+ resource: code,
852
+ id,
853
+ values: next,
854
+ meta: { expectedRevision: record?.revision },
855
+ });
856
+ message.success('修改已保存');
857
+ navigate(`/${code}/${result.data.id}`);
858
+ }
701
859
  };
702
860
  return (
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}>
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>)}>
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>
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>
861
+ <Page
862
+ backLabel={mode === 'edit' ? `返回${name}详情` : `返回${name}`}
863
+ backPath={mode === 'edit' ? detailPath : listPath}
864
+ capability={mode === 'create' ? capabilities.create : capabilities.update}
865
+ detailPath={record ? detailPath : undefined}
866
+ loading={mode === 'edit' && query.query.isFetching}
867
+ mode={mode}
868
+ notFound={mode === 'edit' && !record}
869
+ resource={code}
870
+ surface={surface}
871
+ title={name}
872
+ >
873
+ <Form
874
+ className={variant === 'mobile' ? 'oxa-mobile-form' : 'oxa-form-full'}
875
+ form={form}
876
+ initialValues={undefined}
877
+ layout="vertical"
878
+ onFinish={(values) => void submit(values as Record<string, unknown>)}
879
+ >
880
+ <div className="oxa-sections">
881
+ {fieldsBySection(surface).map((group) => (
882
+ <Card className="oxa-section-card" key={group.section} title={group.section === 'default' ? '基本信息' : group.section}>
883
+ <div className="oxa-grid">
884
+ {group.fields.map((field) => (
885
+ <FieldControl
886
+ key={field.key}
887
+ disabled={!fieldWritable(field, mode, hasCapability, identity.isAppSuperAdmin)}
888
+ field={field}
889
+ operation={mode === 'create' ? 'create' : 'update'}
890
+ recordId={record?.id}
891
+ renderers={{
892
+ upload: (current, file, recordId) => createNativeResourceClient(code, surface).upload(current.key, file, recordId),
893
+ }}
894
+ />
895
+ ))}
896
+ </div>
897
+ </Card>
898
+ ))}
899
+ </div>
900
+ <div className="oxa-actions">
901
+ <Button onClick={() => navigate(mode === 'edit' ? detailPath : listPath)}>取消</Button>
902
+ <Button htmlType="submit" loading={create.mutation.isPending || update.mutation.isPending} type="primary">
903
+ {mode === 'create' ? '保存' : '保存修改'}
904
+ </Button>
905
+ </div>
707
906
  </Form>
708
907
  </Page>
709
908
  );
@@ -712,7 +911,11 @@ function GeneratedResourceFormPage({ definition, mode, variant }: { definition:
712
911
  function GeneratedResourceDetailPage({ definition, variant }: { definition: GeneratedDefinition; variant: 'desktop' | 'mobile' }) {
713
912
  const { id = '' } = useParams();
714
913
  const navigate = useNavigate();
715
- const query = useOne<ResourceRecord>({ resource: definition.code, id, queryOptions: { retry: false } });
914
+ const query = useOne<ResourceRecord>({
915
+ resource: definition.code,
916
+ id,
917
+ queryOptions: { retry: false },
918
+ });
716
919
  const record = query.result;
717
920
  const [auditEntries, setAuditEntries] = useState<DataAuditEntry[]>([]);
718
921
  const [auditLoading, setAuditLoading] = useState(false);
@@ -724,14 +927,15 @@ function GeneratedResourceDetailPage({ definition, variant }: { definition: Gene
724
927
  let active = true;
725
928
  setAuditLoading(true);
726
929
  setAuditError('');
727
- void createNativeResourceClient(definition.code, definition.surface).audit(record.id)
728
- .then(page => {
930
+ void createNativeResourceClient(definition.code, definition.surface)
931
+ .audit(record.id)
932
+ .then((page) => {
729
933
  if (active) {
730
934
  setAuditEntries(page.items || []);
731
935
  setAuditLoadedFor(record.id);
732
936
  }
733
937
  })
734
- .catch(reason => {
938
+ .catch((reason) => {
735
939
  if (!active) return;
736
940
  setAuditEntries([]);
737
941
  setAuditError(reason instanceof Error ? reason.message : String(reason));
@@ -762,37 +966,25 @@ function GeneratedResourceDetailPage({ definition, variant }: { definition: Gene
762
966
  type="error"
763
967
  />
764
968
  ) : auditLoading ? (
765
- <div className="oxa-audit-loading"><Spin size="small" /> 正在读取操作记录</div>
969
+ <div className="oxa-audit-loading">
970
+ <Spin size="small" /> 正在读取操作记录
971
+ </div>
766
972
  ) : 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
- ))
973
+ auditEntries.slice(0, 5).map((entry) => <AuditEntry entry={entry} key={entry.id} surface={definition.surface} />)
776
974
  ) : (
777
975
  <Typography.Text type="secondary">暂无变更记录</Typography.Text>
778
976
  );
779
977
  const content = record ? (
780
978
  <div className="oxa-detail-layout">
781
979
  <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
- >
980
+ {fieldsBySection(definition.surface).map((group) => (
981
+ <Card className="oxa-section-card" key={group.section} title={group.section === 'default' ? '基本信息' : group.section}>
788
982
  <Descriptions
789
983
  column={{ xs: 1, sm: 2, lg: 3 }}
790
- items={group.fields.map(field => ({
984
+ items={group.fields.map((field) => ({
791
985
  key: field.key,
792
986
  label: field.label,
793
- children: (
794
- <SurfaceFieldValue field={field} value={record[field.key]} />
795
- ),
987
+ children: <SurfaceFieldValue field={field} value={record[field.key]} />,
796
988
  }))}
797
989
  />
798
990
  </Card>
@@ -814,11 +1006,7 @@ function GeneratedResourceDetailPage({ definition, variant }: { definition: Gene
814
1006
  children: auditContent,
815
1007
  },
816
1008
  ]}
817
- onChange={keys =>
818
- setAuditExpanded(
819
- (Array.isArray(keys) ? keys : [keys]).includes('audit'),
820
- )
821
- }
1009
+ onChange={(keys) => setAuditExpanded((Array.isArray(keys) ? keys : [keys]).includes('audit'))}
822
1010
  />
823
1011
  </div>
824
1012
  </div>
@@ -826,9 +1014,7 @@ function GeneratedResourceDetailPage({ definition, variant }: { definition: Gene
826
1014
  ) : null;
827
1015
  const hero = record ? (
828
1016
  <div>
829
- <Typography.Title level={variant === 'mobile' ? 3 : 2}>
830
- {definition.name}详情
831
- </Typography.Title>
1017
+ <Typography.Title level={variant === 'mobile' ? 3 : 2}>{definition.name}详情</Typography.Title>
832
1018
  </div>
833
1019
  ) : null;
834
1020
  const props = {
@@ -837,20 +1023,10 @@ function GeneratedResourceDetailPage({ definition, variant }: { definition: Gene
837
1023
  backPath: `/${definition.code}`,
838
1024
  editCapability: definition.capabilities.update,
839
1025
  editPath: `/${definition.code}/${id}/edit`,
840
- error:
841
- query.query.isError || (!query.query.isFetching && !record)
842
- ? query.query.error?.message || '记录不存在'
843
- : undefined,
1026
+ error: query.query.isError || (!query.query.isFetching && !record) ? query.query.error?.message || '记录不存在' : undefined,
844
1027
  hero,
845
1028
  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,
1029
+ meta: record ? <span>最近更新 {record.updated_at ? new Date(record.updated_at).toLocaleString('zh-CN') : '-'}</span> : null,
854
1030
  onAuditOpen: () => setAuditExpanded(true),
855
1031
  readCapability: definition.capabilities.read,
856
1032
  resource: definition.code,