@aiao/rxdb-model 0.0.19 → 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.
- package/LICENSE +1 -1
- package/README.md +43 -6
- package/dist/cn.d.ts +19 -0
- package/dist/cn.d.ts.map +1 -0
- package/dist/entity-capabilities.d.ts +31 -0
- package/dist/entity-capabilities.d.ts.map +1 -0
- package/dist/entity-detail/detail-tabs.d.ts +13 -2
- package/dist/entity-detail/detail-tabs.d.ts.map +1 -1
- package/dist/entity-detail/index.d.ts +5 -0
- package/dist/entity-detail/index.d.ts.map +1 -1
- package/dist/entity-detail/interfaces.d.ts +29 -1
- package/dist/entity-detail/interfaces.d.ts.map +1 -1
- package/dist/entity-field.utils.d.ts +60 -2
- package/dist/entity-field.utils.d.ts.map +1 -1
- package/dist/entity-form/form-data.d.ts +22 -1
- package/dist/entity-form/form-data.d.ts.map +1 -1
- package/dist/entity-form/form-fields.d.ts +25 -2
- package/dist/entity-form/form-fields.d.ts.map +1 -1
- package/dist/entity-form/form-validation.d.ts +20 -2
- package/dist/entity-form/form-validation.d.ts.map +1 -1
- package/dist/entity-form/index.d.ts +5 -0
- package/dist/entity-form/index.d.ts.map +1 -1
- package/dist/entity-form/interfaces.d.ts +65 -1
- package/dist/entity-form/interfaces.d.ts.map +1 -1
- package/dist/entity-list/manual-order-list.d.ts +99 -0
- package/dist/entity-list/manual-order-list.d.ts.map +1 -0
- package/dist/entity-table/columns/build-editable-columns.d.ts +26 -4
- package/dist/entity-table/columns/build-editable-columns.d.ts.map +1 -1
- package/dist/entity-table/columns/column-utils.d.ts +78 -12
- package/dist/entity-table/columns/column-utils.d.ts.map +1 -1
- package/dist/entity-table/editors/color-editor.d.ts +23 -0
- package/dist/entity-table/editors/color-editor.d.ts.map +1 -0
- package/dist/entity-table/editors/date-editor.d.ts +6 -1
- package/dist/entity-table/editors/date-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/enum-editor.d.ts +11 -1
- package/dist/entity-table/editors/enum-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/global-overlay-editor.d.ts +27 -4
- package/dist/entity-table/editors/global-overlay-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/icon-list-editor.d.ts +13 -1
- package/dist/entity-table/editors/icon-list-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/json-editor.d.ts +6 -1
- package/dist/entity-table/editors/json-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/key-value-editor.d.ts +12 -2
- package/dist/entity-table/editors/key-value-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/lucide-svg.d.ts +3 -2
- package/dist/entity-table/editors/lucide-svg.d.ts.map +1 -1
- package/dist/entity-table/editors/multiselect-editor.d.ts +24 -6
- package/dist/entity-table/editors/multiselect-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/number-editor.d.ts +33 -3
- package/dist/entity-table/editors/number-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/relation-editor.d.ts +14 -2
- package/dist/entity-table/editors/relation-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/safe-list-editor.d.ts +1 -0
- package/dist/entity-table/editors/safe-list-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/tags-editor.d.ts +12 -1
- package/dist/entity-table/editors/tags-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/text-format-editor.d.ts +32 -0
- package/dist/entity-table/editors/text-format-editor.d.ts.map +1 -0
- package/dist/entity-table/editors/uuid-editor.d.ts +14 -1
- package/dist/entity-table/editors/uuid-editor.d.ts.map +1 -1
- package/dist/entity-table/index.d.ts +13 -3
- package/dist/entity-table/index.d.ts.map +1 -1
- package/dist/entity-table/interfaces.d.ts +13 -0
- package/dist/entity-table/interfaces.d.ts.map +1 -1
- package/dist/entity-table/vtable/table-clipboard.d.ts +31 -2
- package/dist/entity-table/vtable/table-clipboard.d.ts.map +1 -1
- package/dist/entity-table/vtable/table-factory.d.ts +25 -5
- package/dist/entity-table/vtable/table-factory.d.ts.map +1 -1
- package/dist/entity-table/vtable/table-keyboard.d.ts +18 -1
- package/dist/entity-table/vtable/table-keyboard.d.ts.map +1 -1
- package/dist/entity-table/vtable/table-operations.d.ts +44 -3
- package/dist/entity-table/vtable/table-operations.d.ts.map +1 -1
- package/dist/entity-table/vtable/table-row-move.d.ts +88 -0
- package/dist/entity-table/vtable/table-row-move.d.ts.map +1 -0
- package/dist/entity-table/vtable/table-theme.d.ts +47 -2
- package/dist/entity-table/vtable/table-theme.d.ts.map +1 -1
- package/dist/entity-table/vtable/table-tooltip.d.ts +7 -1
- package/dist/entity-table/vtable/table-tooltip.d.ts.map +1 -1
- package/dist/entity-table/vtable/vtable-compat.d.ts +33 -5
- package/dist/entity-table/vtable/vtable-compat.d.ts.map +1 -1
- package/dist/entity-value.utils.d.ts +50 -2
- package/dist/entity-value.utils.d.ts.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4392 -64471
- package/dist/query-builder/models/config.interface.d.ts +2 -2
- package/dist/query-builder/models/operator.interface.d.ts +6 -2
- package/dist/query-builder/models/operator.interface.d.ts.map +1 -1
- package/dist/query-builder/models/query-builder-state.d.ts +31 -2
- package/dist/query-builder/models/query-builder-state.d.ts.map +1 -1
- package/dist/query-builder/models/validation.interface.d.ts +2 -2
- package/dist/query-builder/services/operator-registry.service.d.ts +13 -3
- package/dist/query-builder/services/operator-registry.service.d.ts.map +1 -1
- package/dist/query-builder/services/query-builder.service.d.ts +16 -6
- package/dist/query-builder/services/query-builder.service.d.ts.map +1 -1
- package/dist/query-builder/services/query-converter.service.d.ts +10 -2
- package/dist/query-builder/services/query-converter.service.d.ts.map +1 -1
- package/dist/query-builder/services/schema-parser.service.d.ts +10 -2
- package/dist/query-builder/services/schema-parser.service.d.ts.map +1 -1
- package/dist/query-builder/services/validation.service.d.ts +34 -4
- package/dist/query-builder/services/validation.service.d.ts.map +1 -1
- package/dist/query-builder/utils/date.d.ts +12 -0
- package/dist/query-builder/utils/date.d.ts.map +1 -1
- package/dist/query-builder/utils/field-tree.d.ts +7 -1
- package/dist/query-builder/utils/field-tree.d.ts.map +1 -1
- package/dist/query-builder/utils/id.d.ts +2 -0
- package/dist/query-builder/utils/id.d.ts.map +1 -1
- package/dist/query-builder/utils/metadata-field-extractor.d.ts +15 -3
- package/dist/query-builder/utils/metadata-field-extractor.d.ts.map +1 -1
- package/dist/query-builder/utils/popover-position.d.ts +11 -0
- package/dist/query-builder/utils/popover-position.d.ts.map +1 -1
- package/dist/query-builder/utils/schema-factory.d.ts +7 -3
- package/dist/query-builder/utils/structural-equal.d.ts +4 -0
- package/dist/query-builder/utils/structural-equal.d.ts.map +1 -1
- package/dist/query-builder/utils/value-input.d.ts +21 -1
- package/dist/query-builder/utils/value-input.d.ts.map +1 -1
- package/dist/structural-equal.d.ts +5 -1
- package/dist/structural-equal.d.ts.map +1 -1
- package/package.json +34 -10
- package/src/cn.ts +21 -0
- package/src/entity-capabilities.ts +37 -0
- package/src/entity-detail/detail-tabs.ts +48 -0
- package/src/entity-detail/index.ts +7 -0
- package/src/entity-detail/interfaces.ts +55 -0
- package/src/entity-field.utils.ts +205 -0
- package/src/entity-form/form-data.ts +83 -0
- package/src/entity-form/form-fields.ts +48 -0
- package/src/entity-form/form-validation.ts +34 -0
- package/src/entity-form/index.ts +9 -0
- package/src/entity-form/interfaces.ts +116 -0
- package/src/entity-list/manual-order-list.ts +159 -0
- package/src/entity-table/columns/build-editable-columns.ts +184 -0
- package/src/entity-table/columns/column-utils.ts +657 -0
- package/src/entity-table/editors/color-editor.ts +239 -0
- package/src/entity-table/editors/date-editor.ts +169 -0
- package/src/entity-table/editors/enum-editor.ts +167 -0
- package/src/entity-table/editors/global-overlay-editor.ts +277 -0
- package/src/entity-table/editors/icon-list-editor.ts +130 -0
- package/src/entity-table/editors/json-editor.ts +212 -0
- package/src/entity-table/editors/key-value-editor.ts +595 -0
- package/src/entity-table/editors/lucide-svg.ts +78 -0
- package/src/entity-table/editors/multiselect-editor.ts +171 -0
- package/src/entity-table/editors/number-editor.ts +322 -0
- package/src/entity-table/editors/relation-editor.ts +316 -0
- package/src/entity-table/editors/safe-list-editor.ts +17 -0
- package/src/entity-table/editors/tags-editor.ts +239 -0
- package/src/entity-table/editors/text-format-editor.ts +220 -0
- package/src/entity-table/editors/uuid-editor.ts +198 -0
- package/src/entity-table/index.ts +96 -0
- package/src/entity-table/interfaces.ts +72 -0
- package/src/entity-table/vtable/table-clipboard.ts +419 -0
- package/src/entity-table/vtable/table-factory.ts +117 -0
- package/src/entity-table/vtable/table-keyboard.ts +89 -0
- package/src/entity-table/vtable/table-operations.ts +123 -0
- package/src/entity-table/vtable/table-row-move.ts +122 -0
- package/src/entity-table/vtable/table-theme.ts +181 -0
- package/src/entity-table/vtable/table-tooltip.ts +70 -0
- package/src/entity-table/vtable/vtable-compat.ts +84 -0
- package/src/entity-value.utils.ts +456 -0
- package/src/index.ts +57 -0
- package/src/query-builder/models/config.interface.ts +31 -0
- package/src/query-builder/models/index.ts +8 -0
- package/src/query-builder/models/operator.interface.ts +191 -0
- package/src/query-builder/models/query-builder-state.ts +222 -0
- package/src/query-builder/models/validation.interface.ts +184 -0
- package/src/query-builder/services/index.ts +9 -0
- package/src/query-builder/services/operator-registry.service.ts +243 -0
- package/src/query-builder/services/query-builder.service.ts +568 -0
- package/src/query-builder/services/query-converter.service.ts +153 -0
- package/src/query-builder/services/schema-parser.service.ts +309 -0
- package/src/query-builder/services/validation.service.ts +390 -0
- package/src/query-builder/utils/date.ts +35 -0
- package/src/query-builder/utils/field-tree.ts +400 -0
- package/src/query-builder/utils/id.ts +15 -0
- package/src/query-builder/utils/index.ts +12 -0
- package/src/query-builder/utils/metadata-field-extractor.ts +460 -0
- package/src/query-builder/utils/popover-position.ts +59 -0
- package/src/query-builder/utils/schema-factory.ts +75 -0
- package/src/query-builder/utils/structural-equal.ts +5 -0
- package/src/query-builder/utils/value-input.ts +105 -0
- package/src/structural-equal.ts +49 -0
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import type { EntityMetadata, EntityRelationMetadata, KeyValuePropertyMetadata } from '@aiao/rxdb';
|
|
2
|
+
import { PropertyType, RelationKind } from '@aiao/rxdb';
|
|
3
|
+
import type { FieldMetadata, PropertyType as QBPropertyType } from '../models/query-builder-state.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 模型信息接口
|
|
7
|
+
*/
|
|
8
|
+
export interface ModelInfo {
|
|
9
|
+
/** 实体名称 */
|
|
10
|
+
name: string;
|
|
11
|
+
/** 实体显示名称 */
|
|
12
|
+
displayName: string;
|
|
13
|
+
|
|
14
|
+
/** 实体类构造函数 */
|
|
15
|
+
entityClass: new (...args: never[]) => unknown;
|
|
16
|
+
/** RxDB 实体元数据 */
|
|
17
|
+
metadata: EntityMetadata;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 查询示例接口
|
|
22
|
+
*/
|
|
23
|
+
export interface QueryExample {
|
|
24
|
+
/** 示例名称 */
|
|
25
|
+
name: string;
|
|
26
|
+
/** 示例描述 */
|
|
27
|
+
description: string;
|
|
28
|
+
/** 示例对应的模型名称 */
|
|
29
|
+
modelName: string;
|
|
30
|
+
/** 示例查询内容(结构由具体模型决定) */
|
|
31
|
+
query: unknown;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 字段提取器配置
|
|
36
|
+
*/
|
|
37
|
+
export interface FieldExtractorConfig {
|
|
38
|
+
/** 关系查询深度,默认 4 */
|
|
39
|
+
relationQueryDeep?: number;
|
|
40
|
+
/** 系统字段列表,默认 ['id', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'] */
|
|
41
|
+
systemFields?: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 关系深度配置(默认值)
|
|
46
|
+
*/
|
|
47
|
+
const DEFAULT_RELATION_QUERY_DEEP = 4;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 系统字段列表(默认值)
|
|
51
|
+
*/
|
|
52
|
+
const DEFAULT_SYSTEM_FIELDS = ['id', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 将 PropertyType 映射到 FieldMetadata 的 type
|
|
56
|
+
*
|
|
57
|
+
* @param type - RxDB 属性类型
|
|
58
|
+
* @returns QueryBuilder 属性类型
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* mapPropertyType(PropertyType.string) // 'string'
|
|
63
|
+
* mapPropertyType(PropertyType.number) // 'number'
|
|
64
|
+
* mapPropertyType(PropertyType.boolean) // 'boolean'
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export function mapPropertyType(type: PropertyType): QBPropertyType {
|
|
68
|
+
switch (type) {
|
|
69
|
+
case PropertyType.uuid:
|
|
70
|
+
return 'uuid';
|
|
71
|
+
case PropertyType.string:
|
|
72
|
+
return 'string';
|
|
73
|
+
case PropertyType.enum:
|
|
74
|
+
return 'enum';
|
|
75
|
+
case PropertyType.number:
|
|
76
|
+
case PropertyType.integer:
|
|
77
|
+
return 'number';
|
|
78
|
+
case PropertyType.boolean:
|
|
79
|
+
return 'boolean';
|
|
80
|
+
case PropertyType.date:
|
|
81
|
+
return 'date';
|
|
82
|
+
case PropertyType.stringArray:
|
|
83
|
+
case PropertyType.numberArray:
|
|
84
|
+
return 'array';
|
|
85
|
+
case PropertyType.json:
|
|
86
|
+
return 'object';
|
|
87
|
+
case PropertyType.keyValue:
|
|
88
|
+
return 'keyValue';
|
|
89
|
+
default:
|
|
90
|
+
return 'string';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 获取关系需要忽略的键(避免循环引用)
|
|
96
|
+
*
|
|
97
|
+
* @param relation - 实体关系元数据
|
|
98
|
+
* @returns 需要忽略的键列表
|
|
99
|
+
*
|
|
100
|
+
* @remarks
|
|
101
|
+
* 对于 ONE_TO_MANY 和 MANY_TO_MANY 关系,返回 mappedProperty(如果存在)
|
|
102
|
+
* 以避免无限递归
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```typescript
|
|
106
|
+
* // ONE_TO_MANY: User.orders -> Order.owner
|
|
107
|
+
* getRelationIgnoreKeys(ordersRelation) // ['owner']
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
export function getRelationIgnoreKeys(relation: EntityRelationMetadata): string[] {
|
|
111
|
+
if (relation.kind === RelationKind.ONE_TO_MANY || relation.kind === RelationKind.MANY_TO_MANY) {
|
|
112
|
+
return [relation.mappedProperty];
|
|
113
|
+
}
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 从 EntityMetadata 提取字段(支持递归关系)
|
|
119
|
+
*
|
|
120
|
+
* @param metadata - 实体元数据
|
|
121
|
+
* @param allModels - 所有模型的映射表
|
|
122
|
+
* @param config - 字段提取器配置
|
|
123
|
+
* @returns 字段元数据列表
|
|
124
|
+
*
|
|
125
|
+
* @remarks
|
|
126
|
+
* 此函数会递归提取实体的所有字段,包括:
|
|
127
|
+
* 1. 系统字段(id, createdAt, updatedAt 等)
|
|
128
|
+
* 2. 实体属性字段
|
|
129
|
+
* 3. 外键字段
|
|
130
|
+
* 4. 关系字段(递归,直到达到深度限制)
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```typescript
|
|
134
|
+
* const fields = extractFieldsFromMetadata(
|
|
135
|
+
* userMetadata,
|
|
136
|
+
* modelMap,
|
|
137
|
+
* { relationQueryDeep: 3 }
|
|
138
|
+
* );
|
|
139
|
+
* // 返回: [
|
|
140
|
+
* // { name: 'id', displayName: 'ID', type: 'string' },
|
|
141
|
+
* // { name: 'name', displayName: '姓名', type: 'string' },
|
|
142
|
+
* // { name: 'orders', displayName: '订单 (ONE_TO_MANY)', type: 'relation', ... },
|
|
143
|
+
* // { name: 'orders.id', displayName: '订单.ID', type: 'string' },
|
|
144
|
+
* // ...
|
|
145
|
+
* // ]
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
export function extractFieldsFromMetadata(
|
|
149
|
+
metadata: EntityMetadata,
|
|
150
|
+
allModels: Map<string, ModelInfo>,
|
|
151
|
+
config: FieldExtractorConfig = {}
|
|
152
|
+
): FieldMetadata[] {
|
|
153
|
+
return extractFieldsRecursive(metadata, allModels, config, metadata, [], '', new Set(), 0);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 递归提取字段的内部实现
|
|
158
|
+
*/
|
|
159
|
+
function extractFieldsRecursive(
|
|
160
|
+
metadata: EntityMetadata,
|
|
161
|
+
allModels: Map<string, ModelInfo>,
|
|
162
|
+
config: FieldExtractorConfig,
|
|
163
|
+
firstMetadata: EntityMetadata,
|
|
164
|
+
ignoreKeys: string[],
|
|
165
|
+
prefix: string,
|
|
166
|
+
visitedEntities: Set<string>,
|
|
167
|
+
depth: number
|
|
168
|
+
): FieldMetadata[] {
|
|
169
|
+
const fields: FieldMetadata[] = [];
|
|
170
|
+
const relationQueryDeep = config.relationQueryDeep ?? DEFAULT_RELATION_QUERY_DEEP;
|
|
171
|
+
const systemFields = config.systemFields ?? DEFAULT_SYSTEM_FIELDS;
|
|
172
|
+
|
|
173
|
+
// 防止循环引用
|
|
174
|
+
if (visitedEntities.has(metadata.name)) {
|
|
175
|
+
return fields;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 使用 propertyMap 做 O(1) 名称查找
|
|
179
|
+
const propertyByName = metadata.propertyMap;
|
|
180
|
+
|
|
181
|
+
// 1. 添加系统字段(仅顶层)
|
|
182
|
+
if (!prefix) {
|
|
183
|
+
for (const sysField of systemFields) {
|
|
184
|
+
const sysProp = propertyByName.get(sysField);
|
|
185
|
+
if (sysProp) {
|
|
186
|
+
fields.push({
|
|
187
|
+
name: sysField,
|
|
188
|
+
displayName: sysProp.displayName ?? sysField,
|
|
189
|
+
type: mapPropertyType(sysProp.type as PropertyType),
|
|
190
|
+
nullable: sysProp.nullable ?? false
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 2. 添加实体属性字段
|
|
197
|
+
for (const [key, property] of metadata.propertyMap) {
|
|
198
|
+
const fieldName = prefix ? `${prefix}.${key}` : key;
|
|
199
|
+
const field: FieldMetadata = {
|
|
200
|
+
name: fieldName,
|
|
201
|
+
displayName: property.displayName ?? key,
|
|
202
|
+
type: mapPropertyType(property.type as PropertyType),
|
|
203
|
+
nullable: property.nullable ?? false
|
|
204
|
+
};
|
|
205
|
+
if (property.type === PropertyType.enum && Array.isArray(property.enum)) {
|
|
206
|
+
field.enum = property.enum;
|
|
207
|
+
}
|
|
208
|
+
if (property.type === PropertyType.keyValue) {
|
|
209
|
+
const kvProperties = (property as unknown as { properties: KeyValuePropertyMetadata[] }).properties;
|
|
210
|
+
if (kvProperties?.length > 0) {
|
|
211
|
+
field.keyValueFields = extractKeyValueFields(kvProperties);
|
|
212
|
+
for (const kvField of field.keyValueFields) {
|
|
213
|
+
fields.push({
|
|
214
|
+
...kvField,
|
|
215
|
+
name: `${fieldName}.${kvField.name}`,
|
|
216
|
+
displayName: `${field.displayName}.${kvField.displayName}`
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
fields.push(field);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 3. 添加外键字段(使用 Set 避免 O(n²) 重复检测)
|
|
225
|
+
const existingNames = new Set<string>();
|
|
226
|
+
for (const f of fields) existingNames.add(f.name);
|
|
227
|
+
for (const fkName of metadata.foreignKeyNames) {
|
|
228
|
+
const fieldName = prefix ? `${prefix}.${fkName}` : fkName;
|
|
229
|
+
if (existingNames.has(fieldName)) continue;
|
|
230
|
+
const relationName = fkName.slice(0, -2);
|
|
231
|
+
const relation = metadata.foreignKeyRelationMap.get(relationName);
|
|
232
|
+
const relationDisplayName = relation?.displayName ?? relationName;
|
|
233
|
+
const nullable = relation && 'nullable' in relation ? (relation.nullable ?? false) : false;
|
|
234
|
+
fields.push({
|
|
235
|
+
name: fieldName,
|
|
236
|
+
displayName: `${relationDisplayName}ID`,
|
|
237
|
+
type: 'string',
|
|
238
|
+
nullable
|
|
239
|
+
});
|
|
240
|
+
existingNames.add(fieldName);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 4. 处理关系(递归)
|
|
244
|
+
if (depth < relationQueryDeep) {
|
|
245
|
+
for (const [key, relation] of metadata.relationMap) {
|
|
246
|
+
if (ignoreKeys.includes(key)) continue;
|
|
247
|
+
|
|
248
|
+
const relatedModel = allModels.get(relation.mappedEntity);
|
|
249
|
+
if (!relatedModel) continue;
|
|
250
|
+
|
|
251
|
+
if (depth > 0 && relatedModel.metadata === firstMetadata) continue;
|
|
252
|
+
if (visitedEntities.has(relatedModel.metadata.name)) continue;
|
|
253
|
+
|
|
254
|
+
const relPrefix = prefix ? `${prefix}.${key}` : key;
|
|
255
|
+
const relDisplayName = relation.displayName || key;
|
|
256
|
+
|
|
257
|
+
// 提取关联实体的字段(用于 EXISTS 子查询)
|
|
258
|
+
const relationFields = extractRelationTargetFields(relatedModel.metadata, config);
|
|
259
|
+
|
|
260
|
+
fields.push({
|
|
261
|
+
name: relPrefix,
|
|
262
|
+
displayName: `${relDisplayName} (${relation.kind})`,
|
|
263
|
+
type: 'relation' as QBPropertyType,
|
|
264
|
+
isRelation: true,
|
|
265
|
+
relationTarget: relation.mappedEntity,
|
|
266
|
+
relationFields
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const nextIgnoreKeys = getRelationIgnoreKeys(relation);
|
|
270
|
+
const nextVisited = new Set(visitedEntities);
|
|
271
|
+
nextVisited.add(metadata.name);
|
|
272
|
+
|
|
273
|
+
const nestedFields = extractFieldsRecursive(
|
|
274
|
+
relatedModel.metadata,
|
|
275
|
+
allModels,
|
|
276
|
+
config,
|
|
277
|
+
firstMetadata,
|
|
278
|
+
nextIgnoreKeys,
|
|
279
|
+
relPrefix,
|
|
280
|
+
nextVisited,
|
|
281
|
+
depth + 1
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
// 添加嵌套字段的系统字段(使用 propertyMap 做 O(1) 查找)
|
|
285
|
+
const relatedPropertyMap = relatedModel.metadata.propertyMap;
|
|
286
|
+
const existingFieldNames = new Set(fields.map(f => f.name));
|
|
287
|
+
for (const sysField of systemFields) {
|
|
288
|
+
const sysFieldName = `${relPrefix}.${sysField}`;
|
|
289
|
+
const sysProp = relatedPropertyMap.get(sysField);
|
|
290
|
+
if (sysProp && !existingFieldNames.has(sysFieldName)) {
|
|
291
|
+
fields.push({
|
|
292
|
+
name: sysFieldName,
|
|
293
|
+
displayName: `${relDisplayName}.${sysProp.displayName ?? sysField}`,
|
|
294
|
+
type: mapPropertyType(sysProp.type as PropertyType),
|
|
295
|
+
nullable: sysProp.nullable ?? false
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
fields.push(...nestedFields);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return fields;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* 提取关系目标实体的字段(用于 EXISTS 子查询)
|
|
309
|
+
*
|
|
310
|
+
* @param metadata - 实体元数据
|
|
311
|
+
* @param config - 字段提取器配置
|
|
312
|
+
* @returns 字段元数据列表(仅一层,不递归)
|
|
313
|
+
*
|
|
314
|
+
* @remarks
|
|
315
|
+
* 此函数只提取一层字段,不递归关系。用于 EXISTS/NOT EXISTS 子查询的字段列表。
|
|
316
|
+
*
|
|
317
|
+
* @example
|
|
318
|
+
* ```typescript
|
|
319
|
+
* const fields = extractRelationTargetFields(orderMetadata);
|
|
320
|
+
* // 返回: [
|
|
321
|
+
* // { name: 'id', displayName: 'ID', type: 'string' },
|
|
322
|
+
* // { name: 'total', displayName: '总金额', type: 'number' },
|
|
323
|
+
* // { name: 'ownerId', displayName: '用户ID', type: 'string' },
|
|
324
|
+
* // ...
|
|
325
|
+
* // ]
|
|
326
|
+
* ```
|
|
327
|
+
*/
|
|
328
|
+
export function extractRelationTargetFields(
|
|
329
|
+
metadata: EntityMetadata,
|
|
330
|
+
config: FieldExtractorConfig = {}
|
|
331
|
+
): FieldMetadata[] {
|
|
332
|
+
const fields: FieldMetadata[] = [];
|
|
333
|
+
const systemFields = config.systemFields ?? DEFAULT_SYSTEM_FIELDS;
|
|
334
|
+
const propertyByName = metadata.propertyMap;
|
|
335
|
+
|
|
336
|
+
// 添加系统字段
|
|
337
|
+
for (const sysField of systemFields) {
|
|
338
|
+
const sysProp = propertyByName.get(sysField);
|
|
339
|
+
if (sysProp) {
|
|
340
|
+
fields.push({
|
|
341
|
+
name: sysField,
|
|
342
|
+
displayName: sysProp.displayName ?? sysField,
|
|
343
|
+
type: mapPropertyType(sysProp.type as PropertyType),
|
|
344
|
+
nullable: sysProp.nullable ?? false
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// 添加实体属性字段
|
|
350
|
+
for (const [key, property] of metadata.propertyMap) {
|
|
351
|
+
const field: FieldMetadata = {
|
|
352
|
+
name: key,
|
|
353
|
+
displayName: property.displayName ?? key,
|
|
354
|
+
type: mapPropertyType(property.type as PropertyType),
|
|
355
|
+
nullable: property.nullable ?? false
|
|
356
|
+
};
|
|
357
|
+
if (property.type === PropertyType.enum && Array.isArray(property.enum)) {
|
|
358
|
+
field.enum = property.enum;
|
|
359
|
+
}
|
|
360
|
+
fields.push(field);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// 添加外键字段
|
|
364
|
+
const existingFkNames = new Set<string>();
|
|
365
|
+
for (const f of fields) existingFkNames.add(f.name);
|
|
366
|
+
for (const fkName of metadata.foreignKeyNames) {
|
|
367
|
+
if (existingFkNames.has(fkName)) continue;
|
|
368
|
+
const relationName = fkName.slice(0, -2);
|
|
369
|
+
const relation = metadata.foreignKeyRelationMap.get(relationName);
|
|
370
|
+
const relationDisplayName = relation?.displayName ?? relationName;
|
|
371
|
+
const nullable = relation && 'nullable' in relation ? (relation.nullable ?? false) : false;
|
|
372
|
+
fields.push({
|
|
373
|
+
name: fkName,
|
|
374
|
+
displayName: `${relationDisplayName}ID`,
|
|
375
|
+
type: 'string',
|
|
376
|
+
nullable
|
|
377
|
+
});
|
|
378
|
+
existingFkNames.add(fkName);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return fields;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* 将 KeyValuePropertyMetadata 的 type 映射到 QueryBuilder PropertyType
|
|
386
|
+
*/
|
|
387
|
+
function mapKVPropertyType(type?: string): QBPropertyType {
|
|
388
|
+
switch (type) {
|
|
389
|
+
case 'number':
|
|
390
|
+
case 'integer':
|
|
391
|
+
return 'number';
|
|
392
|
+
case 'boolean':
|
|
393
|
+
return 'boolean';
|
|
394
|
+
case 'date':
|
|
395
|
+
return 'date';
|
|
396
|
+
default:
|
|
397
|
+
return 'string';
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* 从 keyValue 属性的 properties 提取子字段列表
|
|
403
|
+
*
|
|
404
|
+
* @param properties keyValue 属性的子属性元数据列表
|
|
405
|
+
* @returns 子字段元数据列表
|
|
406
|
+
*/
|
|
407
|
+
export function extractKeyValueFields(properties: KeyValuePropertyMetadata[]): FieldMetadata[] {
|
|
408
|
+
return properties.map(p => ({
|
|
409
|
+
name: p.name,
|
|
410
|
+
displayName: p.displayName ?? p.name,
|
|
411
|
+
type: mapKVPropertyType(p.type as string),
|
|
412
|
+
nullable: p.nullable ?? false,
|
|
413
|
+
required: p.required ?? false
|
|
414
|
+
}));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* 对字段进行分组和排序
|
|
419
|
+
*
|
|
420
|
+
* @param fields - 字段元数据列表
|
|
421
|
+
* @returns 排序后的字段列表
|
|
422
|
+
*
|
|
423
|
+
* @remarks
|
|
424
|
+
* 排序规则:
|
|
425
|
+
* 1. 按层级深度排序(字段名中 '.' 的数量)
|
|
426
|
+
* 2. 同一层级按字段名字母顺序排序
|
|
427
|
+
*
|
|
428
|
+
* @example
|
|
429
|
+
* ```typescript
|
|
430
|
+
* const fields = [
|
|
431
|
+
* { name: 'orders.id', ... },
|
|
432
|
+
* { name: 'name', ... },
|
|
433
|
+
* { name: 'id', ... },
|
|
434
|
+
* { name: 'orders.total', ... }
|
|
435
|
+
* ];
|
|
436
|
+
*
|
|
437
|
+
* const sorted = organizeFields(fields);
|
|
438
|
+
* // 返回: [
|
|
439
|
+
* // { name: 'id', ... },
|
|
440
|
+
* // { name: 'name', ... },
|
|
441
|
+
* // { name: 'orders.id', ... },
|
|
442
|
+
* // { name: 'orders.total', ... }
|
|
443
|
+
* // ]
|
|
444
|
+
* ```
|
|
445
|
+
*/
|
|
446
|
+
export function organizeFields(fields: FieldMetadata[]): FieldMetadata[] {
|
|
447
|
+
// 预计算深度,避免在排序比较器中重复执行 regex
|
|
448
|
+
const depths = new Map<FieldMetadata, number>();
|
|
449
|
+
for (const f of fields) {
|
|
450
|
+
let depth = 0;
|
|
451
|
+
for (let i = 0; i < f.name.length; i++) {
|
|
452
|
+
if (f.name.charCodeAt(i) === 46) depth++;
|
|
453
|
+
}
|
|
454
|
+
depths.set(f, depth);
|
|
455
|
+
}
|
|
456
|
+
return [...fields].sort((a, b) => {
|
|
457
|
+
const d = (depths.get(a) ?? 0) - (depths.get(b) ?? 0);
|
|
458
|
+
return d !== 0 ? d : a.name.localeCompare(b.name);
|
|
459
|
+
});
|
|
460
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 计算弹出层相对于触发元素的位置,优先下方展示,空间不足时向上。
|
|
3
|
+
*
|
|
4
|
+
* @param triggerRect 触发按钮的 DOMRect
|
|
5
|
+
* @param gap 触发元素与弹出层之间的间距(单位 px,默认 2)
|
|
6
|
+
* @param minSpaceBelow 判断下方空间是否充足的阈值(单位 px,默认 120)
|
|
7
|
+
* @returns 弹出层定位信息:left、minWidth,以及 top(下方展示)或 bottom(上方展示)
|
|
8
|
+
*/
|
|
9
|
+
export function calcPopoverPosition(
|
|
10
|
+
triggerRect: DOMRect,
|
|
11
|
+
gap = 2,
|
|
12
|
+
minSpaceBelow = 120
|
|
13
|
+
): { left: number; top?: number; bottom?: number; minWidth: number } {
|
|
14
|
+
const spaceBelow = window.innerHeight - triggerRect.bottom;
|
|
15
|
+
if (spaceBelow > minSpaceBelow) {
|
|
16
|
+
return { left: triggerRect.left, top: triggerRect.bottom + gap, minWidth: triggerRect.width };
|
|
17
|
+
}
|
|
18
|
+
return { left: triggerRect.left, bottom: window.innerHeight - triggerRect.top + gap, minWidth: triggerRect.width };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 将 calcPopoverPosition 的结果直接应用到弹出层 DOM 元素。
|
|
23
|
+
*
|
|
24
|
+
* @param el 弹出层 DOM 元素
|
|
25
|
+
* @param triggerRect 触发按钮的 DOMRect
|
|
26
|
+
* @param gap 触发元素与弹出层之间的间距(单位 px,默认 2)
|
|
27
|
+
* @param minSpaceBelow 判断下方空间是否充足的阈值(单位 px,默认 120)
|
|
28
|
+
*/
|
|
29
|
+
export function applyPopoverPosition(el: HTMLElement, triggerRect: DOMRect, gap = 2, minSpaceBelow = 120): void {
|
|
30
|
+
const pos = calcPopoverPosition(triggerRect, gap, minSpaceBelow);
|
|
31
|
+
el.style.position = 'fixed';
|
|
32
|
+
el.style.margin = '0';
|
|
33
|
+
el.style.inset = 'unset';
|
|
34
|
+
el.style.minWidth = `${pos.minWidth}px`;
|
|
35
|
+
el.style.left = `${pos.left}px`;
|
|
36
|
+
if (pos.top != null) {
|
|
37
|
+
el.style.top = `${pos.top}px`;
|
|
38
|
+
el.style.bottom = 'unset';
|
|
39
|
+
} else {
|
|
40
|
+
el.style.top = 'unset';
|
|
41
|
+
el.style.bottom = `${pos.bottom}px`;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 在弹出层选项列表中查找匹配项的 label。
|
|
47
|
+
*
|
|
48
|
+
* @param options 选项列表(value 为选中值、label 为显示文本)
|
|
49
|
+
* @param selected 当前选中的 value
|
|
50
|
+
* @param placeholder 未找到匹配项时返回的占位文本
|
|
51
|
+
* @returns 匹配项的 label;未找到时返回 placeholder
|
|
52
|
+
*/
|
|
53
|
+
export function findOptionLabel(
|
|
54
|
+
options: ReadonlyArray<{ value: string; label: string }>,
|
|
55
|
+
selected: string,
|
|
56
|
+
placeholder: string
|
|
57
|
+
): string {
|
|
58
|
+
return options.find(o => o.value === selected)?.label ?? placeholder;
|
|
59
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Schema 工厂函数
|
|
3
|
+
* 提供便捷的工厂方法从 RxDB 实体创建 SchemaParser 实例并解析字段
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { EntityMetadata } from '@aiao/rxdb';
|
|
7
|
+
import type { FieldMetadata } from '../models/query-builder-state.js';
|
|
8
|
+
import { createSchemaParser, type SchemaParserOptions } from '../services/schema-parser.service.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 从 EntityMetadata 创建字段元数据
|
|
12
|
+
*
|
|
13
|
+
* 这是一个便捷的工厂函数,封装了 SchemaParser 的创建和解析过程
|
|
14
|
+
*
|
|
15
|
+
* @param metadata - RxDB 实体元数据
|
|
16
|
+
* @param options - 解析器选项
|
|
17
|
+
* @returns 字段元数据数组
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* // 基础用法
|
|
22
|
+
* const fields = createSchemaFromEntity(userMetadata);
|
|
23
|
+
*
|
|
24
|
+
* // 包含外键
|
|
25
|
+
* const fieldsWithFK = createSchemaFromEntity(userMetadata, {
|
|
26
|
+
* includeForeignKeys: true
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* // 包含计算属性
|
|
30
|
+
* const fieldsWithComputed = createSchemaFromEntity(userMetadata, {
|
|
31
|
+
* includeComputedProperties: true
|
|
32
|
+
* });
|
|
33
|
+
*
|
|
34
|
+
* // 自定义过滤器
|
|
35
|
+
* const onlyStringFields = createSchemaFromEntity(userMetadata, {
|
|
36
|
+
* fieldFilter: (field) => field.type === 'string'
|
|
37
|
+
* });
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function createSchemaFromEntity(metadata: EntityMetadata, options?: SchemaParserOptions): FieldMetadata[] {
|
|
41
|
+
const parser = createSchemaParser(options);
|
|
42
|
+
return parser.parse(metadata);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 从多个 EntityMetadata 批量创建字段元数据
|
|
47
|
+
*
|
|
48
|
+
* @param metadataMap - 实体名称到元数据的映射
|
|
49
|
+
* @param options - 解析器选项
|
|
50
|
+
* @returns 实体名称到字段数组的映射
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```typescript
|
|
54
|
+
* const entities = new Map([
|
|
55
|
+
* ['User', userMetadata],
|
|
56
|
+
* ['Post', postMetadata],
|
|
57
|
+
* ['Comment', commentMetadata]
|
|
58
|
+
* ]);
|
|
59
|
+
*
|
|
60
|
+
* const schemas = createSchemaFromEntities(entities, {
|
|
61
|
+
* includeForeignKeys: true
|
|
62
|
+
* });
|
|
63
|
+
*
|
|
64
|
+
* // 使用结果
|
|
65
|
+
* const userFields = schemas.get('User');
|
|
66
|
+
* const postFields = schemas.get('Post');
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export function createSchemaFromEntities(
|
|
70
|
+
metadataMap: Map<string, EntityMetadata>,
|
|
71
|
+
options?: SchemaParserOptions
|
|
72
|
+
): Map<string, FieldMetadata[]> {
|
|
73
|
+
const parser = createSchemaParser(options);
|
|
74
|
+
return parser.parseMany(metadataMap);
|
|
75
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { UUID_RE } from '../../entity-value.utils.js';
|
|
2
|
+
import type { OperatorDefinition } from '../models/operator.interface.js';
|
|
3
|
+
import type { PropertyType } from '../models/query-builder-state.js';
|
|
4
|
+
import type { OperatorRegistry } from '../services/operator-registry.service.js';
|
|
5
|
+
import { getDefaultOperatorRegistry } from '../services/operator-registry.service.js';
|
|
6
|
+
|
|
7
|
+
/** 标准 UUID 格式正则(8-4-4-4-12 十六进制,忽略大小写),重新导出供校验使用 */
|
|
8
|
+
export { UUID_RE };
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 根据字段类型和运算符确定查询条件值的输入控件类型。
|
|
12
|
+
*
|
|
13
|
+
* @param fieldType 字段类型(如 'string'、'number'、'date'、'enum' 等)
|
|
14
|
+
* @param operator 运算符 key(如 'between'、'in'、'null' 等)
|
|
15
|
+
* @param enumOptions 字段的枚举选项列表
|
|
16
|
+
* @param operatorRegistry 运算符注册表(默认使用全局默认注册表)
|
|
17
|
+
* @returns 输入控件类型标识(如 'string'、'number'、'boolean'、'date'、'range'、'array'、'enum'、'enum-array'、'subquery'、'uuid'、'none')
|
|
18
|
+
*/
|
|
19
|
+
export function getInputType(
|
|
20
|
+
fieldType: string,
|
|
21
|
+
operator: string,
|
|
22
|
+
enumOptions?: unknown[],
|
|
23
|
+
operatorRegistry: OperatorRegistry = getDefaultOperatorRegistry()
|
|
24
|
+
): string {
|
|
25
|
+
if (operator === 'null' || operator === 'notNull') return 'none';
|
|
26
|
+
|
|
27
|
+
const operators = operatorRegistry.getForType(fieldType as PropertyType) || [];
|
|
28
|
+
const op = operators.find((o: OperatorDefinition) => o.key === operator);
|
|
29
|
+
|
|
30
|
+
if (op?.valueType === 'none') return 'none';
|
|
31
|
+
if (op?.valueType === 'subquery') return 'subquery';
|
|
32
|
+
if (operator === 'between' || operator === 'notBetween') return 'range';
|
|
33
|
+
if (operator === 'in' || operator === 'notIn') {
|
|
34
|
+
if ((enumOptions && enumOptions.length > 0) || fieldType === 'enum') return 'enum-array';
|
|
35
|
+
return 'array';
|
|
36
|
+
}
|
|
37
|
+
if (enumOptions && enumOptions.length > 0) return 'enum';
|
|
38
|
+
|
|
39
|
+
switch (fieldType) {
|
|
40
|
+
case 'boolean':
|
|
41
|
+
return 'boolean';
|
|
42
|
+
case 'number':
|
|
43
|
+
case 'integer':
|
|
44
|
+
return 'number';
|
|
45
|
+
case 'date':
|
|
46
|
+
case 'date-time':
|
|
47
|
+
return 'date';
|
|
48
|
+
case 'array':
|
|
49
|
+
return 'array';
|
|
50
|
+
case 'enum':
|
|
51
|
+
return 'enum';
|
|
52
|
+
case 'uuid':
|
|
53
|
+
return 'uuid';
|
|
54
|
+
default:
|
|
55
|
+
return 'string';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 返回指定输入控件类型的默认值。
|
|
61
|
+
*
|
|
62
|
+
* @param type 输入控件类型(getInputType 的返回值)
|
|
63
|
+
* @returns 对应类型的默认值:boolean 为 false、range/array/enum-array 为 []、subquery 为 undefined、none/number/date 为 null、其余为 ''
|
|
64
|
+
*/
|
|
65
|
+
export function getDefaultValueForType(type: string): unknown {
|
|
66
|
+
switch (type) {
|
|
67
|
+
case 'none':
|
|
68
|
+
return null;
|
|
69
|
+
case 'boolean':
|
|
70
|
+
return false;
|
|
71
|
+
case 'number':
|
|
72
|
+
return null;
|
|
73
|
+
case 'date':
|
|
74
|
+
return null;
|
|
75
|
+
case 'range':
|
|
76
|
+
return [];
|
|
77
|
+
case 'array':
|
|
78
|
+
return [];
|
|
79
|
+
case 'enum-array':
|
|
80
|
+
return [];
|
|
81
|
+
case 'subquery':
|
|
82
|
+
return undefined;
|
|
83
|
+
default:
|
|
84
|
+
return '';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 将逗号分隔的字符串解析为数组。
|
|
90
|
+
* 当 fieldType 为 'number' 时返回数字数组,否则返回字符串数组。
|
|
91
|
+
*
|
|
92
|
+
* @param text 逗号分隔的字符串
|
|
93
|
+
* @param fieldType 字段类型;为 'number' 时各项解析为数字并过滤无效值
|
|
94
|
+
* @returns 解析后的数组(空白项被过滤)
|
|
95
|
+
*/
|
|
96
|
+
export function parseCommaSeparatedInput(text: string, fieldType: string): unknown[] {
|
|
97
|
+
const items = text
|
|
98
|
+
.split(',')
|
|
99
|
+
.map(s => s.trim())
|
|
100
|
+
.filter(s => s.length > 0);
|
|
101
|
+
if (fieldType === 'number') {
|
|
102
|
+
return items.map(s => Number(s)).filter(n => !isNaN(n));
|
|
103
|
+
}
|
|
104
|
+
return items;
|
|
105
|
+
}
|