@aiao/rxdb-model 0.0.18 → 0.0.26

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.
Files changed (181) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +43 -6
  3. package/dist/cn.d.ts +19 -0
  4. package/dist/cn.d.ts.map +1 -0
  5. package/dist/entity-capabilities.d.ts +31 -0
  6. package/dist/entity-capabilities.d.ts.map +1 -0
  7. package/dist/entity-detail/detail-tabs.d.ts +13 -2
  8. package/dist/entity-detail/detail-tabs.d.ts.map +1 -1
  9. package/dist/entity-detail/index.d.ts +5 -0
  10. package/dist/entity-detail/index.d.ts.map +1 -1
  11. package/dist/entity-detail/interfaces.d.ts +29 -1
  12. package/dist/entity-detail/interfaces.d.ts.map +1 -1
  13. package/dist/entity-field.utils.d.ts +60 -2
  14. package/dist/entity-field.utils.d.ts.map +1 -1
  15. package/dist/entity-form/form-data.d.ts +22 -1
  16. package/dist/entity-form/form-data.d.ts.map +1 -1
  17. package/dist/entity-form/form-fields.d.ts +25 -2
  18. package/dist/entity-form/form-fields.d.ts.map +1 -1
  19. package/dist/entity-form/form-validation.d.ts +20 -2
  20. package/dist/entity-form/form-validation.d.ts.map +1 -1
  21. package/dist/entity-form/index.d.ts +5 -0
  22. package/dist/entity-form/index.d.ts.map +1 -1
  23. package/dist/entity-form/interfaces.d.ts +65 -1
  24. package/dist/entity-form/interfaces.d.ts.map +1 -1
  25. package/dist/entity-list/manual-order-list.d.ts +99 -0
  26. package/dist/entity-list/manual-order-list.d.ts.map +1 -0
  27. package/dist/entity-table/columns/build-editable-columns.d.ts +26 -4
  28. package/dist/entity-table/columns/build-editable-columns.d.ts.map +1 -1
  29. package/dist/entity-table/columns/column-utils.d.ts +78 -12
  30. package/dist/entity-table/columns/column-utils.d.ts.map +1 -1
  31. package/dist/entity-table/editors/color-editor.d.ts +23 -0
  32. package/dist/entity-table/editors/color-editor.d.ts.map +1 -0
  33. package/dist/entity-table/editors/date-editor.d.ts +6 -1
  34. package/dist/entity-table/editors/date-editor.d.ts.map +1 -1
  35. package/dist/entity-table/editors/enum-editor.d.ts +11 -1
  36. package/dist/entity-table/editors/enum-editor.d.ts.map +1 -1
  37. package/dist/entity-table/editors/global-overlay-editor.d.ts +27 -4
  38. package/dist/entity-table/editors/global-overlay-editor.d.ts.map +1 -1
  39. package/dist/entity-table/editors/icon-list-editor.d.ts +13 -1
  40. package/dist/entity-table/editors/icon-list-editor.d.ts.map +1 -1
  41. package/dist/entity-table/editors/json-editor.d.ts +6 -1
  42. package/dist/entity-table/editors/json-editor.d.ts.map +1 -1
  43. package/dist/entity-table/editors/key-value-editor.d.ts +12 -2
  44. package/dist/entity-table/editors/key-value-editor.d.ts.map +1 -1
  45. package/dist/entity-table/editors/lucide-svg.d.ts +3 -2
  46. package/dist/entity-table/editors/lucide-svg.d.ts.map +1 -1
  47. package/dist/entity-table/editors/multiselect-editor.d.ts +24 -6
  48. package/dist/entity-table/editors/multiselect-editor.d.ts.map +1 -1
  49. package/dist/entity-table/editors/number-editor.d.ts +33 -3
  50. package/dist/entity-table/editors/number-editor.d.ts.map +1 -1
  51. package/dist/entity-table/editors/relation-editor.d.ts +14 -2
  52. package/dist/entity-table/editors/relation-editor.d.ts.map +1 -1
  53. package/dist/entity-table/editors/safe-list-editor.d.ts +1 -0
  54. package/dist/entity-table/editors/safe-list-editor.d.ts.map +1 -1
  55. package/dist/entity-table/editors/tags-editor.d.ts +12 -1
  56. package/dist/entity-table/editors/tags-editor.d.ts.map +1 -1
  57. package/dist/entity-table/editors/text-format-editor.d.ts +32 -0
  58. package/dist/entity-table/editors/text-format-editor.d.ts.map +1 -0
  59. package/dist/entity-table/editors/uuid-editor.d.ts +14 -1
  60. package/dist/entity-table/editors/uuid-editor.d.ts.map +1 -1
  61. package/dist/entity-table/index.d.ts +13 -3
  62. package/dist/entity-table/index.d.ts.map +1 -1
  63. package/dist/entity-table/interfaces.d.ts +13 -0
  64. package/dist/entity-table/interfaces.d.ts.map +1 -1
  65. package/dist/entity-table/vtable/table-clipboard.d.ts +31 -2
  66. package/dist/entity-table/vtable/table-clipboard.d.ts.map +1 -1
  67. package/dist/entity-table/vtable/table-factory.d.ts +25 -5
  68. package/dist/entity-table/vtable/table-factory.d.ts.map +1 -1
  69. package/dist/entity-table/vtable/table-keyboard.d.ts +18 -1
  70. package/dist/entity-table/vtable/table-keyboard.d.ts.map +1 -1
  71. package/dist/entity-table/vtable/table-operations.d.ts +44 -3
  72. package/dist/entity-table/vtable/table-operations.d.ts.map +1 -1
  73. package/dist/entity-table/vtable/table-row-move.d.ts +88 -0
  74. package/dist/entity-table/vtable/table-row-move.d.ts.map +1 -0
  75. package/dist/entity-table/vtable/table-theme.d.ts +47 -2
  76. package/dist/entity-table/vtable/table-theme.d.ts.map +1 -1
  77. package/dist/entity-table/vtable/table-tooltip.d.ts +7 -1
  78. package/dist/entity-table/vtable/table-tooltip.d.ts.map +1 -1
  79. package/dist/entity-table/vtable/vtable-compat.d.ts +33 -5
  80. package/dist/entity-table/vtable/vtable-compat.d.ts.map +1 -1
  81. package/dist/entity-value.utils.d.ts +50 -2
  82. package/dist/entity-value.utils.d.ts.map +1 -1
  83. package/dist/index.d.ts +9 -0
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +4392 -64471
  86. package/dist/query-builder/models/config.interface.d.ts +2 -2
  87. package/dist/query-builder/models/operator.interface.d.ts +6 -2
  88. package/dist/query-builder/models/operator.interface.d.ts.map +1 -1
  89. package/dist/query-builder/models/query-builder-state.d.ts +31 -2
  90. package/dist/query-builder/models/query-builder-state.d.ts.map +1 -1
  91. package/dist/query-builder/models/validation.interface.d.ts +2 -2
  92. package/dist/query-builder/services/operator-registry.service.d.ts +13 -3
  93. package/dist/query-builder/services/operator-registry.service.d.ts.map +1 -1
  94. package/dist/query-builder/services/query-builder.service.d.ts +16 -6
  95. package/dist/query-builder/services/query-builder.service.d.ts.map +1 -1
  96. package/dist/query-builder/services/query-converter.service.d.ts +10 -2
  97. package/dist/query-builder/services/query-converter.service.d.ts.map +1 -1
  98. package/dist/query-builder/services/schema-parser.service.d.ts +10 -2
  99. package/dist/query-builder/services/schema-parser.service.d.ts.map +1 -1
  100. package/dist/query-builder/services/validation.service.d.ts +34 -4
  101. package/dist/query-builder/services/validation.service.d.ts.map +1 -1
  102. package/dist/query-builder/utils/date.d.ts +12 -0
  103. package/dist/query-builder/utils/date.d.ts.map +1 -1
  104. package/dist/query-builder/utils/field-tree.d.ts +7 -1
  105. package/dist/query-builder/utils/field-tree.d.ts.map +1 -1
  106. package/dist/query-builder/utils/id.d.ts +2 -0
  107. package/dist/query-builder/utils/id.d.ts.map +1 -1
  108. package/dist/query-builder/utils/metadata-field-extractor.d.ts +15 -3
  109. package/dist/query-builder/utils/metadata-field-extractor.d.ts.map +1 -1
  110. package/dist/query-builder/utils/popover-position.d.ts +11 -0
  111. package/dist/query-builder/utils/popover-position.d.ts.map +1 -1
  112. package/dist/query-builder/utils/schema-factory.d.ts +7 -3
  113. package/dist/query-builder/utils/structural-equal.d.ts +4 -0
  114. package/dist/query-builder/utils/structural-equal.d.ts.map +1 -1
  115. package/dist/query-builder/utils/value-input.d.ts +21 -1
  116. package/dist/query-builder/utils/value-input.d.ts.map +1 -1
  117. package/dist/structural-equal.d.ts +5 -1
  118. package/dist/structural-equal.d.ts.map +1 -1
  119. package/package.json +34 -10
  120. package/src/cn.ts +21 -0
  121. package/src/entity-capabilities.ts +37 -0
  122. package/src/entity-detail/detail-tabs.ts +48 -0
  123. package/src/entity-detail/index.ts +7 -0
  124. package/src/entity-detail/interfaces.ts +55 -0
  125. package/src/entity-field.utils.ts +205 -0
  126. package/src/entity-form/form-data.ts +83 -0
  127. package/src/entity-form/form-fields.ts +48 -0
  128. package/src/entity-form/form-validation.ts +34 -0
  129. package/src/entity-form/index.ts +9 -0
  130. package/src/entity-form/interfaces.ts +116 -0
  131. package/src/entity-list/manual-order-list.ts +159 -0
  132. package/src/entity-table/columns/build-editable-columns.ts +184 -0
  133. package/src/entity-table/columns/column-utils.ts +657 -0
  134. package/src/entity-table/editors/color-editor.ts +239 -0
  135. package/src/entity-table/editors/date-editor.ts +169 -0
  136. package/src/entity-table/editors/enum-editor.ts +167 -0
  137. package/src/entity-table/editors/global-overlay-editor.ts +277 -0
  138. package/src/entity-table/editors/icon-list-editor.ts +130 -0
  139. package/src/entity-table/editors/json-editor.ts +212 -0
  140. package/src/entity-table/editors/key-value-editor.ts +595 -0
  141. package/src/entity-table/editors/lucide-svg.ts +78 -0
  142. package/src/entity-table/editors/multiselect-editor.ts +171 -0
  143. package/src/entity-table/editors/number-editor.ts +322 -0
  144. package/src/entity-table/editors/relation-editor.ts +316 -0
  145. package/src/entity-table/editors/safe-list-editor.ts +17 -0
  146. package/src/entity-table/editors/tags-editor.ts +239 -0
  147. package/src/entity-table/editors/text-format-editor.ts +220 -0
  148. package/src/entity-table/editors/uuid-editor.ts +198 -0
  149. package/src/entity-table/index.ts +96 -0
  150. package/src/entity-table/interfaces.ts +72 -0
  151. package/src/entity-table/vtable/table-clipboard.ts +419 -0
  152. package/src/entity-table/vtable/table-factory.ts +117 -0
  153. package/src/entity-table/vtable/table-keyboard.ts +89 -0
  154. package/src/entity-table/vtable/table-operations.ts +123 -0
  155. package/src/entity-table/vtable/table-row-move.ts +122 -0
  156. package/src/entity-table/vtable/table-theme.ts +181 -0
  157. package/src/entity-table/vtable/table-tooltip.ts +70 -0
  158. package/src/entity-table/vtable/vtable-compat.ts +84 -0
  159. package/src/entity-value.utils.ts +456 -0
  160. package/src/index.ts +57 -0
  161. package/src/query-builder/models/config.interface.ts +31 -0
  162. package/src/query-builder/models/index.ts +8 -0
  163. package/src/query-builder/models/operator.interface.ts +191 -0
  164. package/src/query-builder/models/query-builder-state.ts +222 -0
  165. package/src/query-builder/models/validation.interface.ts +184 -0
  166. package/src/query-builder/services/index.ts +9 -0
  167. package/src/query-builder/services/operator-registry.service.ts +243 -0
  168. package/src/query-builder/services/query-builder.service.ts +568 -0
  169. package/src/query-builder/services/query-converter.service.ts +153 -0
  170. package/src/query-builder/services/schema-parser.service.ts +309 -0
  171. package/src/query-builder/services/validation.service.ts +390 -0
  172. package/src/query-builder/utils/date.ts +35 -0
  173. package/src/query-builder/utils/field-tree.ts +400 -0
  174. package/src/query-builder/utils/id.ts +15 -0
  175. package/src/query-builder/utils/index.ts +12 -0
  176. package/src/query-builder/utils/metadata-field-extractor.ts +460 -0
  177. package/src/query-builder/utils/popover-position.ts +59 -0
  178. package/src/query-builder/utils/schema-factory.ts +75 -0
  179. package/src/query-builder/utils/structural-equal.ts +5 -0
  180. package/src/query-builder/utils/value-input.ts +105 -0
  181. package/src/structural-equal.ts +49 -0
@@ -0,0 +1,657 @@
1
+ import type { FieldFormat, FieldOptions } from '@aiao/rxdb';
2
+ import type { ColumnDefine, StylePropertyFunctionArg } from '@visactor/vtable/es/ts-types/index.js';
3
+ import type { RelatedEntityItem } from '../../entity-form/interfaces.js';
4
+ import { formatEntityFieldValue, parseEntityFieldValue } from '../../entity-value.utils.js';
5
+ import { ColorEditor } from '../editors/color-editor.js';
6
+ import { EnumEditor, type EnumItem } from '../editors/enum-editor.js';
7
+ import { KeyValueEditor } from '../editors/key-value-editor.js';
8
+ import { MultiSelectEditor } from '../editors/multiselect-editor.js';
9
+ import { NumberEditor } from '../editors/number-editor.js';
10
+ import { RelationEditor } from '../editors/relation-editor.js';
11
+ import { TextFormatEditor } from '../editors/text-format-editor.js';
12
+ import { getCSSVariableValue, getDocumentRootStyle } from '../vtable/table-theme.js';
13
+
14
+ /** 删除图标默认颜色 */
15
+ export const DELETE_COLOR = 'var(--entity-table-delete, #ef4444)';
16
+
17
+ const DELETE_FALLBACK_COLOR = '#ef4444';
18
+ const SAFE_COLOR = /^[#\w(),.\-\s/%]+$/;
19
+
20
+ // ── PropertyType 列构建器 ──────────────────────────────────────────────────────
21
+
22
+ /**
23
+ * 实体属性类型字符串字面量
24
+ * 与 `@aiao/rxdb` 的 `PropertyType` 枚举值一一对应,另扩展关系类型与计算属性
25
+ */
26
+ export type PropertyTypeString =
27
+ | 'uuid'
28
+ | 'string'
29
+ | 'enum'
30
+ | 'number'
31
+ | 'integer'
32
+ | 'bigint'
33
+ | 'boolean'
34
+ | 'date'
35
+ | 'binary'
36
+ | 'stringArray'
37
+ | 'numberArray'
38
+ | 'keyValue'
39
+ | 'json'
40
+ // ── 关系类型 ──────────────────────────────────────────────
41
+ /** 一对一关系:存外键 ID,显示关联实体 displayName,可搜索下拉编辑 */
42
+ | 'oneToOne'
43
+ /** 多对一关系:存外键 ID,显示关联实体 displayName,可搜索下拉编辑 */
44
+ | 'manyToOne'
45
+ // ── 计算属性 ──────────────────────────────────────────────
46
+ /** 计算属性:只读显示,无编辑器 */
47
+ | 'computed';
48
+
49
+ /**
50
+ * 构建属性列所需的配置
51
+ */
52
+ export interface PropertyColumnConfig {
53
+ /** 字段名(record 中的键) */
54
+ field: string;
55
+ /** 列标题(默认用 field) */
56
+ title?: string;
57
+ /** 属性类型 */
58
+ type: PropertyTypeString | string;
59
+ /** 属性级只读(整列不可编辑) */
60
+ readonly?: boolean;
61
+ /** 枚举值列表(type=enum 时使用,不含图标/i18n 时的简短形式) */
62
+ enumValues?: readonly string[];
63
+ /** 枚举项完整定义(type=enum 时使用,支持 text 国际化、icon 图标;优先级高于 enumValues) */
64
+ enumItems?: readonly EnumItem[];
65
+ /** 是否允许 null(type=enum 时使用) */
66
+ nullable?: boolean;
67
+ /** 列宽(默认 'auto',uuid 类型默认 320) */
68
+ width?: number | 'auto';
69
+ /** 是否启用排序 */
70
+ sort?: boolean;
71
+ /** keyValue 属性 Schema—定义允许的键以及对应的值类型 */
72
+ keyValueSchema?: Record<string, KVSchemaEntry>;
73
+ /**
74
+ * 字段语义标注(决定展示形态与编辑器选择,不改变运行时值类型)
75
+ */
76
+ format?: FieldFormat;
77
+ /**
78
+ * 枚举/多选值的展示元数据(label / color / disabled),键是 `enum` 的子集
79
+ */
80
+ options?: FieldOptions;
81
+ /**
82
+ * 关联实体候选列表(oneToOne / manyToOne 类型使用)
83
+ *
84
+ * 每项包含 `id`(外键值)和 `displayName`(单元格显示文本)。
85
+ * 列更新时重新传入新列表即可刷新编辑器选项。
86
+ */
87
+ relatedItems?: RelatedEntityItem[];
88
+ /**
89
+ * 关联实体候选动态 provider(优先级高于 relatedItems)
90
+ *
91
+ * 每次单元格渲染或编辑器打开时调用,可返回最新的候选列表。
92
+ * 适合业务层需要运行时动态查询关联实体的场景。
93
+ */
94
+ relatedItemsProvider?: () => RelatedEntityItem[];
95
+ }
96
+
97
+ /** keyValue 模式中单个键的属性描述 */
98
+ export interface KVSchemaEntry {
99
+ /** 显示标签(默认用键名) */
100
+ label?: string;
101
+ /** 值类型(用于输入验证) */
102
+ type?: 'string' | 'number' | 'integer' | 'boolean' | 'date';
103
+ /** 是否必填 */
104
+ required?: boolean;
105
+ /** 是否可以为 null */
106
+ nullable?: boolean;
107
+ }
108
+
109
+ /**
110
+ * 格式化日期值为本地字符串
111
+ *
112
+ * @param v 待格式化的日期值
113
+ * @returns 本地化日期字符串
114
+ */
115
+ export function formatDateValue(v: unknown): string {
116
+ return formatEntityFieldValue('date', v);
117
+ }
118
+
119
+ /**
120
+ * 构建编辑器函数:非只读行使用指定编辑器名称,只读行返回 undefined
121
+ *
122
+ * 用于 ColumnDefine.editor,以便在行级别区分只读/可编辑状态。
123
+ *
124
+ * @param editorName 非只读行使用的编辑器名称
125
+ * @returns 可挂载到 ColumnDefine.editor 的编辑器回调
126
+ */
127
+ export function makeEditorForReadonly(editorName: string): ColumnDefine['editor'] {
128
+ return ((args: StylePropertyFunctionArg) => {
129
+ const record = getCellRecord(args);
130
+ return isReadonly(record) ? undefined : editorName;
131
+ }) as ColumnDefine['editor'];
132
+ }
133
+
134
+ function formatArray(v: unknown): string {
135
+ return formatEntityFieldValue('stringArray', v);
136
+ }
137
+
138
+ function formatJson(v: unknown): string {
139
+ return formatEntityFieldValue('json', v);
140
+ }
141
+
142
+ /** 数字类 format 的值域约束(min / max / step) */
143
+ interface NumericBounds {
144
+ min?: number;
145
+ max?: number;
146
+ step?: number;
147
+ }
148
+
149
+ /** 从数字类 format 提取值域约束;非数字类 format 返回 null */
150
+ function numericBoundsOf(format: FieldFormat | undefined): NumericBounds | null {
151
+ if (!format) return null;
152
+ switch (format.kind) {
153
+ case 'number':
154
+ case 'currency':
155
+ case 'percentage':
156
+ case 'duration':
157
+ case 'rating':
158
+ return {
159
+ ...(format.min === undefined ? {} : { min: format.min }),
160
+ ...(format.max === undefined ? {} : { max: format.max }),
161
+ ...(format.step === undefined ? {} : { step: format.step })
162
+ };
163
+ default:
164
+ return null;
165
+ }
166
+ }
167
+
168
+ /** 值域约束是否声明了任何一维 */
169
+ const hasNumericBounds = (bounds: NumericBounds): boolean =>
170
+ bounds.min !== undefined || bounds.max !== undefined || bounds.step !== undefined;
171
+
172
+ function stringField(r: Record<string, unknown>, field: string): string {
173
+ const v = r[field];
174
+ return v == null ? '' : String(v);
175
+ }
176
+
177
+ /** 各属性类型的默认列宽及最小/最大宽度 */
178
+ export const DEFAULT_COLUMN_SIZES: Record<string, { width: number; minWidth?: number; maxWidth?: number }> = {
179
+ boolean: { width: 80, minWidth: 60, maxWidth: 120 },
180
+ enum: { width: 120, minWidth: 80 },
181
+ date: { width: 180, minWidth: 140 },
182
+ stringArray: { width: 160, minWidth: 100 },
183
+ numberArray: { width: 160, minWidth: 100 },
184
+ keyValue: { width: 200, minWidth: 120 },
185
+ json: { width: 200, minWidth: 120 },
186
+ uuid: { width: 320, minWidth: 200, maxWidth: 400 },
187
+ number: { width: 100, minWidth: 70 },
188
+ integer: { width: 80, minWidth: 60 },
189
+ bigint: { width: 120, minWidth: 80 },
190
+ binary: { width: 160, minWidth: 100 },
191
+ oneToOne: { width: 160, minWidth: 100 },
192
+ manyToOne: { width: 160, minWidth: 100 },
193
+ computed: { width: 0, minWidth: 80 },
194
+ string: { width: 0, minWidth: 80 }
195
+ };
196
+
197
+ function applyDefaultSizes(
198
+ col: ColumnDefine & Record<string, unknown>,
199
+ type: string,
200
+ configWidth?: number | 'auto'
201
+ ): void {
202
+ const sizes = DEFAULT_COLUMN_SIZES[type];
203
+ if (!sizes) return;
204
+ if (configWidth == null && sizes.width > 0) col['width'] = sizes.width;
205
+ if (sizes.minWidth) col['minWidth'] = sizes.minWidth;
206
+ if (sizes.maxWidth) col['maxWidth'] = sizes.maxWidth;
207
+ }
208
+
209
+ /**
210
+ * 根据属性类型配置构建 VTable ColumnDefine
211
+ *
212
+ * 处理全部 11 种 PropertyType:
213
+ * - uuid / string / number / integer → 文本显示 + input-editor(非只读时)
214
+ * - enum → 文本显示 + list-editor 下拉框(非只读时)
215
+ * - boolean → switch cellType
216
+ * - date → 本地时间格式化 + date-editor 日期时间选择器(非只读时)
217
+ * - stringArray → 逗号分隔显示 + tags-editor 标签输入(非只读时)
218
+ * - numberArray → 逗号分隔显示 + number-tags-editor 数字标签输入(非只读时)
219
+ * - keyValue → JSON 字符串显示 + key-value-editor 键值对编辑器(非只读时)
220
+ * - json → JSON 字符串显示 + json-editor 文本区域(非只读时)
221
+ *
222
+ * @param config 属性列配置
223
+ * @returns 构建好的 VTable ColumnDefine
224
+ */
225
+ export function buildPropertyColumn(config: PropertyColumnConfig): ColumnDefine {
226
+ const {
227
+ field,
228
+ title,
229
+ type,
230
+ readonly: propReadonly,
231
+ enumValues,
232
+ enumItems,
233
+ nullable,
234
+ keyValueSchema,
235
+ format,
236
+ options,
237
+ sort = true
238
+ } = config;
239
+
240
+ const col: ColumnDefine & Record<string, unknown> = {
241
+ field,
242
+ title: title ?? field,
243
+ width: config.width ?? 'auto',
244
+ sort
245
+ };
246
+
247
+ col['_propertyType'] = type;
248
+
249
+ switch (type) {
250
+ case 'boolean': {
251
+ const b = col as unknown as Record<string, unknown>;
252
+ b['cellType'] = 'switch';
253
+ applyDefaultSizes(col, type, config.width);
254
+ if (propReadonly) {
255
+ b['disable'] = true;
256
+ } else {
257
+ b['disable'] = switchDisabledForReadonly;
258
+ }
259
+ break;
260
+ }
261
+
262
+ case 'enum': {
263
+ // 有 enumItems 用 enumItems;否则从 enumValues + options 派生(label / color / disabled)
264
+ const resolvedItems: EnumItem[] =
265
+ enumItems ?
266
+ [...enumItems]
267
+ : (enumValues ?? []).map(v => {
268
+ const option = options?.[v];
269
+ return {
270
+ value: v,
271
+ ...(option?.label === undefined ? {} : { text: option.label }),
272
+ ...(option?.color === undefined ? {} : { color: option.color }),
273
+ ...(option?.disabled === undefined ? {} : { disabled: option.disabled })
274
+ };
275
+ });
276
+ const textMap = new Map(resolvedItems.map(i => [i.value, i.text ?? i.value]));
277
+ col['fieldFormat'] = (r: Record<string, unknown>) => {
278
+ const v = stringField(r, field);
279
+ return textMap.get(v) ?? v;
280
+ };
281
+ applyDefaultSizes(col, type, config.width);
282
+ if (!propReadonly) {
283
+ const allItems: EnumItem[] = nullable ? [{ value: '', text: '(空)' }, ...resolvedItems] : resolvedItems;
284
+ const enumInst = new EnumEditor(allItems);
285
+ col['editor'] = ((args: StylePropertyFunctionArg) =>
286
+ isReadonly(getCellRecord(args)) ? undefined : enumInst) as ColumnDefine['editor'];
287
+ if (nullable) col['nullable'] = true;
288
+ }
289
+ break;
290
+ }
291
+
292
+ case 'date':
293
+ col['fieldFormat'] = (r: Record<string, unknown>) => formatEntityFieldValue('date', r[field], format);
294
+ applyDefaultSizes(col, type, config.width);
295
+ if (!propReadonly) col['editor'] = makeEditorForReadonly('date-editor');
296
+ break;
297
+
298
+ case 'stringArray': {
299
+ const textMap = new Map((enumValues ?? []).map(v => [v, options?.[v]?.label ?? v]));
300
+ col['fieldFormat'] = (r: Record<string, unknown>) => {
301
+ const v = r[field];
302
+ const items =
303
+ Array.isArray(v) ? v
304
+ : v == null || v === '' ? []
305
+ : String(v).split(',');
306
+ return items.map(item => textMap.get(String(item).trim()) ?? String(item).trim()).join(', ');
307
+ };
308
+ applyDefaultSizes(col, type, config.width);
309
+ if (!propReadonly) {
310
+ if (enumValues && enumValues.length > 0) {
311
+ const optionList = enumValues.map(v => {
312
+ const option = options?.[v];
313
+ return {
314
+ id: v,
315
+ name: option?.label ?? v,
316
+ ...(option?.color === undefined ? {} : { color: option.color }),
317
+ ...(option?.disabled === undefined ? {} : { disabled: option.disabled })
318
+ };
319
+ });
320
+ const multiInst = new MultiSelectEditor(optionList);
321
+ col['editor'] = ((args: StylePropertyFunctionArg) =>
322
+ isReadonly(getCellRecord(args)) ? undefined : multiInst) as ColumnDefine['editor'];
323
+ } else {
324
+ col['editor'] = makeEditorForReadonly('tags-editor');
325
+ }
326
+ }
327
+ break;
328
+ }
329
+
330
+ case 'numberArray':
331
+ col['fieldFormat'] = (r: Record<string, unknown>) => formatArray(r[field]);
332
+ applyDefaultSizes(col, type, config.width);
333
+ if (!propReadonly) col['editor'] = makeEditorForReadonly('number-tags-editor');
334
+ break;
335
+
336
+ case 'keyValue':
337
+ col['fieldFormat'] = (r: Record<string, unknown>) => formatJson(r[field]);
338
+ applyDefaultSizes(col, type, config.width);
339
+ if (!propReadonly) {
340
+ if (keyValueSchema) {
341
+ const kvInst = new KeyValueEditor(keyValueSchema);
342
+ col['editor'] = ((args: StylePropertyFunctionArg) =>
343
+ isReadonly(getCellRecord(args)) ? undefined : kvInst) as ColumnDefine['editor'];
344
+ } else {
345
+ col['editor'] = makeEditorForReadonly('key-value-editor');
346
+ }
347
+ }
348
+ break;
349
+
350
+ case 'json':
351
+ col['fieldFormat'] = (r: Record<string, unknown>) => formatJson(r[field]);
352
+ applyDefaultSizes(col, type, config.width);
353
+ if (!propReadonly) col['editor'] = makeEditorForReadonly('json-editor');
354
+ break;
355
+
356
+ // uuid / string / number / integer / 未知类型
357
+ case 'uuid':
358
+ col['fieldFormat'] = (r: Record<string, unknown>) => stringField(r, field);
359
+ applyDefaultSizes(col, type, config.width);
360
+ if (!propReadonly) col['editor'] = makeEditorForReadonly('uuid-editor');
361
+ break;
362
+
363
+ case 'number':
364
+ col['fieldFormat'] = (r: Record<string, unknown>) => formatEntityFieldValue('number', r[field], format);
365
+ applyDefaultSizes(col, type, config.width);
366
+ if (!propReadonly) {
367
+ const bounds = numericBoundsOf(format);
368
+ if (bounds && hasNumericBounds(bounds)) {
369
+ const inst = new NumberEditor(false, bounds);
370
+ col['editor'] = ((args: StylePropertyFunctionArg) =>
371
+ isReadonly(getCellRecord(args)) ? undefined : inst) as ColumnDefine['editor'];
372
+ } else {
373
+ col['editor'] = makeEditorForReadonly('number-editor');
374
+ }
375
+ }
376
+ break;
377
+
378
+ case 'integer':
379
+ col['fieldFormat'] = (r: Record<string, unknown>) => formatEntityFieldValue('integer', r[field], format);
380
+ applyDefaultSizes(col, type, config.width);
381
+ if (!propReadonly) {
382
+ const bounds = numericBoundsOf(format);
383
+ if (bounds && hasNumericBounds(bounds)) {
384
+ const inst = new NumberEditor(true, bounds);
385
+ col['editor'] = ((args: StylePropertyFunctionArg) =>
386
+ isReadonly(getCellRecord(args)) ? undefined : inst) as ColumnDefine['editor'];
387
+ } else {
388
+ col['editor'] = makeEditorForReadonly('integer-editor');
389
+ }
390
+ }
391
+ break;
392
+
393
+ case 'bigint':
394
+ col['fieldFormat'] = (r: Record<string, unknown>) => stringField(r, field);
395
+ applyDefaultSizes(col, type, config.width);
396
+ if (!propReadonly) col['editor'] = makeEditorForReadonly('bigint-editor');
397
+ break;
398
+
399
+ case 'binary':
400
+ col['fieldFormat'] = (r: Record<string, unknown>) => formatEntityFieldValue('binary', r[field]);
401
+ applyDefaultSizes(col, type, config.width);
402
+ // 二进制列只读展示(表格内不做字节级编辑,表单里用 hex 编辑)
403
+ break;
404
+
405
+ // ── 关系类型 ─────────────────────────────────────────────────────────
406
+ case 'oneToOne':
407
+ case 'manyToOne': {
408
+ // provider 优先,其次是静态 relatedItems;单元格渲染时实时调用以反映最新数据
409
+ const provider = config.relatedItemsProvider;
410
+ const staticItems = config.relatedItems ?? [];
411
+ col['fieldFormat'] = (r: Record<string, unknown>) => {
412
+ const v = r[field];
413
+ if (v == null || v === '') return '';
414
+ const id = String(v);
415
+ const items = provider ? provider() : staticItems;
416
+ return items.find(i => i.id === id)?.displayName ?? id;
417
+ };
418
+ applyDefaultSizes(col, type, config.width);
419
+ if (!propReadonly) {
420
+ // 将 provider 或静态 items 传入 RelationEditor,打开时动态格取
421
+ const relEditor = new RelationEditor(provider ?? staticItems, nullable ?? false);
422
+ col['editor'] = ((args: StylePropertyFunctionArg) =>
423
+ isReadonly(getCellRecord(args)) ? undefined : relEditor) as ColumnDefine['editor'];
424
+ }
425
+ break;
426
+ }
427
+
428
+ // ── 计算属性 ─────────────────────────────────────────────────────────
429
+ case 'computed':
430
+ col['fieldFormat'] = (r: Record<string, unknown>) => stringField(r, field);
431
+ applyDefaultSizes(col, type, config.width);
432
+ // 无编辑器,始终只读
433
+ break;
434
+
435
+ // string / 未知类型
436
+ default:
437
+ col['fieldFormat'] = (r: Record<string, unknown>) => stringField(r, field);
438
+ applyDefaultSizes(col, 'string', config.width);
439
+ if (!propReadonly) {
440
+ switch (format?.kind) {
441
+ case 'color': {
442
+ const colorInst = new ColorEditor();
443
+ col['editor'] = ((args: StylePropertyFunctionArg) =>
444
+ isReadonly(getCellRecord(args)) ? undefined : colorInst) as ColumnDefine['editor'];
445
+ break;
446
+ }
447
+ case 'url': {
448
+ const urlInst = new TextFormatEditor('url', format.schemes);
449
+ col['editor'] = ((args: StylePropertyFunctionArg) =>
450
+ isReadonly(getCellRecord(args)) ? undefined : urlInst) as ColumnDefine['editor'];
451
+ break;
452
+ }
453
+ case 'email': {
454
+ const emailInst = new TextFormatEditor('email');
455
+ col['editor'] = ((args: StylePropertyFunctionArg) =>
456
+ isReadonly(getCellRecord(args)) ? undefined : emailInst) as ColumnDefine['editor'];
457
+ break;
458
+ }
459
+ case 'phone': {
460
+ const phoneInst = new TextFormatEditor('phone');
461
+ col['editor'] = ((args: StylePropertyFunctionArg) =>
462
+ isReadonly(getCellRecord(args)) ? undefined : phoneInst) as ColumnDefine['editor'];
463
+ break;
464
+ }
465
+ case 'multilineText':
466
+ case 'richText':
467
+ case 'code':
468
+ col['editor'] = makeEditorForReadonly('vtable-textarea-editor');
469
+ break;
470
+ default:
471
+ col['editor'] = disabledEditorForReadonly;
472
+ }
473
+ }
474
+ break;
475
+ }
476
+
477
+ return col as ColumnDefine;
478
+ }
479
+
480
+ /**
481
+ * 将 input-editor 输入的字符串值解析回属性原始类型
482
+ *
483
+ * 在 cellChanged 事件处理器中调用此函数,将用户输入转成正确的 JS 类型后再保存。
484
+ *
485
+ * @param type 属性类型
486
+ * @param raw 编辑器输入的原始值
487
+ * @returns 解析后的属性值
488
+ */
489
+ export function parsePropertyColumnValue(type: PropertyTypeString | string, raw: unknown): unknown {
490
+ return parseEntityFieldValue(type, raw);
491
+ }
492
+
493
+ const resolveDeleteColor = (): string => {
494
+ const style = getDocumentRootStyle();
495
+ const errorColor = getCSSVariableValue('--color-error', DELETE_FALLBACK_COLOR, style);
496
+ const color = getCSSVariableValue('--entity-table-delete', errorColor, style);
497
+ return SAFE_COLOR.test(color) ? color : DELETE_FALLBACK_COLOR;
498
+ };
499
+
500
+ const createDeleteSvg = (color: string) =>
501
+ `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>`;
502
+
503
+ const VIEW_COLOR = '#3b82f6';
504
+ const VIEW_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="${VIEW_COLOR}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>`;
505
+
506
+ /** VTable 回调参数中实际携带 table 实例 */
507
+ type CellCallbackArgs = StylePropertyFunctionArg & {
508
+ table: { getRecordByCell: (col: number, row: number) => unknown };
509
+ };
510
+
511
+ type ColumnRecord = Record<string, unknown> & {
512
+ _readonly?: boolean;
513
+ _isAddRow?: boolean;
514
+ };
515
+
516
+ /**
517
+ * 通过 VTable 回调参数安全获取当前行记录
518
+ *
519
+ * @param args VTable 回调参数
520
+ * @returns 当前行记录;无法获取时返回 undefined
521
+ */
522
+ export const getCellRecord = (args: StylePropertyFunctionArg): ColumnRecord | undefined => {
523
+ const casted = args as CellCallbackArgs;
524
+ return casted.table?.getRecordByCell(casted.col, casted.row) as ColumnRecord | undefined;
525
+ };
526
+
527
+ /**
528
+ * switch 禁用状态判断回调
529
+ *
530
+ * @param args VTable 回调参数(用于获取当前行记录)
531
+ * @returns true 表示禁用 switch
532
+ */
533
+ export type SwitchDisableCallback = (args: StylePropertyFunctionArg) => boolean;
534
+
535
+ /**
536
+ * 检查记录是否为只读行
537
+ *
538
+ * @param record 待检查的行记录
539
+ * @returns 是否为只读行
540
+ */
541
+ export const isReadonly = (record: unknown): boolean => {
542
+ return (record as ColumnRecord | undefined)?._readonly === true;
543
+ };
544
+
545
+ /**
546
+ * 检查记录是否为只读行或新增模板行
547
+ *
548
+ * @param record 待检查的行记录
549
+ * @returns 是否为只读行或新增模板行
550
+ */
551
+ export const isReadonlyOrAddRow = (record: unknown): boolean => {
552
+ const r = record as ColumnRecord | undefined;
553
+ return r?._readonly === true || r?._isAddRow === true;
554
+ };
555
+
556
+ /**
557
+ * 生成查看按钮的图标选项(仅 SVG,避免 text 撑满剩余宽度)
558
+ *
559
+ * @returns 图标选项数组
560
+ */
561
+ export function makeViewIcons(): unknown[] {
562
+ return [
563
+ {
564
+ type: 'svg',
565
+ svg: VIEW_SVG,
566
+ name: 'view-action',
567
+ positionType: 'contentLeft',
568
+ width: 16,
569
+ height: 16,
570
+ marginRight: 10,
571
+ cursor: 'pointer',
572
+ hover: { width: 20, height: 20, bgColor: 'rgba(59,130,246,0.12)' }
573
+ }
574
+ ];
575
+ }
576
+
577
+ /**
578
+ * 生成删除按钮的图标选项(SVG 图标 + 文字标签)
579
+ *
580
+ * @param label 删除按钮文字
581
+ * @returns 图标选项数组
582
+ */
583
+ export function makeDeleteIcons(label: string): unknown[] {
584
+ const deleteColor = resolveDeleteColor();
585
+
586
+ return [
587
+ {
588
+ type: 'svg',
589
+ svg: createDeleteSvg(deleteColor),
590
+ name: 'delete-action',
591
+ positionType: 'contentLeft',
592
+ width: 14,
593
+ height: 14,
594
+ marginRight: 4,
595
+ cursor: 'pointer',
596
+ hover: { width: 20, height: 20, bgColor: 'rgba(239,68,68,0.12)' }
597
+ },
598
+ {
599
+ type: 'text',
600
+ content: label,
601
+ name: 'delete-action',
602
+ positionType: 'contentLeft',
603
+ cursor: 'pointer',
604
+ style: { fill: deleteColor, fontSize: 12 },
605
+ hover: { bgColor: 'rgba(239,68,68,0.12)' }
606
+ }
607
+ ];
608
+ }
609
+
610
+ /** 不可编辑的行:禁用 editor */
611
+ export const disabledEditorForReadonly = ((args: StylePropertyFunctionArg) => {
612
+ const record = getCellRecord(args);
613
+ return isReadonly(record) ? undefined : 'input-editor';
614
+ }) as ColumnDefine['editor'];
615
+
616
+ /** 禁用 switch:只读行不可操作 */
617
+ export const switchDisabledForReadonly: SwitchDisableCallback = (args: StylePropertyFunctionArg) => {
618
+ const record = getCellRecord(args);
619
+ return isReadonly(record);
620
+ };
621
+
622
+ /**
623
+ * 判定某一行能否删除;`actionsColumn` 逐行调用
624
+ */
625
+ export type RecordDeletePredicate = (record: Readonly<Record<string, unknown>>) => boolean;
626
+
627
+ /**
628
+ * 操作列:可选包含查看(view)和删除按钮
629
+ *
630
+ * @param title 列标题
631
+ * @param deleteLabel 删除按钮文案
632
+ * @param canDelete 逐行判定是否出删除按钮;与行是否只读无关(`update: 'system'` 的实体行只读仍可删)
633
+ * @param viewLabel 查看按钮文案(可选,不传则只显示删除按钮,不可删的行因此无图标)
634
+ * @returns 操作列定义
635
+ */
636
+ export function actionsColumn(
637
+ title: string,
638
+ deleteLabel: string,
639
+ canDelete: RecordDeletePredicate,
640
+ viewLabel?: string
641
+ ): ColumnDefine {
642
+ const width = viewLabel ? 130 : 100;
643
+ return {
644
+ field: 'actions',
645
+ title,
646
+ width,
647
+ maxWidth: width,
648
+ minWidth: width,
649
+ disableSelect: true,
650
+ disableHeaderSelect: true,
651
+ icon: ((args: StylePropertyFunctionArg) => {
652
+ const record = getCellRecord(args);
653
+ const viewIcons = viewLabel ? makeViewIcons() : [];
654
+ return record && canDelete(record) ? [...viewIcons, ...makeDeleteIcons(deleteLabel)] : viewIcons;
655
+ }) as ColumnDefine['icon']
656
+ };
657
+ }