best-lowcode-runtime 0.2.7 → 0.2.9

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/dist/index.js CHANGED
@@ -34,7 +34,7 @@ function createBestCrudAdapter(options = {}) {
34
34
 
35
35
  // src/lowcode/BestCrudPage.tsx
36
36
  import { Button as Button4, Modal as Modal2, message } from "antd";
37
- import dayjs5 from "dayjs";
37
+ import dayjs6 from "dayjs";
38
38
  import { useCallback as useCallback2, useEffect as useEffect3, useLayoutEffect, useMemo as useMemo5, useRef as useRef3, useState as useState4 } from "react";
39
39
 
40
40
  // src/runtime/BestProvider.tsx
@@ -169,14 +169,65 @@ function BestBatchInput({
169
169
  }
170
170
 
171
171
  // src/ui/BestDetail.tsx
172
- import { Descriptions } from "antd";
173
- import { jsx as jsx3 } from "react/jsx-runtime";
174
- function BestDetail({ fields, record = {}, column = 2 }) {
175
- return /* @__PURE__ */ jsx3(Descriptions, { bordered: true, column, size: "small", children: fields.map((field) => {
172
+ import { Descriptions, Empty, Table, Typography } from "antd";
173
+ import dayjs2 from "dayjs";
174
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
175
+ function BestDetail({ fields, record = {}, column = 2, sections, sectionColumns, sectionGap = 20 }) {
176
+ const content = sections?.length ? /* @__PURE__ */ jsx3("div", { style: { display: "grid", minWidth: 0, maxWidth: "100%", width: "100%", boxSizing: "border-box", gridTemplateColumns: `repeat(${sectionColumns ?? 1}, minmax(0, 1fr))`, gap: sectionGap }, children: sections.map((section) => {
177
+ const card = section.variant === "card";
178
+ return /* @__PURE__ */ jsxs("section", { style: { minWidth: 0, maxWidth: "100%", gridColumn: section.span ? `span ${section.span}` : void 0, marginBottom: 0, border: card ? "1px solid #f0f0f0" : void 0, borderRadius: card ? 6 : void 0, overflow: card ? "hidden" : void 0, background: card ? "#fff" : void 0 }, children: [
179
+ section.title ? /* @__PURE__ */ jsx3("div", { style: { padding: card ? "8px 12px" : void 0, background: card ? "#fafafa" : void 0, borderBottom: card ? "1px solid #f0f0f0" : void 0 }, children: /* @__PURE__ */ jsx3(Typography.Title, { level: 5, style: { margin: 0 }, children: section.title }) }) : null,
180
+ /* @__PURE__ */ jsxs("div", { style: { padding: card ? 12 : void 0 }, children: [
181
+ section.description ? /* @__PURE__ */ jsx3(Typography.Paragraph, { type: "secondary", children: section.description }) : null,
182
+ section.content,
183
+ section.fields ? /* @__PURE__ */ jsx3(FieldDescriptions, { fields: section.fields, record, column: section.columns ?? column }) : null,
184
+ section.table ? /* @__PURE__ */ jsx3(DetailTable, { table: section.table }) : null
185
+ ] })
186
+ ] }, section.key);
187
+ }) }) : /* @__PURE__ */ jsx3(FieldDescriptions, { fields, record, column });
188
+ return content;
189
+ }
190
+ function FieldDescriptions({ fields, record, column }) {
191
+ const visibleFields = fields.filter((field) => field.visible !== false);
192
+ return /* @__PURE__ */ jsx3("div", { style: { minWidth: 0, maxWidth: "100%", width: "100%", overflowX: "auto" }, children: /* @__PURE__ */ jsx3(Descriptions, { bordered: true, column, size: "small", style: { minWidth: 0 }, children: visibleFields.map((field) => {
176
193
  const value = getPathValue(record, field.field);
177
- const display = field.render ? field.render(value, record) : field.valueEnum?.[String(value)] ?? (value == null || value === "" ? "-" : String(value));
194
+ const display = field.render ? field.render(value, record) : field.valueEnum?.[String(value)] ?? formatValue(value, field.format, field.emptyText);
178
195
  return /* @__PURE__ */ jsx3(Descriptions.Item, { label: field.label, span: field.span, children: display }, field.field);
179
- }) });
196
+ }) }) });
197
+ }
198
+ function DetailTable({ table }) {
199
+ if (!table.data.length) return /* @__PURE__ */ jsx3(Empty, { image: Empty.PRESENTED_IMAGE_SIMPLE, description: "\u6682\u65E0\u6570\u636E" });
200
+ return /* @__PURE__ */ jsx3("div", { style: { minWidth: 0, maxWidth: "100%", width: "100%", overflowX: "auto" }, children: /* @__PURE__ */ jsx3(
201
+ Table,
202
+ {
203
+ size: "small",
204
+ bordered: true,
205
+ pagination: false,
206
+ rowKey: table.rowKey,
207
+ dataSource: table.data,
208
+ scroll: table.scrollX ? { x: table.scrollX } : void 0,
209
+ columns: table.columns.map((column) => ({ ...column, dataIndex: column.dataIndex ?? column.key }))
210
+ }
211
+ ) });
212
+ }
213
+ function formatValue(value, format, emptyText = "-") {
214
+ if (value == null || value === "") return emptyText;
215
+ const type = typeof format === "object" ? format.type : format;
216
+ if (!type || type === "text") return String(value);
217
+ if (type === "number") {
218
+ const number = Number(value);
219
+ if (!Number.isFinite(number)) return String(value);
220
+ const precision = typeof format === "object" ? format.precision : void 0;
221
+ return number.toLocaleString("zh-CN", precision === void 0 ? void 0 : { minimumFractionDigits: precision, maximumFractionDigits: precision });
222
+ }
223
+ if (type === "money") {
224
+ const number = Number(value);
225
+ return Number.isFinite(number) ? number.toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : String(value);
226
+ }
227
+ if (type === "boolean") return value ? "\u662F" : "\u5426";
228
+ if (type === "json") return typeof value === "string" ? value : JSON.stringify(value);
229
+ const date = dayjs2(value);
230
+ return date.isValid() ? date.format(type === "date" ? "YYYY-MM-DD" : "YYYY-MM-DD HH:mm:ss") : String(value);
180
231
  }
181
232
  function getPathValue(values, path) {
182
233
  if (Object.hasOwn(values, path)) return values[path];
@@ -189,8 +240,8 @@ function getPathValue(values, path) {
189
240
  }
190
241
 
191
242
  // src/ui/BestFilePreview.tsx
192
- import { Button, Image, List, Typography } from "antd";
193
- import { jsx as jsx4, jsxs } from "react/jsx-runtime";
243
+ import { Button, Image, List, Typography as Typography2 } from "antd";
244
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
194
245
  function isImage(file) {
195
246
  return file.mimeType?.startsWith("image/") || /\.(avif|bmp|gif|jpe?g|png|svg|webp)(?:$|[?#])/i.test(file.url);
196
247
  }
@@ -199,7 +250,7 @@ function BestFilePreview({ files, onDownload }) {
199
250
  List,
200
251
  {
201
252
  dataSource: files,
202
- renderItem: (file) => /* @__PURE__ */ jsxs(
253
+ renderItem: (file) => /* @__PURE__ */ jsxs2(
203
254
  List.Item,
204
255
  {
205
256
  actions: [
@@ -207,7 +258,7 @@ function BestFilePreview({ files, onDownload }) {
207
258
  ],
208
259
  children: [
209
260
  isImage(file) ? /* @__PURE__ */ jsx4(Image, { alt: file.name ?? "\u6587\u4EF6\u9884\u89C8", height: 48, preview: true, src: file.url, width: 48 }) : null,
210
- /* @__PURE__ */ jsx4(Typography.Text, { ellipsis: true, style: { marginInlineStart: isImage(file) ? 12 : 0 }, children: file.name ?? file.url })
261
+ /* @__PURE__ */ jsx4(Typography2.Text, { ellipsis: true, style: { marginInlineStart: isImage(file) ? 12 : 0 }, children: file.name ?? file.url })
211
262
  ]
212
263
  }
213
264
  )
@@ -217,9 +268,9 @@ function BestFilePreview({ files, onDownload }) {
217
268
 
218
269
  // src/ui/BestForm.tsx
219
270
  import { Button as Button2, DatePicker, Form, Input as Input2, InputNumber, Select, Space } from "antd";
220
- import dayjs2 from "dayjs";
271
+ import dayjs3 from "dayjs";
221
272
  import { useEffect, useMemo as useMemo3, useRef, useState as useState3 } from "react";
222
- import { Fragment, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
273
+ import { Fragment, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
223
274
  function toNamePath(path) {
224
275
  return path === void 0 ? [] : Array.isArray(path) ? path : [path];
225
276
  }
@@ -230,7 +281,7 @@ function isRecord(value) {
230
281
  if (!value || typeof value !== "object" || Array.isArray(value) || value instanceof Date) {
231
282
  return false;
232
283
  }
233
- if (dayjs2.isDayjs(value)) return false;
284
+ if (dayjs3.isDayjs(value)) return false;
234
285
  const prototype = Object.getPrototypeOf(value);
235
286
  return prototype === Object.prototype || prototype === null;
236
287
  }
@@ -252,17 +303,17 @@ function resolveFieldDisabled(field, values, mode) {
252
303
  return field.disabled || Boolean(field.disabledWhen && evaluateCondition(field.disabledWhen, values, mode));
253
304
  }
254
305
  function toDayjsValue(value) {
255
- if (dayjs2.isDayjs(value)) return value;
256
- if (value instanceof Date) return dayjs2(value);
306
+ if (dayjs3.isDayjs(value)) return value;
307
+ if (value instanceof Date) return dayjs3(value);
257
308
  if (typeof value === "number") {
258
309
  const normalized = Math.abs(value) >= 1e12 ? value : value * 1e3;
259
- const parsed = dayjs2(normalized);
310
+ const parsed = dayjs3(normalized);
260
311
  return parsed.isValid() ? parsed : null;
261
312
  }
262
313
  if (typeof value === "string") {
263
314
  const trimmed = value.trim();
264
315
  if (!trimmed) return null;
265
- const parsed = dayjs2(trimmed);
316
+ const parsed = dayjs3(trimmed);
266
317
  return parsed.isValid() ? parsed : null;
267
318
  }
268
319
  return null;
@@ -356,7 +407,7 @@ function FormControl({
356
407
  value: toDatePickerValue(value),
357
408
  onChange: (date, dateString) => onChange?.(serializeDatePickerValue(date, dateString)),
358
409
  disabled: field.disabled,
359
- disabledDate: (current) => field.maxDate === "today" && current.isAfter(dayjs2(), "day"),
410
+ disabledDate: (current) => field.maxDate === "today" && current.isAfter(dayjs3(), "day"),
360
411
  style: { width: "100%" }
361
412
  }
362
413
  );
@@ -368,7 +419,7 @@ function FormControl({
368
419
  value: toDateRangePickerValue(value),
369
420
  onChange: (dates, dateStrings) => onChange?.(serializeDateRangePickerValue(dates, dateStrings)),
370
421
  disabled: field.disabled,
371
- disabledDate: (current) => field.maxDate === "today" && current.isAfter(dayjs2(), "day"),
422
+ disabledDate: (current) => field.maxDate === "today" && current.isAfter(dayjs3(), "day"),
372
423
  style: { width: "100%" }
373
424
  }
374
425
  );
@@ -427,7 +478,7 @@ function RepeatableField({
427
478
  values
428
479
  }) {
429
480
  const listName = namePath ?? field.field;
430
- return /* @__PURE__ */ jsx5(Form.List, { name: listName, children: (items, { add, remove }) => /* @__PURE__ */ jsxs2(Fragment, { children: [
481
+ return /* @__PURE__ */ jsx5(Form.List, { name: listName, children: (items, { add, remove }) => /* @__PURE__ */ jsxs3(Fragment, { children: [
431
482
  items.map((item) => /* @__PURE__ */ jsx5(
432
483
  RepeatableFieldRow,
433
484
  {
@@ -477,7 +528,7 @@ function RepeatableFieldRow({
477
528
  }
478
529
  });
479
530
  }, [field.itemFields, form, itemPath, mode, scopeValues, values]);
480
- return /* @__PURE__ */ jsxs2(Space, { align: "start", children: [
531
+ return /* @__PURE__ */ jsxs3(Space, { align: "start", children: [
481
532
  field.itemFields?.map((itemField) => {
482
533
  if (!isFieldVisible(itemField, scopeValues, mode)) return null;
483
534
  const nextPath = appendNamePath(itemPath, itemField.field);
@@ -743,9 +794,9 @@ function BestFormBody({
743
794
  onCancel
744
795
  }) {
745
796
  const [form] = Form.useForm();
746
- return /* @__PURE__ */ jsxs2(Form, { form, initialValues, layout: "vertical", onFinish: onSubmit, children: [
797
+ return /* @__PURE__ */ jsxs3(Form, { form, initialValues, layout: "vertical", onFinish: onSubmit, children: [
747
798
  /* @__PURE__ */ jsx5(FormFields, { fields, form, initialValues, mode }),
748
- /* @__PURE__ */ jsxs2(Space, { children: [
799
+ /* @__PURE__ */ jsxs3(Space, { children: [
749
800
  /* @__PURE__ */ jsx5(Button2, { htmlType: "submit", loading, type: "primary", children: submitText }),
750
801
  onCancel ? /* @__PURE__ */ jsx5(Button2, { onClick: onCancel, children: "\u53D6\u6D88" }) : null,
751
802
  extra
@@ -807,7 +858,7 @@ function evaluateCondition(condition, values, mode) {
807
858
  }
808
859
 
809
860
  // src/ui/BestLightTable.tsx
810
- import { Table } from "antd";
861
+ import { Table as Table2 } from "antd";
811
862
  import { jsx as jsx6 } from "react/jsx-runtime";
812
863
  function BestLightTable({
813
864
  columns,
@@ -822,7 +873,7 @@ function BestLightTable({
822
873
  render: column.render
823
874
  }));
824
875
  return /* @__PURE__ */ jsx6(
825
- Table,
876
+ Table2,
826
877
  {
827
878
  columns: tableColumns,
828
879
  dataSource: data,
@@ -836,18 +887,33 @@ function BestLightTable({
836
887
  // src/ui/BestOverlay.tsx
837
888
  import { Drawer, Modal } from "antd";
838
889
  import { jsx as jsx7 } from "react/jsx-runtime";
839
- function BestDrawer({ children, open, title, onClose }) {
840
- return /* @__PURE__ */ jsx7(Drawer, { destroyOnHidden: true, open, title, width: 640, onClose, children });
890
+ function BestDrawer({ children, open, title, onClose, width, footer }) {
891
+ return /* @__PURE__ */ jsx7(Drawer, { destroyOnHidden: true, open, title, width: width ?? 640, footer, onClose, children });
841
892
  }
842
- function BestModal({ children, open, title, onClose }) {
843
- return /* @__PURE__ */ jsx7(Modal, { destroyOnHidden: true, footer: null, open, title, onCancel: onClose, children });
893
+ function BestModal({ children, open, title, onClose, width, footer }) {
894
+ return /* @__PURE__ */ jsx7(
895
+ Modal,
896
+ {
897
+ destroyOnHidden: true,
898
+ footer: footer ?? null,
899
+ open,
900
+ title,
901
+ width,
902
+ onCancel: onClose,
903
+ styles: {
904
+ root: { minWidth: 0, maxWidth: "100%", boxSizing: "border-box" },
905
+ body: { minWidth: 0, maxWidth: "100%", overflowX: "hidden", boxSizing: "border-box" }
906
+ },
907
+ children: /* @__PURE__ */ jsx7("div", { style: { minWidth: 0, maxWidth: "100%", width: "100%", boxSizing: "border-box" }, children })
908
+ }
909
+ );
844
910
  }
845
911
 
846
912
  // src/ui/BestSearch.tsx
847
913
  import { Button as Button3, Col, DatePicker as DatePicker2, Form as Form2, Input as Input3, InputNumber as InputNumber2, Row, Select as Select2 } from "antd";
848
- import dayjs3 from "dayjs";
914
+ import dayjs4 from "dayjs";
849
915
  import { useEffect as useEffect2, useMemo as useMemo4, useRef as useRef2 } from "react";
850
- import { jsx as jsx8, jsxs as jsxs3 } from "react/jsx-runtime";
916
+ import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
851
917
  function searchFormSignature(fields, initialValues) {
852
918
  return JSON.stringify([
853
919
  fields.map((field) => ({
@@ -912,7 +978,7 @@ function FieldControl({ field, value, onChange, ...controlProps }) {
912
978
  value: toDatePickerValue(value),
913
979
  onChange: (date, dateString) => onChange?.(serializeDatePickerValue(date, dateString)),
914
980
  disabled: field.disabled,
915
- disabledDate: (current) => field.maxDate === "today" && current.isAfter(dayjs3(), "day"),
981
+ disabledDate: (current) => field.maxDate === "today" && current.isAfter(dayjs4(), "day"),
916
982
  placeholder: field.placeholder,
917
983
  style: { width: "100%" }
918
984
  }
@@ -925,7 +991,7 @@ function FieldControl({ field, value, onChange, ...controlProps }) {
925
991
  value: toDateRangePickerValue(value),
926
992
  onChange: (dates, dateStrings) => onChange?.(serializeDateRangePickerValue(dates, dateStrings)),
927
993
  disabled: field.disabled,
928
- disabledDate: (current) => field.maxDate === "today" && current.isAfter(dayjs3(), "day"),
994
+ disabledDate: (current) => field.maxDate === "today" && current.isAfter(dayjs4(), "day"),
929
995
  style: { width: "100%" }
930
996
  }
931
997
  );
@@ -1027,9 +1093,9 @@ function BestSearchBody({
1027
1093
  onFinish: (values) => onSearch(values),
1028
1094
  onValuesChange: (_, values) => onChange?.(values),
1029
1095
  className: "best-lowcode-search",
1030
- children: /* @__PURE__ */ jsxs3(Row, { gutter: 16, align: "bottom", children: [
1096
+ children: /* @__PURE__ */ jsxs4(Row, { gutter: 16, align: "bottom", children: [
1031
1097
  fields.filter((field) => !field.hidden).map((field) => /* @__PURE__ */ jsx8(Col, { span: field.span ?? 6, children: /* @__PURE__ */ jsx8(Form2.Item, { label: field.label, name: field.field, rules: toAntRules(field.rules), children: /* @__PURE__ */ jsx8(FieldControl, { field }) }) }, field.field)),
1032
- /* @__PURE__ */ jsx8(Col, { children: /* @__PURE__ */ jsxs3(Form2.Item, { children: [
1098
+ /* @__PURE__ */ jsx8(Col, { children: /* @__PURE__ */ jsxs4(Form2.Item, { children: [
1033
1099
  /* @__PURE__ */ jsx8(Button3, { htmlType: "submit", loading, type: "primary", children: submitText }),
1034
1100
  /* @__PURE__ */ jsx8(
1035
1101
  Button3,
@@ -1052,13 +1118,13 @@ function BestSearchBody({
1052
1118
  }
1053
1119
 
1054
1120
  // src/ui/BestStates.tsx
1055
- import { Empty, Result, Spin } from "antd";
1121
+ import { Empty as Empty2, Result, Spin } from "antd";
1056
1122
  import { jsx as jsx9 } from "react/jsx-runtime";
1057
1123
  function BestLoading({ description = "\u52A0\u8F7D\u4E2D" }) {
1058
1124
  return /* @__PURE__ */ jsx9("div", { style: { display: "grid", minHeight: 160, placeItems: "center" }, children: /* @__PURE__ */ jsx9(Spin, { tip: description }) });
1059
1125
  }
1060
1126
  function BestEmpty({ description = "\u6682\u65E0\u6570\u636E" }) {
1061
- return /* @__PURE__ */ jsx9(Empty, { description });
1127
+ return /* @__PURE__ */ jsx9(Empty2, { description });
1062
1128
  }
1063
1129
  function BestError({ description = "\u52A0\u8F7D\u5931\u8D25" }) {
1064
1130
  return /* @__PURE__ */ jsx9(Result, { status: "error", title: description });
@@ -1109,9 +1175,9 @@ function BestTable({
1109
1175
  }
1110
1176
 
1111
1177
  // src/lowcode/runtime.ts
1112
- import dayjs4 from "dayjs";
1178
+ import dayjs5 from "dayjs";
1113
1179
  import utc from "dayjs/plugin/utc.js";
1114
- dayjs4.extend(utc);
1180
+ dayjs5.extend(utc);
1115
1181
  function toBestListQuery(params, sort) {
1116
1182
  const { current, pageSize, ...filters } = params;
1117
1183
  const normalizedSort = Object.fromEntries(
@@ -1144,7 +1210,7 @@ function formatCrudValue(value, format) {
1144
1210
  const numericValue = typeof value === "number" ? value : Number(value.trim());
1145
1211
  timestamp = Number.isFinite(numericValue) && numericValue !== 0 ? Math.abs(numericValue) >= 1e12 ? numericValue : numericValue * 1e3 : value;
1146
1212
  }
1147
- const date = dayjs4.utc(timestamp);
1213
+ const date = dayjs5.utc(timestamp);
1148
1214
  if (!date.isValid()) return String(value);
1149
1215
  return date.format(format === "date" ? "YYYY-MM-DD" : "YYYY-MM-DD HH:mm:ss");
1150
1216
  }
@@ -1219,9 +1285,11 @@ var builtInEffects = /* @__PURE__ */ new Set([
1219
1285
  "openEdit",
1220
1286
  "remove",
1221
1287
  "runAction",
1222
- "slot"
1288
+ "slot",
1289
+ "closeDetail"
1223
1290
  ]);
1224
1291
  var columnFormats = /* @__PURE__ */ new Set(["date", "datetime", "money", "text"]);
1292
+ var detailFormats = /* @__PURE__ */ new Set(["text", "number", "money", "date", "datetime", "boolean", "json"]);
1225
1293
  function push(diagnostics, path, code, message2) {
1226
1294
  diagnostics.push({ path, code, message: message2 });
1227
1295
  }
@@ -1268,6 +1336,14 @@ function validateCondition(condition, path, diagnostics) {
1268
1336
  }
1269
1337
  push(diagnostics, path, "condition.operator", `\u4E0D\u652F\u6301\u7684\u6761\u4EF6\uFF1A${condition.operator}`);
1270
1338
  }
1339
+ function validateDetailField(field, path, registry, diagnostics) {
1340
+ if (field.visibleWhen) validateCondition(field.visibleWhen, `${path}/visibleWhen`, diagnostics);
1341
+ if (field.slot && registry && !registry.slots[field.slot])
1342
+ push(diagnostics, `${path}/slot`, "registry.slot", `\u672A\u6CE8\u518C\u63D2\u69FD\uFF1A${field.slot}`);
1343
+ const format = typeof field.format === "object" ? field.format.type : field.format;
1344
+ if (format && !detailFormats.has(format))
1345
+ push(diagnostics, `${path}/format`, "detail.format", `\u4E0D\u652F\u6301\u7684\u8BE6\u60C5\u683C\u5F0F\uFF1A${format}`);
1346
+ }
1271
1347
  function validateFields(fields, path, registry, diagnostics) {
1272
1348
  const seen = /* @__PURE__ */ new Set();
1273
1349
  fields?.forEach((field, index) => {
@@ -1421,17 +1497,26 @@ function validateCrudPageSchema(schema, registry) {
1421
1497
  }
1422
1498
  });
1423
1499
  if (schema.detail !== void 0) {
1424
- if (!schema.detail || typeof schema.detail !== "object" || !Array.isArray(schema.detail.fields)) {
1425
- push(diagnostics, "/detail", "detail.type", "detail \u5FC5\u987B\u5305\u542B fields \u5BF9\u8C61\u6570\u7EC4");
1500
+ if (!schema.detail || typeof schema.detail !== "object" || schema.detail.fields !== void 0 && !Array.isArray(schema.detail.fields) || schema.detail.sections !== void 0 && !Array.isArray(schema.detail.sections)) {
1501
+ push(diagnostics, "/detail", "detail.type", "detail \u5FC5\u987B\u5305\u542B fields \u6216 sections \u6570\u7EC4");
1426
1502
  } else {
1427
- schema.detail.fields.forEach((field, index) => {
1428
- if (field.slot && registry && !registry.slots[field.slot])
1429
- push(
1430
- diagnostics,
1431
- `/detail/fields/${index}/slot`,
1432
- "registry.slot",
1433
- `\u672A\u6CE8\u518C\u63D2\u69FD\uFF1A${field.slot}`
1434
- );
1503
+ schema.detail.fields?.forEach((field, index) => validateDetailField(field, `/detail/fields/${index}`, registry, diagnostics));
1504
+ schema.detail.sections?.forEach((section, index) => {
1505
+ if (section.visibleWhen) validateCondition(section.visibleWhen, `/detail/sections/${index}/visibleWhen`, diagnostics);
1506
+ if (!section.key) push(diagnostics, `/detail/sections/${index}/key`, "detail.section.key", "\u8BE6\u60C5\u533A\u5757\u5FC5\u987B\u6307\u5B9A key");
1507
+ if (section.span !== void 0 && (!Number.isInteger(section.span) || section.span < 1))
1508
+ push(diagnostics, `/detail/sections/${index}/span`, "detail.section.span", "\u8BE6\u60C5\u533A\u5757 span \u5FC5\u987B\u662F\u6B63\u6574\u6570");
1509
+ if (section.columns !== void 0 && (!Number.isInteger(section.columns) || section.columns < 1))
1510
+ push(diagnostics, `/detail/sections/${index}/columns`, "detail.section.columns", "\u8BE6\u60C5\u5B57\u6BB5 columns \u5FC5\u987B\u662F\u6B63\u6574\u6570");
1511
+ if (section.layout === "slot" && !section.slot)
1512
+ push(diagnostics, `/detail/sections/${index}/slot`, "detail.slot", "slot \u533A\u5757\u5FC5\u987B\u6307\u5B9A slot");
1513
+ if (section.layout === "table" && !section.table)
1514
+ push(diagnostics, `/detail/sections/${index}/table`, "detail.table", "table \u533A\u5757\u5FC5\u987B\u6307\u5B9A table");
1515
+ if (section.table && typeof section.table.data !== "string")
1516
+ push(diagnostics, `/detail/sections/${index}/table/data`, "detail.table.data", "\u8BE6\u60C5\u8868\u683C data \u5FC5\u987B\u662F\u5B57\u6BB5\u8DEF\u5F84");
1517
+ if (section.slot && registry && !registry.slots[section.slot])
1518
+ push(diagnostics, `/detail/sections/${index}/slot`, "registry.slot", `\u672A\u6CE8\u518C\u63D2\u69FD\uFF1A${section.slot}`);
1519
+ section.fields?.forEach((field, fieldIndex) => validateDetailField(field, `/detail/sections/${index}/fields/${fieldIndex}`, registry, diagnostics));
1435
1520
  });
1436
1521
  }
1437
1522
  }
@@ -1449,6 +1534,13 @@ function validateCrudPageSchema(schema, registry) {
1449
1534
  Boolean(schema.dataSource?.remove),
1450
1535
  diagnostics
1451
1536
  );
1537
+ validateActions(
1538
+ schema.detail?.footer,
1539
+ "/detail/footer",
1540
+ registry,
1541
+ Boolean(schema.dataSource?.remove),
1542
+ diagnostics
1543
+ );
1452
1544
  return { valid: diagnostics.length === 0, diagnostics };
1453
1545
  }
1454
1546
  function assertValidCrudPageSchema(schema, registry) {
@@ -1500,7 +1592,7 @@ ${message2}`);
1500
1592
  }
1501
1593
 
1502
1594
  // src/lowcode/BestCrudPage.tsx
1503
- import { Fragment as Fragment2, jsx as jsx11, jsxs as jsxs4 } from "react/jsx-runtime";
1595
+ import { Fragment as Fragment2, jsx as jsx11, jsxs as jsxs5 } from "react/jsx-runtime";
1504
1596
  function pickRowKeyValues(record, rowKey) {
1505
1597
  if (!record) return {};
1506
1598
  const keys = Array.isArray(rowKey) ? rowKey : [rowKey];
@@ -1573,7 +1665,7 @@ function useFields(fields = []) {
1573
1665
  const registry = useBestRegistry();
1574
1666
  return fields.map((field) => toFieldDefinition(field, registry.dictionaries, registry.slots));
1575
1667
  }
1576
- function toDetailField(field, dictionary, slots) {
1668
+ function toDetailField(field, dictionary, slots, record = {}) {
1577
1669
  const slot = field.slot;
1578
1670
  const valueEnum = field.dict ? Object.fromEntries(
1579
1671
  (dictionary[field.dict] ?? []).map((item) => [String(item.value), item.label])
@@ -1582,10 +1674,50 @@ function toDetailField(field, dictionary, slots) {
1582
1674
  field: field.field,
1583
1675
  label: field.label,
1584
1676
  span: field.span,
1677
+ format: field.format,
1678
+ emptyText: field.emptyText,
1679
+ visible: !field.visibleWhen || evaluateCondition(field.visibleWhen, record, "detail"),
1585
1680
  valueEnum,
1586
- render: slot ? (value, record) => slots[slot]?.({ field: field.field, record, value }) ?? "-" : void 0
1681
+ render: slot ? (value, record2) => slots[slot]?.({ field: field.field, record: record2, value }) ?? "-" : void 0
1587
1682
  };
1588
1683
  }
1684
+ function getPathValue3(values, path) {
1685
+ if (Object.hasOwn(values, path)) return values[path];
1686
+ return path.split(".").reduce((current, part) => {
1687
+ if (current && typeof current === "object") return current[part];
1688
+ return void 0;
1689
+ }, values);
1690
+ }
1691
+ function toDetailSection(section, record, dictionaries, slots) {
1692
+ if (section.visibleWhen && !evaluateCondition(section.visibleWhen, record, "detail")) return null;
1693
+ const fields = section.fields?.map((field) => toDetailField(field, dictionaries, slots, record));
1694
+ const table = section.table ? {
1695
+ key: section.key,
1696
+ data: getPathValue3(record, section.table.data) ?? [],
1697
+ rowKey: section.table.rowKey,
1698
+ scrollX: section.table.scrollX,
1699
+ columns: section.table.columns.map((column) => ({
1700
+ key: column.field,
1701
+ title: column.title,
1702
+ dataIndex: column.field,
1703
+ width: column.width,
1704
+ fixed: column.fixed,
1705
+ render: (value, row) => {
1706
+ if (column.slot) return slots[column.slot]?.({ field: column.field, record: row, value }) ?? "-";
1707
+ if (column.dict) {
1708
+ const item = (dictionaries[column.dict] ?? []).find((candidate) => candidate.value === value);
1709
+ if (item) return item.label;
1710
+ }
1711
+ const format = column.format;
1712
+ if (format === "date" || format === "datetime") return formatCrudValue(value, format);
1713
+ if (format === "money") return formatCrudValue(value, format);
1714
+ return value == null || value === "" ? "-" : String(value);
1715
+ }
1716
+ }))
1717
+ } : void 0;
1718
+ const content = section.layout === "slot" && section.slot ? slots[section.slot]?.({ record }) : void 0;
1719
+ return { key: section.key, title: section.title, description: section.description, columns: section.columns, span: section.span, variant: section.variant, fields, table, content };
1720
+ }
1589
1721
  function toTableColumn(column, dictionary, slots) {
1590
1722
  const slot = column.slot;
1591
1723
  const valueEnum = column.dict ? Object.fromEntries(
@@ -1631,7 +1763,7 @@ function toProTableSearchColumn(field, dictionary) {
1631
1763
  initialValue: field.defaultValue,
1632
1764
  fieldProps: {
1633
1765
  ...options ? { options } : {},
1634
- ...field.maxDate === "today" ? { disabledDate: (current) => current.isAfter(dayjs5(), "day") } : {}
1766
+ ...field.maxDate === "today" ? { disabledDate: (current) => current.isAfter(dayjs6(), "day") } : {}
1635
1767
  }
1636
1768
  };
1637
1769
  }
@@ -1685,14 +1817,44 @@ function BestCrudPage({ adapter, className, schema }) {
1685
1817
  userTouched: false
1686
1818
  });
1687
1819
  const [drawer, setDrawer] = useState4({ mode: "closed" });
1820
+ const [detailState, setDetailState] = useState4({ loading: false });
1821
+ const detailRequestRef = useRef3(void 0);
1688
1822
  const [submitting, setSubmitting] = useState4(false);
1689
1823
  const submittingRef = useRef3(false);
1690
1824
  const handleAction = useCallback2(
1691
1825
  async (action, record) => {
1692
1826
  if (action.access && !registry.access(action.access)) return;
1827
+ if (action.effect === "closeDetail") {
1828
+ detailRequestRef.current?.abort();
1829
+ setDrawer({ mode: "closed" });
1830
+ setDetailState({ loading: false });
1831
+ return;
1832
+ }
1693
1833
  if (action.effect !== "remove" && !await confirmBeforeAction(action.confirm, confirmAction))
1694
1834
  return;
1695
- if (action.effect === "openDetail") setDrawer({ mode: "detail", record });
1835
+ if (action.effect === "openDetail") {
1836
+ detailRequestRef.current?.abort();
1837
+ const controller = new AbortController();
1838
+ detailRequestRef.current = controller;
1839
+ setDrawer({ mode: "detail", record });
1840
+ setDetailState({ loading: Boolean(schema.dataSource.detail), record });
1841
+ if (schema.dataSource.detail) {
1842
+ const detailService = registry.services[schema.dataSource.detail];
1843
+ if (!detailService) {
1844
+ setDetailState({ loading: false, error: new Error(`\u672A\u6CE8\u518C\u670D\u52A1\uFF1A${schema.dataSource.detail}`), record });
1845
+ return;
1846
+ }
1847
+ try {
1848
+ const detail = await detailService(record ?? {}, { signal: controller.signal });
1849
+ if (controller.signal.aborted) return;
1850
+ const normalized = isRecord2(detail) ? adapter?.fromDetail ? adapter.fromDetail(detail) : detail : record ?? {};
1851
+ setDetailState({ loading: false, record: normalized });
1852
+ setDrawer({ mode: "detail", record: normalized });
1853
+ } catch (error) {
1854
+ if (!controller.signal.aborted) setDetailState({ loading: false, error, record });
1855
+ }
1856
+ }
1857
+ }
1696
1858
  if (action.effect === "openEdit") {
1697
1859
  let editRecord = record;
1698
1860
  if (schema.dataSource.detail) {
@@ -1858,7 +2020,7 @@ function BestCrudPage({ adapter, className, schema }) {
1858
2020
  schema.dataSource.update
1859
2021
  ]
1860
2022
  );
1861
- return /* @__PURE__ */ jsxs4(Fragment2, { children: [
2023
+ return /* @__PURE__ */ jsxs5(Fragment2, { children: [
1862
2024
  useBestSearch && searchFields.length ? /* @__PURE__ */ jsx11(
1863
2025
  BestSearch,
1864
2026
  {
@@ -1893,22 +2055,41 @@ function BestCrudPage({ adapter, className, schema }) {
1893
2055
  toolBarRender: () => schema.toolbar?.map((action) => /* @__PURE__ */ jsx11(ActionButton, { action, onExecute: handleAction }, action.id)) ?? []
1894
2056
  }
1895
2057
  ),
1896
- /* @__PURE__ */ jsxs4(
2058
+ drawer.mode === "detail" && schema.detail?.mode === "inline" ? /* @__PURE__ */ jsx11(DetailContent, { state: detailState, record: drawer.record }) : null,
2059
+ drawer.mode !== "closed" && (schema.detail?.mode !== "inline" || drawer.mode !== "detail") && schema.detail?.mode === "drawer" ? /* @__PURE__ */ jsxs5(
2060
+ BestDrawer,
2061
+ {
2062
+ open: true,
2063
+ width: schema.detail.width,
2064
+ title: drawer.mode === "detail" ? `${schema.title}\u8BE6\u60C5` : drawer.mode === "edit" ? `\u7F16\u8F91${schema.title}` : `\u65B0\u5EFA${schema.title}`,
2065
+ footer: drawer.mode === "detail" && schema.detail.footer?.length ? /* @__PURE__ */ jsx11("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8 }, children: schema.detail.footer.map((action) => /* @__PURE__ */ jsx11(ActionButton, { action, record: drawer.record, onExecute: handleAction }, action.id)) }) : void 0,
2066
+ onClose: () => {
2067
+ detailRequestRef.current?.abort();
2068
+ setDrawer({ mode: "closed" });
2069
+ setDetailState({ loading: false });
2070
+ },
2071
+ children: [
2072
+ drawer.mode === "detail" ? /* @__PURE__ */ jsx11(DetailContent, { state: detailState, record: drawer.record }) : null,
2073
+ drawer.mode === "edit" || drawer.mode === "create" ? /* @__PURE__ */ jsx11(BestForm, { fields: formFields, initialValues: drawer.record, loading: submitting, mode: drawer.mode, onCancel: () => setDrawer({ mode: "closed" }), onSubmit: (values) => {
2074
+ void submitForm(values);
2075
+ } }) : null
2076
+ ]
2077
+ }
2078
+ ) : null,
2079
+ drawer.mode !== "closed" && (schema.detail?.mode !== "inline" || drawer.mode !== "detail") && schema.detail?.mode !== "drawer" ? /* @__PURE__ */ jsxs5(
1897
2080
  BestModal,
1898
2081
  {
1899
- open: drawer.mode !== "closed",
2082
+ open: true,
1900
2083
  title: drawer.mode === "detail" ? `${schema.title}\u8BE6\u60C5` : drawer.mode === "edit" ? `\u7F16\u8F91${schema.title}` : `\u65B0\u5EFA${schema.title}`,
1901
- onClose: () => setDrawer({ mode: "closed" }),
2084
+ width: schema.detail?.width,
2085
+ footer: drawer.mode === "detail" && schema.detail?.footer?.length ? /* @__PURE__ */ jsx11("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8 }, children: schema.detail.footer.map((action) => /* @__PURE__ */ jsx11(ActionButton, { action, record: drawer.record, onExecute: handleAction }, action.id)) }) : void 0,
2086
+ onClose: () => {
2087
+ detailRequestRef.current?.abort();
2088
+ setDrawer({ mode: "closed" });
2089
+ setDetailState({ loading: false });
2090
+ },
1902
2091
  children: [
1903
- drawer.mode === "detail" ? /* @__PURE__ */ jsx11(
1904
- BestDetail,
1905
- {
1906
- fields: schema.detail?.fields.map(
1907
- (field) => toDetailField(field, registry.dictionaries, registry.slots)
1908
- ) ?? [],
1909
- record: drawer.record
1910
- }
1911
- ) : null,
2092
+ drawer.mode === "detail" ? /* @__PURE__ */ jsx11(DetailContent, { state: detailState, record: drawer.record }) : null,
1912
2093
  drawer.mode === "edit" || drawer.mode === "create" ? /* @__PURE__ */ jsx11(
1913
2094
  BestForm,
1914
2095
  {
@@ -1924,8 +2105,24 @@ function BestCrudPage({ adapter, className, schema }) {
1924
2105
  ) : null
1925
2106
  ]
1926
2107
  }
1927
- )
2108
+ ) : null
1928
2109
  ] });
2110
+ function DetailContent({ state, record }) {
2111
+ if (state.loading) return /* @__PURE__ */ jsx11(BestLoading, {});
2112
+ if (state.error) return /* @__PURE__ */ jsx11(BestError, { description: errorMessage(state.error, `${schema.title}\u8BE6\u60C5\u52A0\u8F7D\u5931\u8D25`) });
2113
+ if (!record) return /* @__PURE__ */ jsx11(BestEmpty, {});
2114
+ const sections = schema.detail?.sections?.map((section) => toDetailSection(section, record, registry.dictionaries, registry.slots)).filter(Boolean);
2115
+ return /* @__PURE__ */ jsx11(
2116
+ BestDetail,
2117
+ {
2118
+ fields: schema.detail?.fields?.map((field) => toDetailField(field, registry.dictionaries, registry.slots, record)) ?? [],
2119
+ sections,
2120
+ sectionColumns: schema.detail?.columns,
2121
+ sectionGap: schema.detail?.gap,
2122
+ record
2123
+ }
2124
+ );
2125
+ }
1929
2126
  function ActionButton({
1930
2127
  action,
1931
2128
  record,