@aiao/rxdb-model-angular 0.0.17

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.
@@ -0,0 +1,4584 @@
1
+ import { RxDB, getEntityMetadata, RelationKind } from '@aiao/rxdb';
2
+ import { formatEntityFieldValue, parseEntityFieldValue, validateForm, TableClipboardManager, CellTooltipManager, updateTableRecords, changeCellValue, createTheme, getCSSVariables, completeTableEdit, createListTable, patchDragIconForReadonlyRows, collectReorderedIds, handleTableKeydown, ROW_SERIES_COL_OFFSET, setCellSwitchState, collectDeleteWrites, getDisplayNamePath, findNodeByValue, applyPopoverPosition, buildFieldTree, findOptionLabel, getDefaultOperatorRegistry, UUID_RE, getInputType, getDefaultValueForType, parseCommaSeparatedInput, createQueryBuilderService, buildEditableColumns, organizeFields, extractFieldsFromMetadata, actionsColumn, buildFormFields, parsePropertyColumnValue, buildDetailTabs, entityToFormData } from '@aiao/rxdb-model';
3
+ export * from '@aiao/rxdb-model';
4
+ import { DialogRef, Dialog, DIALOG_DATA } from '@angular/cdk/dialog';
5
+ import * as i0 from '@angular/core';
6
+ import { inject, ElementRef, input, output, signal, ChangeDetectionStrategy, Component, linkedSignal, computed, InjectionToken, PLATFORM_ID, viewChild, afterNextRender, effect, untracked, Directive, viewChildren, Injector, ViewContainerRef, ViewChild, ErrorHandler, runInInjectionContext, HostListener, forwardRef } from '@angular/core';
7
+ import { takeUntilDestroyed, toSignal, toObservable } from '@angular/core/rxjs-interop';
8
+ import { switchMap, of } from 'rxjs';
9
+ import * as i1 from '@angular/cdk/drag-drop';
10
+ import { CdkDrag, CdkDragHandle } from '@angular/cdk/drag-drop';
11
+ import * as i1$2 from '@angular/common';
12
+ import { DOCUMENT, DatePipe, JsonPipe, isPlatformBrowser, CommonModule, NgComponentOutlet } from '@angular/common';
13
+ import { InfiniteScrollingList } from '@aiao/rxdb-angular';
14
+ import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
15
+ import * as i2 from 'lucide-angular';
16
+ import { ChevronDownIcon, ChevronRightIcon, TableIcon, ListChecksIcon, Link2Icon, BracesIcon, ListIcon, CalendarIcon, ToggleLeftIcon, HashIcon, TypeIcon, LucideAngularModule, Undo2, Redo2, Funnel } from 'lucide-angular';
17
+ import { ActiveDescendantKeyManager } from '@angular/cdk/a11y';
18
+ import * as i1$1 from '@angular/forms';
19
+ import { FormsModule } from '@angular/forms';
20
+
21
+ class EntityDialogComponent {
22
+ #dialogRef = inject(DialogRef, { optional: true });
23
+ #cdkDrag = inject(CdkDrag, { optional: true, self: true });
24
+ #el = inject(ElementRef);
25
+ #doc = inject(DOCUMENT);
26
+ #pane = null;
27
+ #resizing = false;
28
+ #savedPaneState = null;
29
+ title = input.required(...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
30
+ closeRequested = output();
31
+ isFullscreen = signal(false, ...(ngDevMode ? [{ debugName: "isFullscreen" }] : /* istanbul ignore next */ []));
32
+ isInDialog = !!inject(DialogRef, { optional: true });
33
+ constructor() {
34
+ if (this.#cdkDrag) {
35
+ if (this.isInDialog) {
36
+ this.#cdkDrag.rootElementSelector = '.cdk-overlay-pane';
37
+ this.#cdkDrag.boundaryElement = '.cdk-overlay-container';
38
+ }
39
+ else {
40
+ this.#cdkDrag.disabled = true;
41
+ }
42
+ }
43
+ }
44
+ ngAfterViewInit() {
45
+ if (!this.isInDialog)
46
+ return;
47
+ requestAnimationFrame(() => this.#initFixedPane());
48
+ }
49
+ close() {
50
+ this.closeRequested.emit();
51
+ this.#dialogRef?.close();
52
+ }
53
+ toggleFullscreen() {
54
+ const wasFullscreen = this.isFullscreen();
55
+ this.isFullscreen.set(!wasFullscreen);
56
+ const fullscreen = !wasFullscreen;
57
+ const pane = this.#pane;
58
+ if (!pane)
59
+ return;
60
+ if (fullscreen) {
61
+ this.#savedPaneState = {
62
+ left: pane.style.left,
63
+ top: pane.style.top,
64
+ width: pane.style.width,
65
+ height: pane.style.height
66
+ };
67
+ pane.style.left = '0';
68
+ pane.style.top = '0';
69
+ pane.style.width = '100vw';
70
+ pane.style.height = '100vh';
71
+ if (this.#cdkDrag) {
72
+ this.#cdkDrag.disabled = true;
73
+ this.#cdkDrag.setFreeDragPosition({ x: 0, y: 0 });
74
+ }
75
+ }
76
+ else {
77
+ if (this.#savedPaneState) {
78
+ pane.style.left = this.#savedPaneState.left;
79
+ pane.style.top = this.#savedPaneState.top;
80
+ pane.style.width = this.#savedPaneState.width;
81
+ pane.style.height = this.#savedPaneState.height;
82
+ this.#savedPaneState = null;
83
+ }
84
+ if (this.#cdkDrag)
85
+ this.#cdkDrag.disabled = false;
86
+ }
87
+ }
88
+ startResize(e, dir) {
89
+ const pane = this.#pane;
90
+ if (!pane || this.#resizing || this.isFullscreen())
91
+ return;
92
+ e.preventDefault();
93
+ e.stopPropagation();
94
+ this.#resizing = true;
95
+ const startX = e.clientX;
96
+ const startY = e.clientY;
97
+ const startW = pane.offsetWidth;
98
+ const startH = pane.offsetHeight;
99
+ const startL = parseFloat(pane.style.left) || 0;
100
+ const startT = parseFloat(pane.style.top) || 0;
101
+ const minW = 400;
102
+ const minH = 300;
103
+ const onMove = (ev) => {
104
+ const dx = ev.clientX - startX;
105
+ const dy = ev.clientY - startY;
106
+ if (dir.includes('e'))
107
+ pane.style.width = Math.max(minW, startW + dx) + 'px';
108
+ if (dir.includes('w')) {
109
+ const w = Math.max(minW, startW - dx);
110
+ pane.style.width = w + 'px';
111
+ pane.style.left = startL + startW - w + 'px';
112
+ }
113
+ if (dir.includes('s'))
114
+ pane.style.height = Math.max(minH, startH + dy) + 'px';
115
+ if (dir.includes('n')) {
116
+ const h = Math.max(minH, startH - dy);
117
+ pane.style.height = h + 'px';
118
+ pane.style.top = startT + startH - h + 'px';
119
+ }
120
+ };
121
+ const onUp = () => {
122
+ this.#resizing = false;
123
+ this.#doc.removeEventListener('mousemove', onMove);
124
+ this.#doc.removeEventListener('mouseup', onUp);
125
+ };
126
+ this.#doc.addEventListener('mousemove', onMove);
127
+ this.#doc.addEventListener('mouseup', onUp);
128
+ }
129
+ #initFixedPane() {
130
+ const pane = this.#el.nativeElement.closest('.cdk-overlay-pane');
131
+ if (!pane)
132
+ return;
133
+ this.#pane = pane;
134
+ const rect = pane.getBoundingClientRect();
135
+ if (this.#cdkDrag)
136
+ this.#cdkDrag.setFreeDragPosition({ x: 0, y: 0 });
137
+ pane.style.position = 'fixed';
138
+ pane.style.left = rect.left + 'px';
139
+ pane.style.top = rect.top + 'px';
140
+ pane.style.margin = '0';
141
+ pane.style.maxWidth = 'none';
142
+ pane.style.maxHeight = 'none';
143
+ }
144
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: EntityDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
145
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: EntityDialogComponent, isStandalone: true, selector: "rxdb-entity-dialog", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { closeRequested: "closeRequested" }, hostDirectives: [{ directive: i1.CdkDrag, inputs: ["cdkDragBoundary", "cdkDragBoundary"] }], ngImport: i0, template: "<!-- Resize handles -->\n@if (isInDialog) {\n <div class=\"resize-handle resize-n\" (mousedown)=\"startResize($event, 'n')\"></div>\n <div class=\"resize-handle resize-s\" (mousedown)=\"startResize($event, 's')\"></div>\n <div class=\"resize-handle resize-e\" (mousedown)=\"startResize($event, 'e')\"></div>\n <div class=\"resize-handle resize-w\" (mousedown)=\"startResize($event, 'w')\"></div>\n <div class=\"resize-handle resize-ne\" (mousedown)=\"startResize($event, 'ne')\"></div>\n <div class=\"resize-handle resize-nw\" (mousedown)=\"startResize($event, 'nw')\"></div>\n <div class=\"resize-handle resize-se\" (mousedown)=\"startResize($event, 'se')\"></div>\n <div class=\"resize-handle resize-sw\" (mousedown)=\"startResize($event, 'sw')\"></div>\n}\n\n<!-- Dialog Title Bar (drag handle + fullscreen toggle + close) -->\n@if (isInDialog) {\n <div class=\"border-base-300 flex shrink-0 items-center justify-between border-b px-4 py-2\">\n <span class=\"flex-1 cursor-grab text-base font-semibold select-none active:cursor-grabbing\" cdkDragHandle>\n {{ title() }}\n </span>\n <div class=\"flex items-center gap-1\">\n <!-- Fullscreen Toggle -->\n <button\n class=\"btn btn-ghost btn-xs btn-square\"\n [attr.aria-label]=\"isFullscreen() ? '\u9000\u51FA\u5168\u5C4F' : '\u5168\u5C4F'\"\n (click)=\"toggleFullscreen()\"\n type=\"button\"\n >\n @if (isFullscreen()) {\n <svg class=\"h-4 w-4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" viewBox=\"0 0 24 24\">\n <path d=\"M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3\" />\n </svg>\n } @else {\n <svg class=\"h-4 w-4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" viewBox=\"0 0 24 24\">\n <path d=\"M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7\" />\n </svg>\n }\n </button>\n <!-- Close -->\n <button class=\"btn btn-ghost btn-xs btn-square\" (click)=\"close()\" aria-label=\"\u5173\u95ED\" type=\"button\">\n <svg class=\"h-4 w-4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" viewBox=\"0 0 24 24\">\n <path d=\"M18 6 6 18M6 6l12 12\" />\n </svg>\n </button>\n </div>\n </div>\n}\n\n<!-- Projected content -->\n<ng-content />\n", styles: [":host{display:flex;flex-direction:column;height:100%;overflow:hidden;background-color:var(--color-base-100);border-radius:var(--radius-md)}.resize-handle{position:absolute;z-index:100}.resize-n{top:-4px;left:8px;right:8px;height:8px;cursor:n-resize}.resize-s{bottom:-4px;left:8px;right:8px;height:8px;cursor:s-resize}.resize-e{top:8px;bottom:8px;right:-4px;width:8px;cursor:e-resize}.resize-w{top:8px;bottom:8px;left:-4px;width:8px;cursor:w-resize}.resize-ne{top:-4px;right:-4px;width:16px;height:16px;cursor:ne-resize}.resize-nw{top:-4px;left:-4px;width:16px;height:16px;cursor:nw-resize}.resize-se{bottom:-4px;right:-4px;width:16px;height:16px;cursor:se-resize}.resize-sw{bottom:-4px;left:-4px;width:16px;height:16px;cursor:sw-resize}\n"], dependencies: [{ kind: "directive", type: CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
146
+ }
147
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: EntityDialogComponent, decorators: [{
148
+ type: Component,
149
+ args: [{ selector: 'rxdb-entity-dialog', imports: [CdkDragHandle], hostDirectives: [{ directive: CdkDrag, inputs: ['cdkDragBoundary'] }], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Resize handles -->\n@if (isInDialog) {\n <div class=\"resize-handle resize-n\" (mousedown)=\"startResize($event, 'n')\"></div>\n <div class=\"resize-handle resize-s\" (mousedown)=\"startResize($event, 's')\"></div>\n <div class=\"resize-handle resize-e\" (mousedown)=\"startResize($event, 'e')\"></div>\n <div class=\"resize-handle resize-w\" (mousedown)=\"startResize($event, 'w')\"></div>\n <div class=\"resize-handle resize-ne\" (mousedown)=\"startResize($event, 'ne')\"></div>\n <div class=\"resize-handle resize-nw\" (mousedown)=\"startResize($event, 'nw')\"></div>\n <div class=\"resize-handle resize-se\" (mousedown)=\"startResize($event, 'se')\"></div>\n <div class=\"resize-handle resize-sw\" (mousedown)=\"startResize($event, 'sw')\"></div>\n}\n\n<!-- Dialog Title Bar (drag handle + fullscreen toggle + close) -->\n@if (isInDialog) {\n <div class=\"border-base-300 flex shrink-0 items-center justify-between border-b px-4 py-2\">\n <span class=\"flex-1 cursor-grab text-base font-semibold select-none active:cursor-grabbing\" cdkDragHandle>\n {{ title() }}\n </span>\n <div class=\"flex items-center gap-1\">\n <!-- Fullscreen Toggle -->\n <button\n class=\"btn btn-ghost btn-xs btn-square\"\n [attr.aria-label]=\"isFullscreen() ? '\u9000\u51FA\u5168\u5C4F' : '\u5168\u5C4F'\"\n (click)=\"toggleFullscreen()\"\n type=\"button\"\n >\n @if (isFullscreen()) {\n <svg class=\"h-4 w-4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" viewBox=\"0 0 24 24\">\n <path d=\"M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3\" />\n </svg>\n } @else {\n <svg class=\"h-4 w-4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" viewBox=\"0 0 24 24\">\n <path d=\"M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7\" />\n </svg>\n }\n </button>\n <!-- Close -->\n <button class=\"btn btn-ghost btn-xs btn-square\" (click)=\"close()\" aria-label=\"\u5173\u95ED\" type=\"button\">\n <svg class=\"h-4 w-4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" viewBox=\"0 0 24 24\">\n <path d=\"M18 6 6 18M6 6l12 12\" />\n </svg>\n </button>\n </div>\n </div>\n}\n\n<!-- Projected content -->\n<ng-content />\n", styles: [":host{display:flex;flex-direction:column;height:100%;overflow:hidden;background-color:var(--color-base-100);border-radius:var(--radius-md)}.resize-handle{position:absolute;z-index:100}.resize-n{top:-4px;left:8px;right:8px;height:8px;cursor:n-resize}.resize-s{bottom:-4px;left:8px;right:8px;height:8px;cursor:s-resize}.resize-e{top:8px;bottom:8px;right:-4px;width:8px;cursor:e-resize}.resize-w{top:8px;bottom:8px;left:-4px;width:8px;cursor:w-resize}.resize-ne{top:-4px;right:-4px;width:16px;height:16px;cursor:ne-resize}.resize-nw{top:-4px;left:-4px;width:16px;height:16px;cursor:nw-resize}.resize-se{bottom:-4px;right:-4px;width:16px;height:16px;cursor:se-resize}.resize-sw{bottom:-4px;left:-4px;width:16px;height:16px;cursor:sw-resize}\n"] }]
150
+ }], ctorParameters: () => [], propDecorators: { title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: true }] }], closeRequested: [{ type: i0.Output, args: ["closeRequested"] }] } });
151
+
152
+ class EntityFormComponent {
153
+ fields = input.required(...(ngDevMode ? [{ debugName: "fields" }] : /* istanbul ignore next */ []));
154
+ data = input.required(...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
155
+ mode = input('view', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
156
+ relatedEntityProvider = input(...(ngDevMode ? [undefined, { debugName: "relatedEntityProvider" }] : /* istanbul ignore next */ []));
157
+ showActions = input(true, ...(ngDevMode ? [{ debugName: "showActions" }] : /* istanbul ignore next */ []));
158
+ fieldChanged = output();
159
+ formSubmitted = output();
160
+ formCancelled = output();
161
+ validationErrors = output();
162
+ formData = linkedSignal(() => ({ ...this.data() }), ...(ngDevMode ? [{ debugName: "formData" }] : /* istanbul ignore next */ []));
163
+ editableFields = computed(() => {
164
+ return this.fields().filter(f => !f.hidden);
165
+ }, ...(ngDevMode ? [{ debugName: "editableFields" }] : /* istanbul ignore next */ []));
166
+ isReadonly = computed(() => this.mode() === 'view', ...(ngDevMode ? [{ debugName: "isReadonly" }] : /* istanbul ignore next */ []));
167
+ relatedItemsMap = computed(() => {
168
+ const provider = this.relatedEntityProvider();
169
+ const fields = this.editableFields();
170
+ const map = new Map();
171
+ if (!provider)
172
+ return map;
173
+ for (const field of fields) {
174
+ if (field.relatedEntityName) {
175
+ map.set(field.field, provider(field.relatedEntityName, field.relatedNamespace));
176
+ }
177
+ }
178
+ return map;
179
+ }, ...(ngDevMode ? [{ debugName: "relatedItemsMap" }] : /* istanbul ignore next */ []));
180
+ displayValueMap = computed(() => {
181
+ const data = this.formData();
182
+ const fields = this.editableFields();
183
+ const itemsMap = this.relatedItemsMap();
184
+ const map = new Map();
185
+ for (const field of fields) {
186
+ if (!this.isReadonly() && field.readonly !== true)
187
+ continue;
188
+ const value = data[field.field];
189
+ if ((field.type === 'oneToOne' || field.type === 'manyToOne') && field.relatedEntityName) {
190
+ const items = itemsMap.get(field.field) ?? [];
191
+ const item = items.find(i => i.id === value);
192
+ map.set(field.field, item?.displayName ?? formatEntityFieldValue(field.type, value));
193
+ }
194
+ else {
195
+ map.set(field.field, formatEntityFieldValue(field.type, value));
196
+ }
197
+ }
198
+ return map;
199
+ }, ...(ngDevMode ? [{ debugName: "displayValueMap" }] : /* istanbul ignore next */ []));
200
+ onFieldChange(field, rawValue) {
201
+ const current = this.formData();
202
+ const previousValue = current[field.field];
203
+ const parsed = parseEntityFieldValue(field.type, rawValue);
204
+ this.formData.set({ ...current, [field.field]: parsed });
205
+ this.fieldChanged.emit({ field: field.field, type: field.type, value: parsed, previousValue });
206
+ }
207
+ onSubmit() {
208
+ const currentFields = this.fields();
209
+ const currentData = this.formData();
210
+ const result = validateForm(currentFields, currentData);
211
+ if (!result.valid) {
212
+ this.validationErrors.emit(result);
213
+ return;
214
+ }
215
+ this.formSubmitted.emit(currentData);
216
+ }
217
+ onCancel() {
218
+ this.formData.set({ ...this.data() });
219
+ this.formCancelled.emit();
220
+ }
221
+ isFieldReadonly(field) {
222
+ return this.isReadonly() || field.readonly === true;
223
+ }
224
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: EntityFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
225
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: EntityFormComponent, isStandalone: true, selector: "rxdb-entity-form", inputs: { fields: { classPropertyName: "fields", publicName: "fields", isSignal: true, isRequired: true, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: true, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, relatedEntityProvider: { classPropertyName: "relatedEntityProvider", publicName: "relatedEntityProvider", isSignal: true, isRequired: false, transformFunction: null }, showActions: { classPropertyName: "showActions", publicName: "showActions", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { fieldChanged: "fieldChanged", formSubmitted: "formSubmitted", formCancelled: "formCancelled", validationErrors: "validationErrors" }, ngImport: i0, template: `<form class="grid grid-cols-1 gap-4 md:grid-cols-2" (submit)="$event.preventDefault(); onSubmit()">
226
+ @for (field of editableFields(); track field.field) {
227
+ <fieldset
228
+ class="fieldset"
229
+ [class.md:col-span-2]="field.span === 2 || field.type === 'json' || field.type === 'keyValue'"
230
+ >
231
+ <legend class="fieldset-legend">{{ field.displayName }}</legend>
232
+ @if (isFieldReadonly(field)) {
233
+ <div class="input input-ghost flex min-h-10 items-center">{{ displayValueMap().get(field.field) ?? '' }}</div>
234
+ } @else {
235
+ @switch (field.type) {
236
+ @case ('boolean') {
237
+ <input
238
+ class="toggle"
239
+ [checked]="!!formData()[field.field]"
240
+ (change)="onFieldChange(field, $any($event.target).checked)"
241
+ type="checkbox"
242
+ />
243
+ }
244
+ @case ('enum') {
245
+ <select
246
+ class="select"
247
+ [value]="formData()[field.field] ?? ''"
248
+ (change)="onFieldChange(field, $any($event.target).value)"
249
+ >
250
+ @if (field.nullable) {
251
+ <option value="">(空)</option>
252
+ }
253
+ @for (val of field.enumValues ?? []; track val) {
254
+ <option [value]="val">{{ val }}</option>
255
+ }
256
+ </select>
257
+ }
258
+ @case ('date') {
259
+ <input
260
+ class="input"
261
+ [value]="formData()[field.field] ? ($any(formData()[field.field]) | date: 'yyyy-MM-ddTHH:mm') : ''"
262
+ (change)="onFieldChange(field, $any($event.target).value)"
263
+ type="datetime-local"
264
+ />
265
+ }
266
+ @case ('number') {
267
+ <input
268
+ class="input"
269
+ [attr.placeholder]="field.placeholder ?? ''"
270
+ [value]="formData()[field.field] ?? ''"
271
+ (change)="onFieldChange(field, $any($event.target).value)"
272
+ step="any"
273
+ type="number"
274
+ />
275
+ }
276
+ @case ('integer') {
277
+ <input
278
+ class="input"
279
+ [attr.placeholder]="field.placeholder ?? ''"
280
+ [value]="formData()[field.field] ?? ''"
281
+ (change)="onFieldChange(field, $any($event.target).value)"
282
+ step="1"
283
+ type="number"
284
+ />
285
+ }
286
+ @case ('stringArray') {
287
+ <input
288
+ class="input"
289
+ [attr.placeholder]="field.placeholder ?? '逗号分隔'"
290
+ [value]="$any(formData()[field.field] ?? []).join(', ')"
291
+ (change)="onFieldChange(field, $any($event.target).value)"
292
+ type="text"
293
+ />
294
+ }
295
+ @case ('numberArray') {
296
+ <input
297
+ class="input"
298
+ [attr.placeholder]="field.placeholder ?? '逗号分隔数字'"
299
+ [value]="$any(formData()[field.field] ?? []).join(', ')"
300
+ (change)="onFieldChange(field, $any($event.target).value)"
301
+ type="text"
302
+ />
303
+ }
304
+ @case ('json') {
305
+ <textarea
306
+ class="textarea min-h-24"
307
+ [attr.placeholder]="field.placeholder ?? 'JSON'"
308
+ [value]="formData()[field.field] !== null ? (formData()[field.field] | json) : ''"
309
+ (change)="onFieldChange(field, $any($event.target).value)"
310
+ ></textarea>
311
+ }
312
+ @case ('keyValue') {
313
+ <textarea
314
+ class="textarea min-h-24"
315
+ [attr.placeholder]="field.placeholder ?? 'JSON'"
316
+ [value]="formData()[field.field] !== null ? (formData()[field.field] | json) : ''"
317
+ (change)="onFieldChange(field, $any($event.target).value)"
318
+ ></textarea>
319
+ }
320
+ @case ('oneToOne') {
321
+ <select
322
+ class="select"
323
+ [value]="formData()[field.field] ?? ''"
324
+ (change)="onFieldChange(field, $any($event.target).value)"
325
+ >
326
+ <option value="">(空)</option>
327
+ @for (item of relatedItemsMap().get(field.field) ?? []; track item.id) {
328
+ <option [value]="item.id">{{ item.displayName }}</option>
329
+ }
330
+ </select>
331
+ }
332
+ @case ('manyToOne') {
333
+ <select
334
+ class="select"
335
+ [value]="formData()[field.field] ?? ''"
336
+ (change)="onFieldChange(field, $any($event.target).value)"
337
+ >
338
+ <option value="">(空)</option>
339
+ @for (item of relatedItemsMap().get(field.field) ?? []; track item.id) {
340
+ <option [value]="item.id">{{ item.displayName }}</option>
341
+ }
342
+ </select>
343
+ }
344
+ @default {
345
+ <input
346
+ class="input"
347
+ [attr.placeholder]="field.placeholder ?? ''"
348
+ [value]="formData()[field.field] ?? ''"
349
+ (change)="onFieldChange(field, $any($event.target).value)"
350
+ type="text"
351
+ />
352
+ }
353
+ }
354
+ }
355
+ @if (field.helpText) {
356
+ <p class="label">{{ field.helpText }}</p>
357
+ }
358
+ </fieldset>
359
+ }
360
+ @if (!isReadonly() && showActions()) {
361
+ <div class="flex justify-end gap-2 pt-2 md:col-span-2">
362
+ <button class="btn" (click)="onCancel()" type="button">取消</button>
363
+ <button class="btn btn-primary" type="submit">保存</button>
364
+ </div>
365
+ }
366
+ </form>`, isInline: true, dependencies: [{ kind: "pipe", type: DatePipe, name: "date" }, { kind: "pipe", type: JsonPipe, name: "json" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
367
+ }
368
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: EntityFormComponent, decorators: [{
369
+ type: Component,
370
+ args: [{
371
+ selector: 'rxdb-entity-form',
372
+ imports: [DatePipe, JsonPipe],
373
+ template: `<form class="grid grid-cols-1 gap-4 md:grid-cols-2" (submit)="$event.preventDefault(); onSubmit()">
374
+ @for (field of editableFields(); track field.field) {
375
+ <fieldset
376
+ class="fieldset"
377
+ [class.md:col-span-2]="field.span === 2 || field.type === 'json' || field.type === 'keyValue'"
378
+ >
379
+ <legend class="fieldset-legend">{{ field.displayName }}</legend>
380
+ @if (isFieldReadonly(field)) {
381
+ <div class="input input-ghost flex min-h-10 items-center">{{ displayValueMap().get(field.field) ?? '' }}</div>
382
+ } @else {
383
+ @switch (field.type) {
384
+ @case ('boolean') {
385
+ <input
386
+ class="toggle"
387
+ [checked]="!!formData()[field.field]"
388
+ (change)="onFieldChange(field, $any($event.target).checked)"
389
+ type="checkbox"
390
+ />
391
+ }
392
+ @case ('enum') {
393
+ <select
394
+ class="select"
395
+ [value]="formData()[field.field] ?? ''"
396
+ (change)="onFieldChange(field, $any($event.target).value)"
397
+ >
398
+ @if (field.nullable) {
399
+ <option value="">(空)</option>
400
+ }
401
+ @for (val of field.enumValues ?? []; track val) {
402
+ <option [value]="val">{{ val }}</option>
403
+ }
404
+ </select>
405
+ }
406
+ @case ('date') {
407
+ <input
408
+ class="input"
409
+ [value]="formData()[field.field] ? ($any(formData()[field.field]) | date: 'yyyy-MM-ddTHH:mm') : ''"
410
+ (change)="onFieldChange(field, $any($event.target).value)"
411
+ type="datetime-local"
412
+ />
413
+ }
414
+ @case ('number') {
415
+ <input
416
+ class="input"
417
+ [attr.placeholder]="field.placeholder ?? ''"
418
+ [value]="formData()[field.field] ?? ''"
419
+ (change)="onFieldChange(field, $any($event.target).value)"
420
+ step="any"
421
+ type="number"
422
+ />
423
+ }
424
+ @case ('integer') {
425
+ <input
426
+ class="input"
427
+ [attr.placeholder]="field.placeholder ?? ''"
428
+ [value]="formData()[field.field] ?? ''"
429
+ (change)="onFieldChange(field, $any($event.target).value)"
430
+ step="1"
431
+ type="number"
432
+ />
433
+ }
434
+ @case ('stringArray') {
435
+ <input
436
+ class="input"
437
+ [attr.placeholder]="field.placeholder ?? '逗号分隔'"
438
+ [value]="$any(formData()[field.field] ?? []).join(', ')"
439
+ (change)="onFieldChange(field, $any($event.target).value)"
440
+ type="text"
441
+ />
442
+ }
443
+ @case ('numberArray') {
444
+ <input
445
+ class="input"
446
+ [attr.placeholder]="field.placeholder ?? '逗号分隔数字'"
447
+ [value]="$any(formData()[field.field] ?? []).join(', ')"
448
+ (change)="onFieldChange(field, $any($event.target).value)"
449
+ type="text"
450
+ />
451
+ }
452
+ @case ('json') {
453
+ <textarea
454
+ class="textarea min-h-24"
455
+ [attr.placeholder]="field.placeholder ?? 'JSON'"
456
+ [value]="formData()[field.field] !== null ? (formData()[field.field] | json) : ''"
457
+ (change)="onFieldChange(field, $any($event.target).value)"
458
+ ></textarea>
459
+ }
460
+ @case ('keyValue') {
461
+ <textarea
462
+ class="textarea min-h-24"
463
+ [attr.placeholder]="field.placeholder ?? 'JSON'"
464
+ [value]="formData()[field.field] !== null ? (formData()[field.field] | json) : ''"
465
+ (change)="onFieldChange(field, $any($event.target).value)"
466
+ ></textarea>
467
+ }
468
+ @case ('oneToOne') {
469
+ <select
470
+ class="select"
471
+ [value]="formData()[field.field] ?? ''"
472
+ (change)="onFieldChange(field, $any($event.target).value)"
473
+ >
474
+ <option value="">(空)</option>
475
+ @for (item of relatedItemsMap().get(field.field) ?? []; track item.id) {
476
+ <option [value]="item.id">{{ item.displayName }}</option>
477
+ }
478
+ </select>
479
+ }
480
+ @case ('manyToOne') {
481
+ <select
482
+ class="select"
483
+ [value]="formData()[field.field] ?? ''"
484
+ (change)="onFieldChange(field, $any($event.target).value)"
485
+ >
486
+ <option value="">(空)</option>
487
+ @for (item of relatedItemsMap().get(field.field) ?? []; track item.id) {
488
+ <option [value]="item.id">{{ item.displayName }}</option>
489
+ }
490
+ </select>
491
+ }
492
+ @default {
493
+ <input
494
+ class="input"
495
+ [attr.placeholder]="field.placeholder ?? ''"
496
+ [value]="formData()[field.field] ?? ''"
497
+ (change)="onFieldChange(field, $any($event.target).value)"
498
+ type="text"
499
+ />
500
+ }
501
+ }
502
+ }
503
+ @if (field.helpText) {
504
+ <p class="label">{{ field.helpText }}</p>
505
+ }
506
+ </fieldset>
507
+ }
508
+ @if (!isReadonly() && showActions()) {
509
+ <div class="flex justify-end gap-2 pt-2 md:col-span-2">
510
+ <button class="btn" (click)="onCancel()" type="button">取消</button>
511
+ <button class="btn btn-primary" type="submit">保存</button>
512
+ </div>
513
+ }
514
+ </form>`,
515
+ changeDetection: ChangeDetectionStrategy.OnPush
516
+ }]
517
+ }], propDecorators: { fields: [{ type: i0.Input, args: [{ isSignal: true, alias: "fields", required: true }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: true }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], relatedEntityProvider: [{ type: i0.Input, args: [{ isSignal: true, alias: "relatedEntityProvider", required: false }] }], showActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "showActions", required: false }] }], fieldChanged: [{ type: i0.Output, args: ["fieldChanged"] }], formSubmitted: [{ type: i0.Output, args: ["formSubmitted"] }], formCancelled: [{ type: i0.Output, args: ["formCancelled"] }], validationErrors: [{ type: i0.Output, args: ["validationErrors"] }] } });
518
+
519
+ const ENTITY_TABLE_CONFIG = new InjectionToken('ENTITY_TABLE_CONFIG');
520
+
521
+ /**
522
+ * 通用实体数据表格组件
523
+ *
524
+ * 基于 VTable 提供高性能表格渲染,支持:
525
+ * - 单元格编辑(文本、开关、复选框、下拉)
526
+ * - 剪贴板操作(Ctrl+C/V,TSV 格式)
527
+ * - 拖拽排序
528
+ * - 键盘导航(Enter 切换开关,Delete 清空单元格)
529
+ * - 主题系统(daisyUI CSS 变量)
530
+ * - 校验错误 tooltip
531
+ */
532
+ class EntityTableComponent {
533
+ #platformId = inject(PLATFORM_ID);
534
+ #config = inject(ENTITY_TABLE_CONFIG, { optional: true });
535
+ #tableInstance = null;
536
+ #destroyed = false;
537
+ #tableReady = signal(false, ...(ngDevMode ? [{ debugName: "#tableReady" }] : /* istanbul ignore next */ []));
538
+ #prevColumns = null;
539
+ #prevRecords = null;
540
+ #selectedCell = null;
541
+ #containerKeydownHandler = null;
542
+ #resizeObserver = null;
543
+ #cellTooltip = signal(null, ...(ngDevMode ? [{ debugName: "#cellTooltip" }] : /* istanbul ignore next */ []));
544
+ #headerHeight = signal(40, ...(ngDevMode ? [{ debugName: "#headerHeight" }] : /* istanbul ignore next */ []));
545
+ #clipboard = new TableClipboardManager();
546
+ #tooltip = new CellTooltipManager();
547
+ #container = null;
548
+ tableContainer = viewChild('tableContainer', ...(ngDevMode ? [{ debugName: "tableContainer" }] : /* istanbul ignore next */ []));
549
+ // ── 输入 ──────────────────────────────────────────────────────────
550
+ records = input.required(...(ngDevMode ? [{ debugName: "records" }] : /* istanbul ignore next */ []));
551
+ columns = input.required(...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
552
+ isDarkMode = input(false, ...(ngDevMode ? [{ debugName: "isDarkMode" }] : /* istanbul ignore next */ []));
553
+ idField = input('id', ...(ngDevMode ? [{ debugName: "idField" }] : /* istanbul ignore next */ []));
554
+ nonClearableFields = input(new Set(), ...(ngDevMode ? [{ debugName: "nonClearableFields" }] : /* istanbul ignore next */ []));
555
+ tableOptions = input(...(ngDevMode ? [undefined, { debugName: "tableOptions" }] : /* istanbul ignore next */ []));
556
+ cellErrorDetector = input(...(ngDevMode ? [undefined, { debugName: "cellErrorDetector" }] : /* istanbul ignore next */ []));
557
+ loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
558
+ /** 业务层回调判断单元格是否可清空(返回 false 跳过),替代领域特定的硬编码逻辑 */
559
+ cellClearable = input(...(ngDevMode ? [undefined, { debugName: "cellClearable" }] : /* istanbul ignore next */ []));
560
+ /** 触底时加载更多数据的回调,由表格内部在滚动到底部时调用 */
561
+ loadMore = input(...(ngDevMode ? [undefined, { debugName: "loadMore" }] : /* istanbul ignore next */ []));
562
+ /** 加载更多中状态(已有数据,底部小 spinner,不拦截滚动) */
563
+ loadingMore = input(false, ...(ngDevMode ? [{ debugName: "loadingMore" }] : /* istanbul ignore next */ []));
564
+ // ── 输出 ──────────────────────────────────────────────────────────
565
+ cellChanged = output();
566
+ rowDeleted = output();
567
+ iconClicked = output();
568
+ batchUpdated = output();
569
+ rowReordered = output();
570
+ scrollNearBottom = output();
571
+ cellTooltip = this.#cellTooltip;
572
+ headerHeight = this.#headerHeight;
573
+ get tableInstance() {
574
+ return this.#tableInstance;
575
+ }
576
+ constructor() {
577
+ if (isPlatformBrowser(this.#platformId)) {
578
+ afterNextRender(() => {
579
+ if (this.#destroyed)
580
+ return;
581
+ const el = this.tableContainer()?.nativeElement;
582
+ if (el && !this.#tableInstance && this.columns()?.length) {
583
+ this.#initTable(el);
584
+ this.#prevColumns = this.columns();
585
+ this.#tableReady.set(true);
586
+ }
587
+ });
588
+ effect(() => {
589
+ const records = this.records();
590
+ const columns = this.columns();
591
+ const ready = untracked(() => this.#tableReady());
592
+ if (!this.#tableInstance) {
593
+ if (!ready && columns?.length) {
594
+ const el = this.tableContainer()?.nativeElement;
595
+ if (el) {
596
+ this.#initTable(el);
597
+ this.#prevColumns = columns;
598
+ this.#tableReady.set(true);
599
+ }
600
+ }
601
+ return;
602
+ }
603
+ const columnsChanged = columns !== this.#prevColumns;
604
+ if (columnsChanged) {
605
+ updateTableRecords(this.#tableInstance, records, this.#prevColumns ?? columns, columns);
606
+ this.#prevColumns = columns;
607
+ this.#prevRecords = records;
608
+ }
609
+ else if (records !== this.#prevRecords) {
610
+ this.#tableInstance.setRecords(records);
611
+ this.#prevRecords = records;
612
+ }
613
+ });
614
+ effect(() => {
615
+ this.isDarkMode();
616
+ this.redrawTheme();
617
+ });
618
+ }
619
+ }
620
+ /**
621
+ * 从业务层回滚单元格值(校验失败时恢复原值)。
622
+ * 避免外部直接访问 tableInstance。
623
+ */
624
+ changeCellValue(col, row, value) {
625
+ if (this.#tableInstance)
626
+ changeCellValue(this.#tableInstance, col, row, value);
627
+ }
628
+ redrawTheme() {
629
+ if (this.#tableInstance) {
630
+ this.#tableInstance.updateTheme(createTheme(this.isDarkMode(), getCSSVariables()));
631
+ }
632
+ }
633
+ ngOnDestroy() {
634
+ this.#destroyed = true;
635
+ this.#tooltip.cleanup();
636
+ if (this.#containerKeydownHandler) {
637
+ this.#container?.removeEventListener('keydown', this.#containerKeydownHandler);
638
+ this.#containerKeydownHandler = null;
639
+ }
640
+ this.#resizeObserver?.disconnect();
641
+ this.#resizeObserver = null;
642
+ if (this.#tableInstance) {
643
+ completeTableEdit(this.#tableInstance);
644
+ this.#tableInstance.release();
645
+ }
646
+ this.#tableInstance = null;
647
+ this.#container = null;
648
+ this.#tableReady.set(false);
649
+ }
650
+ #initTable(container) {
651
+ const records = this.records();
652
+ const table = createListTable(container, records, this.columns(), this.isDarkMode(), this.tableOptions());
653
+ this.#tableInstance = table;
654
+ this.#prevRecords = records;
655
+ this.#container = container;
656
+ const h = table.getRowHeight(0);
657
+ if (h > 0)
658
+ this.#headerHeight.set(h);
659
+ this.#resizeObserver = new ResizeObserver(() => table.resize());
660
+ this.#resizeObserver.observe(container);
661
+ table.updateTheme(createTheme(this.isDarkMode(), getCSSVariables()));
662
+ patchDragIconForReadonlyRows(table);
663
+ this.#bindTableEvents(table, container);
664
+ }
665
+ #bindTableEvents(table, container) {
666
+ table.on('change_cell_value', (args) => {
667
+ this.#handleCellChange(args.col, args.row, args.changedValue);
668
+ });
669
+ table.on('switch_state_change', (args) => {
670
+ this.#handleCellChange(args.col, args.row, args.checked);
671
+ });
672
+ table.on('checkbox_state_change', (args) => {
673
+ this.#handleCellChange(args.col, args.row, args.checked);
674
+ });
675
+ table.on('icon_click', (args) => {
676
+ completeTableEdit(table);
677
+ const record = table.getRecordByCell(args.col, args.row);
678
+ if (!record)
679
+ return;
680
+ if (args.name === 'delete-action')
681
+ this.rowDeleted.emit(record);
682
+ else
683
+ this.iconClicked.emit({ name: args.name, record });
684
+ });
685
+ table.on('mouseenter_cell', (args) => this.#showCellError(args.col, args.row));
686
+ table.on('mouseleave_cell', () => this.#tooltip.hide(v => this.#cellTooltip.set(v)));
687
+ table.on('selected_cell', (args) => {
688
+ this.#selectedCell = { col: args.col, row: args.row };
689
+ });
690
+ table.on('change_header_position', () => {
691
+ const ids = collectReorderedIds(table, this.idField());
692
+ if (ids.length > 0)
693
+ this.rowReordered.emit(ids);
694
+ });
695
+ table.on('scroll', (args) => {
696
+ if (args.scrollDirection === 'vertical' &&
697
+ (args.dy ?? 0) > 0 &&
698
+ (args.scrollRatioY ?? 0) > 0.9 &&
699
+ !this.loadingMore()) {
700
+ const fn = this.loadMore();
701
+ if (fn) {
702
+ fn();
703
+ }
704
+ else {
705
+ this.scrollNearBottom.emit();
706
+ }
707
+ }
708
+ });
709
+ this.#containerKeydownHandler = (e) => handleTableKeydown({
710
+ getTable: () => this.#tableInstance,
711
+ getSelectedCell: () => this.#selectedCell,
712
+ getColDef: col => this.#getColDef(col),
713
+ onCopy: () => void this.#clipboard.copy(table, this.columns()),
714
+ onPaste: () => void this.#pasteFromClipboard(),
715
+ onDelete: () => this.#deleteSelectedCells(),
716
+ onCellChange: (col, row, val) => this.#handleCellChange(col, row, val)
717
+ }, e);
718
+ container.addEventListener('keydown', this.#containerKeydownHandler);
719
+ }
720
+ #getColDef(col) {
721
+ return this.columns()?.[col - ROW_SERIES_COL_OFFSET];
722
+ }
723
+ #handleCellChange(col, row, changedValue) {
724
+ const record = this.#tableInstance?.getRecordByCell(col, row);
725
+ const fieldName = this.#getColDef(col)?.field;
726
+ if (!record || !fieldName)
727
+ return;
728
+ this.cellChanged.emit({ col, row, field: fieldName, value: changedValue, record });
729
+ }
730
+ #showCellError(col, row) {
731
+ const table = this.#tableInstance;
732
+ if (!table)
733
+ return;
734
+ const detector = this.cellErrorDetector();
735
+ if (!detector)
736
+ return;
737
+ const record = table.getRecordByCell(col, row);
738
+ if (!record)
739
+ return;
740
+ const field = this.#getColDef(col)?.field;
741
+ if (!field)
742
+ return;
743
+ const content = detector(record, field);
744
+ if (!content)
745
+ return;
746
+ this.#tooltip.showError(table, col, row, content, v => this.#cellTooltip.set(v), this.#config?.tooltipDelay);
747
+ }
748
+ /** 将 PendingWrite[] 按 idField 归并为 BatchChangeItem[] */
749
+ #groupWrites(writes) {
750
+ const map = new Map();
751
+ for (const w of writes) {
752
+ const id = String(w.record[this.idField()]);
753
+ if (!map.has(id))
754
+ map.set(id, {});
755
+ map.get(id)[w.field] = w.value;
756
+ }
757
+ return [...map.entries()].map(([recordId, changes]) => ({ recordId, changes }));
758
+ }
759
+ async #pasteFromClipboard() {
760
+ if (!this.#tableInstance)
761
+ return;
762
+ const writes = await this.#clipboard.paste(this.#tableInstance, this.columns());
763
+ if (writes.length > 0) {
764
+ for (const w of writes) {
765
+ if (typeof w.value === 'boolean') {
766
+ setCellSwitchState(this.#tableInstance, w.col, w.row, w.value);
767
+ }
768
+ }
769
+ this.batchUpdated.emit(this.#groupWrites(writes));
770
+ }
771
+ }
772
+ #deleteSelectedCells() {
773
+ const table = this.#tableInstance;
774
+ if (!table)
775
+ return;
776
+ const columns = this.columns();
777
+ const { writes, boolCells } = collectDeleteWrites(table, columns, this.nonClearableFields(), this.cellClearable());
778
+ for (const bc of boolCells)
779
+ setCellSwitchState(table, bc.col, bc.row, false);
780
+ if (writes.length > 0)
781
+ this.batchUpdated.emit(this.#groupWrites(writes));
782
+ }
783
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: EntityTableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
784
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: EntityTableComponent, isStandalone: true, selector: "rxdb-entity-table", inputs: { records: { classPropertyName: "records", publicName: "records", isSignal: true, isRequired: true, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, isDarkMode: { classPropertyName: "isDarkMode", publicName: "isDarkMode", isSignal: true, isRequired: false, transformFunction: null }, idField: { classPropertyName: "idField", publicName: "idField", isSignal: true, isRequired: false, transformFunction: null }, nonClearableFields: { classPropertyName: "nonClearableFields", publicName: "nonClearableFields", isSignal: true, isRequired: false, transformFunction: null }, tableOptions: { classPropertyName: "tableOptions", publicName: "tableOptions", isSignal: true, isRequired: false, transformFunction: null }, cellErrorDetector: { classPropertyName: "cellErrorDetector", publicName: "cellErrorDetector", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, cellClearable: { classPropertyName: "cellClearable", publicName: "cellClearable", isSignal: true, isRequired: false, transformFunction: null }, loadMore: { classPropertyName: "loadMore", publicName: "loadMore", isSignal: true, isRequired: false, transformFunction: null }, loadingMore: { classPropertyName: "loadingMore", publicName: "loadingMore", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { cellChanged: "cellChanged", rowDeleted: "rowDeleted", iconClicked: "iconClicked", batchUpdated: "batchUpdated", rowReordered: "rowReordered", scrollNearBottom: "scrollNearBottom" }, viewQueries: [{ propertyName: "tableContainer", first: true, predicate: ["tableContainer"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"relative h-full w-full overflow-hidden\">\n <div class=\"absolute inset-0\" #tableContainer></div>\n @if (records().length === 0 && !loading()) {\n <div\n class=\"text-base-content/40 absolute inset-x-0 bottom-0 flex items-center justify-center text-sm\"\n [style.top.px]=\"headerHeight()\"\n aria-label=\"\u6682\u65E0\u6570\u636E\"\n role=\"status\"\n >\n <div class=\"empty-slot\">\n <ng-content select=\"[emptyState]\"></ng-content>\n </div>\n <div class=\"empty-default flex flex-col items-center gap-3\">\n <svg\n aria-hidden=\"true\"\n fill=\"none\"\n height=\"40\"\n stroke=\"currentColor\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"1.2\"\n viewBox=\"0 0 24 24\"\n width=\"40\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <rect height=\"18\" rx=\"2\" width=\"18\" x=\"3\" y=\"3\" />\n <path d=\"M3 9h18\" />\n <path d=\"M9 9v12\" />\n <path d=\"M3 15h18\" />\n </svg>\n <span>\u6682\u65E0\u6570\u636E</span>\n </div>\n </div>\n }\n @if (loading()) {\n <div class=\"bg-base-100/60 absolute inset-0 z-10 flex items-center justify-center\">\n <span class=\"loading loading-spinner loading-md text-primary\"></span>\n </div>\n }\n @if (loadingMore()) {\n <div class=\"pointer-events-none absolute right-0 bottom-0 left-0 z-10 flex justify-center py-1.5\">\n <span class=\"loading loading-dots loading-sm text-primary\"></span>\n </div>\n }\n @if (cellTooltip(); as tip) {\n <div\n class=\"bg-base-300 text-base-content border-base-300 pointer-events-none fixed z-[100] max-w-xs rounded border px-2 py-1.5 text-xs shadow-lg\"\n [style.left.px]=\"tip.x\"\n [style.top.px]=\"tip.y\"\n aria-atomic=\"true\"\n aria-live=\"assertive\"\n role=\"tooltip\"\n >\n {{ tip.content }}\n </div>\n }\n</div>\n", styles: [":host{display:block;width:100%;height:100%;min-height:200px}.empty-slot:has(>*)~.empty-default{display:none}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
785
+ }
786
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: EntityTableComponent, decorators: [{
787
+ type: Component,
788
+ args: [{ selector: 'rxdb-entity-table', imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"relative h-full w-full overflow-hidden\">\n <div class=\"absolute inset-0\" #tableContainer></div>\n @if (records().length === 0 && !loading()) {\n <div\n class=\"text-base-content/40 absolute inset-x-0 bottom-0 flex items-center justify-center text-sm\"\n [style.top.px]=\"headerHeight()\"\n aria-label=\"\u6682\u65E0\u6570\u636E\"\n role=\"status\"\n >\n <div class=\"empty-slot\">\n <ng-content select=\"[emptyState]\"></ng-content>\n </div>\n <div class=\"empty-default flex flex-col items-center gap-3\">\n <svg\n aria-hidden=\"true\"\n fill=\"none\"\n height=\"40\"\n stroke=\"currentColor\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"1.2\"\n viewBox=\"0 0 24 24\"\n width=\"40\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <rect height=\"18\" rx=\"2\" width=\"18\" x=\"3\" y=\"3\" />\n <path d=\"M3 9h18\" />\n <path d=\"M9 9v12\" />\n <path d=\"M3 15h18\" />\n </svg>\n <span>\u6682\u65E0\u6570\u636E</span>\n </div>\n </div>\n }\n @if (loading()) {\n <div class=\"bg-base-100/60 absolute inset-0 z-10 flex items-center justify-center\">\n <span class=\"loading loading-spinner loading-md text-primary\"></span>\n </div>\n }\n @if (loadingMore()) {\n <div class=\"pointer-events-none absolute right-0 bottom-0 left-0 z-10 flex justify-center py-1.5\">\n <span class=\"loading loading-dots loading-sm text-primary\"></span>\n </div>\n }\n @if (cellTooltip(); as tip) {\n <div\n class=\"bg-base-300 text-base-content border-base-300 pointer-events-none fixed z-[100] max-w-xs rounded border px-2 py-1.5 text-xs shadow-lg\"\n [style.left.px]=\"tip.x\"\n [style.top.px]=\"tip.y\"\n aria-atomic=\"true\"\n aria-live=\"assertive\"\n role=\"tooltip\"\n >\n {{ tip.content }}\n </div>\n }\n</div>\n", styles: [":host{display:block;width:100%;height:100%;min-height:200px}.empty-slot:has(>*)~.empty-default{display:none}\n"] }]
789
+ }], ctorParameters: () => [], propDecorators: { tableContainer: [{ type: i0.ViewChild, args: ['tableContainer', { isSignal: true }] }], records: [{ type: i0.Input, args: [{ isSignal: true, alias: "records", required: true }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: true }] }], isDarkMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "isDarkMode", required: false }] }], idField: [{ type: i0.Input, args: [{ isSignal: true, alias: "idField", required: false }] }], nonClearableFields: [{ type: i0.Input, args: [{ isSignal: true, alias: "nonClearableFields", required: false }] }], tableOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "tableOptions", required: false }] }], cellErrorDetector: [{ type: i0.Input, args: [{ isSignal: true, alias: "cellErrorDetector", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], cellClearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "cellClearable", required: false }] }], loadMore: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadMore", required: false }] }], loadingMore: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingMore", required: false }] }], cellChanged: [{ type: i0.Output, args: ["cellChanged"] }], rowDeleted: [{ type: i0.Output, args: ["rowDeleted"] }], iconClicked: [{ type: i0.Output, args: ["iconClicked"] }], batchUpdated: [{ type: i0.Output, args: ["batchUpdated"] }], rowReordered: [{ type: i0.Output, args: ["rowReordered"] }], scrollNearBottom: [{ type: i0.Output, args: ["scrollNearBottom"] }] } });
790
+
791
+ /**
792
+ * 集成查询构建器的实体表格组件
793
+ *
794
+ * 在实体表格上方提供筛选区域位置(通过内容投影),
795
+ * 将 rxdb-query-builder 的查询结果传递给底层 EntityTableComponent。
796
+ *
797
+ * 使用方式:
798
+ * ```html
799
+ * <rxdb-query-table [records]="filteredRecords()" [columns]="columns">
800
+ * <div filterBar>
801
+ * <rxdb-query-builder [fields]="fields" (queryChange)="onQueryChange($event)" />
802
+ * </div>
803
+ * <div emptyState>暂无数据</div>
804
+ * </rxdb-query-table>
805
+ * ```
806
+ */
807
+ class QueryTableComponent {
808
+ records = input.required(...(ngDevMode ? [{ debugName: "records" }] : /* istanbul ignore next */ []));
809
+ columns = input.required(...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
810
+ idField = input('id', ...(ngDevMode ? [{ debugName: "idField" }] : /* istanbul ignore next */ []));
811
+ nonClearableFields = input(new Set(), ...(ngDevMode ? [{ debugName: "nonClearableFields" }] : /* istanbul ignore next */ []));
812
+ tableOptions = input(...(ngDevMode ? [undefined, { debugName: "tableOptions" }] : /* istanbul ignore next */ []));
813
+ cellErrorDetector = input(...(ngDevMode ? [undefined, { debugName: "cellErrorDetector" }] : /* istanbul ignore next */ []));
814
+ isDarkMode = input(false, ...(ngDevMode ? [{ debugName: "isDarkMode" }] : /* istanbul ignore next */ []));
815
+ /** 业务层回调判断单元格是否可清空 */
816
+ cellClearable = input(...(ngDevMode ? [undefined, { debugName: "cellClearable" }] : /* istanbul ignore next */ []));
817
+ /** 查询条件是否激活(显示筛选状态标签) */
818
+ queryActive = input(false, ...(ngDevMode ? [{ debugName: "queryActive" }] : /* istanbul ignore next */ []));
819
+ /** 结果总数(显示在筛选状态栏中) */
820
+ totalCount = input(...(ngDevMode ? [undefined, { debugName: "totalCount" }] : /* istanbul ignore next */ []));
821
+ /** 筛选后结果数 */
822
+ filteredCount = input(...(ngDevMode ? [undefined, { debugName: "filteredCount" }] : /* istanbul ignore next */ []));
823
+ /** 是否显示加载状态 */
824
+ loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
825
+ /** 加载更多中(已有数据,底部小 spinner,不拦截滚动) */
826
+ loadingMore = input(false, ...(ngDevMode ? [{ debugName: "loadingMore" }] : /* istanbul ignore next */ []));
827
+ /** 触底时加载更多数据的回调,透传给 EntityTableComponent */
828
+ loadMore = input(...(ngDevMode ? [undefined, { debugName: "loadMore" }] : /* istanbul ignore next */ []));
829
+ cellChanged = output();
830
+ rowDeleted = output();
831
+ iconClicked = output();
832
+ batchUpdated = output();
833
+ rowReordered = output();
834
+ scrollNearBottom = output();
835
+ entityTable = viewChild(EntityTableComponent, ...(ngDevMode ? [{ debugName: "entityTable" }] : /* istanbul ignore next */ []));
836
+ statusText = computed(() => {
837
+ const total = this.totalCount();
838
+ const filtered = this.filteredCount();
839
+ if (total == null)
840
+ return '';
841
+ if (filtered != null && filtered !== total)
842
+ return `${filtered} / ${total}`;
843
+ return `${total}`;
844
+ }, ...(ngDevMode ? [{ debugName: "statusText" }] : /* istanbul ignore next */ []));
845
+ get tableInstance() {
846
+ return this.entityTable()?.tableInstance ?? null;
847
+ }
848
+ changeCellValue(col, row, value) {
849
+ this.entityTable()?.changeCellValue(col, row, value);
850
+ }
851
+ redrawTheme() {
852
+ this.entityTable()?.redrawTheme();
853
+ }
854
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: QueryTableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
855
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: QueryTableComponent, isStandalone: true, selector: "rxdb-query-table", inputs: { records: { classPropertyName: "records", publicName: "records", isSignal: true, isRequired: true, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, idField: { classPropertyName: "idField", publicName: "idField", isSignal: true, isRequired: false, transformFunction: null }, nonClearableFields: { classPropertyName: "nonClearableFields", publicName: "nonClearableFields", isSignal: true, isRequired: false, transformFunction: null }, tableOptions: { classPropertyName: "tableOptions", publicName: "tableOptions", isSignal: true, isRequired: false, transformFunction: null }, cellErrorDetector: { classPropertyName: "cellErrorDetector", publicName: "cellErrorDetector", isSignal: true, isRequired: false, transformFunction: null }, isDarkMode: { classPropertyName: "isDarkMode", publicName: "isDarkMode", isSignal: true, isRequired: false, transformFunction: null }, cellClearable: { classPropertyName: "cellClearable", publicName: "cellClearable", isSignal: true, isRequired: false, transformFunction: null }, queryActive: { classPropertyName: "queryActive", publicName: "queryActive", isSignal: true, isRequired: false, transformFunction: null }, totalCount: { classPropertyName: "totalCount", publicName: "totalCount", isSignal: true, isRequired: false, transformFunction: null }, filteredCount: { classPropertyName: "filteredCount", publicName: "filteredCount", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, loadingMore: { classPropertyName: "loadingMore", publicName: "loadingMore", isSignal: true, isRequired: false, transformFunction: null }, loadMore: { classPropertyName: "loadMore", publicName: "loadMore", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { cellChanged: "cellChanged", rowDeleted: "rowDeleted", iconClicked: "iconClicked", batchUpdated: "batchUpdated", rowReordered: "rowReordered", scrollNearBottom: "scrollNearBottom" }, viewQueries: [{ propertyName: "entityTable", first: true, predicate: EntityTableComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"flex h-full w-full flex-col overflow-hidden\">\n <!-- \u7B5B\u9009\u680F\u63D2\u69FD -->\n <ng-content select=\"[filterBar]\"></ng-content>\n\n <!-- \u7B5B\u9009\u72B6\u6001\u680F -->\n @if (queryActive() && statusText()) {\n <div class=\"text-base-content/60 flex items-center gap-2 border-b px-3 py-1 text-xs\">\n <span>{{ statusText() }}</span>\n </div>\n }\n\n <!-- \u8868\u683C\u4E3B\u4F53 -->\n <div class=\"min-h-0 flex-1\">\n <rxdb-entity-table\n [cellClearable]=\"cellClearable()\"\n [cellErrorDetector]=\"cellErrorDetector()\"\n [columns]=\"columns()\"\n [idField]=\"idField()\"\n [isDarkMode]=\"isDarkMode()\"\n [loading]=\"loading()\"\n [loadingMore]=\"loadingMore()\"\n [loadMore]=\"loadMore()\"\n [nonClearableFields]=\"nonClearableFields()\"\n [records]=\"records()\"\n [tableOptions]=\"tableOptions()\"\n (batchUpdated)=\"batchUpdated.emit($event)\"\n (cellChanged)=\"cellChanged.emit($event)\"\n (iconClicked)=\"iconClicked.emit($event)\"\n (rowDeleted)=\"rowDeleted.emit($event)\"\n (rowReordered)=\"rowReordered.emit($event)\"\n (scrollNearBottom)=\"scrollNearBottom.emit()\"\n >\n <ng-content select=\"[emptyState]\"></ng-content>\n </rxdb-entity-table>\n </div>\n</div>\n", styles: [":host{display:block;width:100%;height:100%}\n"], dependencies: [{ kind: "component", type: EntityTableComponent, selector: "rxdb-entity-table", inputs: ["records", "columns", "isDarkMode", "idField", "nonClearableFields", "tableOptions", "cellErrorDetector", "loading", "cellClearable", "loadMore", "loadingMore"], outputs: ["cellChanged", "rowDeleted", "iconClicked", "batchUpdated", "rowReordered", "scrollNearBottom"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
856
+ }
857
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: QueryTableComponent, decorators: [{
858
+ type: Component,
859
+ args: [{ selector: 'rxdb-query-table', imports: [EntityTableComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"flex h-full w-full flex-col overflow-hidden\">\n <!-- \u7B5B\u9009\u680F\u63D2\u69FD -->\n <ng-content select=\"[filterBar]\"></ng-content>\n\n <!-- \u7B5B\u9009\u72B6\u6001\u680F -->\n @if (queryActive() && statusText()) {\n <div class=\"text-base-content/60 flex items-center gap-2 border-b px-3 py-1 text-xs\">\n <span>{{ statusText() }}</span>\n </div>\n }\n\n <!-- \u8868\u683C\u4E3B\u4F53 -->\n <div class=\"min-h-0 flex-1\">\n <rxdb-entity-table\n [cellClearable]=\"cellClearable()\"\n [cellErrorDetector]=\"cellErrorDetector()\"\n [columns]=\"columns()\"\n [idField]=\"idField()\"\n [isDarkMode]=\"isDarkMode()\"\n [loading]=\"loading()\"\n [loadingMore]=\"loadingMore()\"\n [loadMore]=\"loadMore()\"\n [nonClearableFields]=\"nonClearableFields()\"\n [records]=\"records()\"\n [tableOptions]=\"tableOptions()\"\n (batchUpdated)=\"batchUpdated.emit($event)\"\n (cellChanged)=\"cellChanged.emit($event)\"\n (iconClicked)=\"iconClicked.emit($event)\"\n (rowDeleted)=\"rowDeleted.emit($event)\"\n (rowReordered)=\"rowReordered.emit($event)\"\n (scrollNearBottom)=\"scrollNearBottom.emit()\"\n >\n <ng-content select=\"[emptyState]\"></ng-content>\n </rxdb-entity-table>\n </div>\n</div>\n", styles: [":host{display:block;width:100%;height:100%}\n"] }]
860
+ }], propDecorators: { records: [{ type: i0.Input, args: [{ isSignal: true, alias: "records", required: true }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: true }] }], idField: [{ type: i0.Input, args: [{ isSignal: true, alias: "idField", required: false }] }], nonClearableFields: [{ type: i0.Input, args: [{ isSignal: true, alias: "nonClearableFields", required: false }] }], tableOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "tableOptions", required: false }] }], cellErrorDetector: [{ type: i0.Input, args: [{ isSignal: true, alias: "cellErrorDetector", required: false }] }], isDarkMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "isDarkMode", required: false }] }], cellClearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "cellClearable", required: false }] }], queryActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryActive", required: false }] }], totalCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalCount", required: false }] }], filteredCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "filteredCount", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], loadingMore: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingMore", required: false }] }], loadMore: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadMore", required: false }] }], cellChanged: [{ type: i0.Output, args: ["cellChanged"] }], rowDeleted: [{ type: i0.Output, args: ["rowDeleted"] }], iconClicked: [{ type: i0.Output, args: ["iconClicked"] }], batchUpdated: [{ type: i0.Output, args: ["batchUpdated"] }], rowReordered: [{ type: i0.Output, args: ["rowReordered"] }], scrollNearBottom: [{ type: i0.Output, args: ["scrollNearBottom"] }], entityTable: [{ type: i0.ViewChild, args: [i0.forwardRef(() => EntityTableComponent), { isSignal: true }] }] } });
861
+
862
+ let _uid$1 = 0;
863
+ /**
864
+ * 树节点 Directive,实现 CDK {@link Highlightable} 接口
865
+ *
866
+ * 用于 {@link ActiveDescendantKeyManager} 管理键盘导航时的激活样式。
867
+ * `exportAs: 'rxdbTreeItem'` 允许模板中通过 `#ref="rxdbTreeItem"` 访问实例。
868
+ */
869
+ class TreeItemDirective {
870
+ _active = signal(false, ...(ngDevMode ? [{ debugName: "_active" }] : /* istanbul ignore next */ []));
871
+ isActive = this._active.asReadonly();
872
+ setActiveStyles() {
873
+ this._active.set(true);
874
+ }
875
+ setInactiveStyles() {
876
+ this._active.set(false);
877
+ }
878
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: TreeItemDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
879
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.5", type: TreeItemDirective, isStandalone: true, selector: "[rxdbTreeItem]", exportAs: ["rxdbTreeItem"], ngImport: i0 });
880
+ }
881
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: TreeItemDirective, decorators: [{
882
+ type: Directive,
883
+ args: [{
884
+ selector: '[rxdbTreeItem]',
885
+ standalone: true,
886
+ exportAs: 'rxdbTreeItem'
887
+ }]
888
+ }] });
889
+ /**
890
+ * 树形下拉选择组件
891
+ *
892
+ * @description
893
+ * 基于 Popover API 和 `@angular/cdk/a11y` 的树形字段选择器:
894
+ * - 使用原生 Popover API,在 `overflow` 容器中不被裁剪
895
+ * - 支持关键字过滤(自动展开匹配路径)
896
+ * - 支持展开/折叠关系节点
897
+ * - 键盘导航:↑↓ 移动、→ 展开、← 折叠、Enter 选中、Escape 关闭
898
+ * - 使用 CDK `ActiveDescendantKeyManager` + `aria-activedescendant` 满足 WAI-ARIA 无障碍规范
899
+ *
900
+ * @example
901
+ * ```html
902
+ * <rxdb-tree-select
903
+ * [nodes]="fieldTree()"
904
+ * [selected]="selectedField()"
905
+ * placeholder="选择字段"
906
+ * (selectChange)="onFieldSelect($event)"
907
+ * />
908
+ * ```
909
+ */
910
+ class TreeSelectComponent {
911
+ trigger = viewChild.required('trigger');
912
+ popoverEl = viewChild.required('popoverEl');
913
+ filterInput = viewChild('filterInput', ...(ngDevMode ? [{ debugName: "filterInput" }] : /* istanbul ignore next */ []));
914
+ treeItemDirs = viewChildren(TreeItemDirective, ...(ngDevMode ? [{ debugName: "treeItemDirs" }] : /* istanbul ignore next */ []));
915
+ expandedPaths = signal(new Set(), ...(ngDevMode ? [{ debugName: "expandedPaths" }] : /* istanbul ignore next */ []));
916
+ keyManager;
917
+ _activeIdx = signal(-1, ...(ngDevMode ? [{ debugName: "_activeIdx" }] : /* istanbul ignore next */ []));
918
+ uid = `rxdb-ts-${++_uid$1}`;
919
+ listId = `${this.uid}-list`;
920
+ filterText = signal('', ...(ngDevMode ? [{ debugName: "filterText" }] : /* istanbul ignore next */ []));
921
+ ChevronDownIcon = ChevronDownIcon;
922
+ ChevronRightIcon = ChevronRightIcon;
923
+ typeIconMap = {
924
+ string: TypeIcon,
925
+ number: HashIcon,
926
+ boolean: ToggleLeftIcon,
927
+ date: CalendarIcon,
928
+ array: ListIcon,
929
+ object: BracesIcon,
930
+ relation: Link2Icon,
931
+ enum: ListChecksIcon,
932
+ keyValue: TableIcon
933
+ };
934
+ typeColorMap = {
935
+ string: 'text-success',
936
+ number: 'text-warning',
937
+ boolean: 'text-info',
938
+ date: 'text-secondary',
939
+ array: 'text-accent',
940
+ object: 'text-base-content/60',
941
+ relation: 'text-primary',
942
+ enum: 'text-warning',
943
+ keyValue: 'text-accent'
944
+ };
945
+ defaultTypeIcon = TypeIcon;
946
+ defaultTypeColor = 'text-base-content/40';
947
+ nodes = input.required(...(ngDevMode ? [{ debugName: "nodes" }] : /* istanbul ignore next */ []));
948
+ selected = input('', ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
949
+ placeholder = input('请选择', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
950
+ minWidth = input('14rem', ...(ngDevMode ? [{ debugName: "minWidth" }] : /* istanbul ignore next */ []));
951
+ selectChange = output();
952
+ displayValue = computed(() => {
953
+ const sel = this.selected();
954
+ return sel ? getDisplayNamePath(this.nodes(), sel) || sel : this.placeholder();
955
+ }, ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
956
+ selectedNode = computed(() => {
957
+ const sel = this.selected();
958
+ return sel ? findNodeByValue(this.nodes(), sel) : null;
959
+ }, ...(ngDevMode ? [{ debugName: "selectedNode" }] : /* istanbul ignore next */ []));
960
+ visibleItems = computed(() => {
961
+ const filter = this.filterText().toLowerCase().trim();
962
+ const expanded = this.expandedPaths();
963
+ return filter ? buildFilteredItems(this.nodes(), filter) : buildExpandedItems(this.nodes(), expanded, 0);
964
+ }, ...(ngDevMode ? [{ debugName: "visibleItems" }] : /* istanbul ignore next */ []));
965
+ activeItemId = computed(() => {
966
+ const idx = this._activeIdx();
967
+ if (idx < 0)
968
+ return null;
969
+ const vis = this.visibleItems();
970
+ return idx < vis.length ? `${this.uid}-item-${vis[idx].node.value}` : null;
971
+ }, ...(ngDevMode ? [{ debugName: "activeItemId" }] : /* istanbul ignore next */ []));
972
+ constructor() {
973
+ const injector = inject(Injector);
974
+ // Signal 初始化必须在 injection context 内(constructor)
975
+ this.keyManager = new ActiveDescendantKeyManager(this.treeItemDirs, injector).withWrap().withHomeAndEnd();
976
+ // 同步 key manager 的激活索引到本地 signal(用于 aria-activedescendant)
977
+ this.keyManager.change.pipe(takeUntilDestroyed()).subscribe(idx => this._activeIdx.set(idx));
978
+ // 过滤文本变化时,自动高亮第一项
979
+ effect(() => {
980
+ this.filterText();
981
+ untracked(() => {
982
+ setTimeout(() => {
983
+ this.keyManager.setFirstItemActive();
984
+ this._activeIdx.set(this.keyManager.activeItemIndex ?? -1);
985
+ });
986
+ });
987
+ });
988
+ }
989
+ onBeforeToggle(event) {
990
+ const te = event;
991
+ if (te.newState === 'open') {
992
+ applyPopoverPosition(this.popoverEl().nativeElement, this.trigger().nativeElement.getBoundingClientRect());
993
+ this.filterText.set('');
994
+ }
995
+ else {
996
+ this._activeIdx.set(-1);
997
+ this.keyManager.setActiveItem(-1);
998
+ }
999
+ }
1000
+ onToggle(event) {
1001
+ if (event.newState === 'open') {
1002
+ this.filterInput()?.nativeElement.focus();
1003
+ setTimeout(() => {
1004
+ this.keyManager.setFirstItemActive();
1005
+ this._activeIdx.set(this.keyManager.activeItemIndex ?? -1);
1006
+ });
1007
+ }
1008
+ }
1009
+ onKeydown(event) {
1010
+ switch (event.key) {
1011
+ case 'ArrowDown':
1012
+ case 'ArrowUp': {
1013
+ event.preventDefault();
1014
+ this.keyManager.onKeydown(event);
1015
+ this.scrollActiveIntoView();
1016
+ break;
1017
+ }
1018
+ case 'ArrowRight': {
1019
+ event.preventDefault();
1020
+ const item = this.visibleItems()[this.keyManager.activeItemIndex ?? -1];
1021
+ if (item?.isExpandable && !item.isExpanded)
1022
+ this.toggleExpand(item.node.value);
1023
+ break;
1024
+ }
1025
+ case 'ArrowLeft': {
1026
+ event.preventDefault();
1027
+ const item = this.visibleItems()[this.keyManager.activeItemIndex ?? -1];
1028
+ if (item?.isExpandable && item.isExpanded)
1029
+ this.toggleExpand(item.node.value);
1030
+ break;
1031
+ }
1032
+ case 'Enter': {
1033
+ event.preventDefault();
1034
+ const item = this.visibleItems()[this.keyManager.activeItemIndex ?? -1];
1035
+ if (item)
1036
+ this.onItemClick(item);
1037
+ break;
1038
+ }
1039
+ case 'Escape': {
1040
+ event.preventDefault();
1041
+ this.popoverEl().nativeElement.hidePopover();
1042
+ this.trigger().nativeElement.focus();
1043
+ break;
1044
+ }
1045
+ case 'Tab': {
1046
+ this.popoverEl().nativeElement.hidePopover();
1047
+ break;
1048
+ }
1049
+ }
1050
+ }
1051
+ onItemClick(item) {
1052
+ if (item.isExpandable) {
1053
+ this.toggleExpand(item.node.value);
1054
+ }
1055
+ else {
1056
+ this.selectChange.emit(item.node.value);
1057
+ this.popoverEl().nativeElement.hidePopover();
1058
+ this.trigger().nativeElement.focus();
1059
+ }
1060
+ }
1061
+ /** 追踪函数:使用 value + isExistsOption 避免重复 key */
1062
+ trackItem(item) {
1063
+ return item.node.isExistsOption ? `${item.node.value}:exists` : item.node.value;
1064
+ }
1065
+ scrollActiveIntoView() {
1066
+ const idx = this.keyManager.activeItemIndex ?? -1;
1067
+ const item = this.visibleItems()[idx];
1068
+ if (!item)
1069
+ return;
1070
+ document.getElementById(`${this.uid}-item-${item.node.value}`)?.scrollIntoView({ block: 'nearest' });
1071
+ }
1072
+ toggleExpand(value) {
1073
+ this.expandedPaths.update(prev => {
1074
+ const next = new Set(prev);
1075
+ if (next.has(value)) {
1076
+ next.delete(value);
1077
+ }
1078
+ else {
1079
+ next.add(value);
1080
+ }
1081
+ return next;
1082
+ });
1083
+ }
1084
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: TreeSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1085
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: TreeSelectComponent, isStandalone: true, selector: "rxdb-tree-select", inputs: { nodes: { classPropertyName: "nodes", publicName: "nodes", isSignal: true, isRequired: true, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, minWidth: { classPropertyName: "minWidth", publicName: "minWidth", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectChange: "selectChange" }, viewQueries: [{ propertyName: "trigger", first: true, predicate: ["trigger"], descendants: true, isSignal: true }, { propertyName: "popoverEl", first: true, predicate: ["popoverEl"], descendants: true, isSignal: true }, { propertyName: "filterInput", first: true, predicate: ["filterInput"], descendants: true, isSignal: true }, { propertyName: "treeItemDirs", predicate: TreeItemDirective, descendants: true, isSignal: true }], ngImport: i0, template: `
1086
+ <!-- 触发按钮 -->
1087
+ <button
1088
+ class="select select-sm text-left"
1089
+ #trigger
1090
+ [attr.popovertarget]="uid"
1091
+ [style.min-width]="minWidth()"
1092
+ aria-haspopup="tree"
1093
+ type="button"
1094
+ >
1095
+ @if (selectedNode(); as node) {
1096
+ <lucide-icon
1097
+ class="w-3.5 shrink-0"
1098
+ [class]="typeColorMap[node.type] ?? defaultTypeColor"
1099
+ [img]="typeIconMap[node.type] ?? defaultTypeIcon"
1100
+ size="14"
1101
+ />
1102
+ }
1103
+ <span class="min-w-0 grow truncate">{{ displayValue() }}</span>
1104
+ </button>
1105
+
1106
+ <!-- Popover 浮层 -->
1107
+ <div
1108
+ class="bg-base-100 text-base-content rounded-box border-base-300 border shadow-xl"
1109
+ #popoverEl
1110
+ [id]="uid"
1111
+ [style.min-width]="minWidth()"
1112
+ (beforetoggle)="onBeforeToggle($event)"
1113
+ (keydown)="onKeydown($event)"
1114
+ (toggle)="onToggle($event)"
1115
+ popover
1116
+ tabindex="-1"
1117
+ >
1118
+ <!-- 过滤输入框 -->
1119
+ <div class="mb-1 p-1">
1120
+ <input
1121
+ class="input input-sm w-full"
1122
+ #filterInput
1123
+ [attr.aria-activedescendant]="activeItemId()"
1124
+ [attr.aria-controls]="listId"
1125
+ [attr.aria-expanded]="true"
1126
+ [attr.aria-owns]="listId"
1127
+ [ngModel]="filterText()"
1128
+ (ngModelChange)="filterText.set($event)"
1129
+ aria-autocomplete="list"
1130
+ placeholder="搜索字段..."
1131
+ role="combobox"
1132
+ type="text"
1133
+ />
1134
+ </div>
1135
+
1136
+ <!-- 树形列表 -->
1137
+ <ul
1138
+ class="menu menu-sm max-h-60 w-full flex-nowrap overflow-y-auto px-2"
1139
+ [attr.aria-label]="placeholder()"
1140
+ [id]="listId"
1141
+ role="tree"
1142
+ >
1143
+ @for (item of visibleItems(); track trackItem(item)) {
1144
+ <li [style.padding-left.rem]="item.depth * 0.75" role="none">
1145
+ <button
1146
+ class="flex w-full items-center gap-1.5 text-left text-sm"
1147
+ #treeItemRef="rxdbTreeItem"
1148
+ [attr.aria-expanded]="item.isExpandable ? item.isExpanded : null"
1149
+ [attr.aria-selected]="!item.isExpandable ? item.node.value === selected() : null"
1150
+ [class.bg-primary/15]="!item.isExpandable && item.node.value === selected()"
1151
+ [class.font-semibold]="!item.isExpandable && item.node.value === selected()"
1152
+ [class.menu-focus]="treeItemRef.isActive()"
1153
+ [id]="uid + '-item-' + item.node.value"
1154
+ (click)="onItemClick(item)"
1155
+ role="treeitem"
1156
+ rxdbTreeItem
1157
+ type="button"
1158
+ >
1159
+ <lucide-icon
1160
+ class="w-3.5 shrink-0"
1161
+ [class]="typeColorMap[item.node.type] ?? defaultTypeColor"
1162
+ [img]="typeIconMap[item.node.type] ?? defaultTypeIcon"
1163
+ size="14"
1164
+ />
1165
+ <span class="min-w-0 grow truncate">{{ item.node.displayName || item.node.label }}</span>
1166
+ @if (!item.isExpandable && item.node.value === selected()) {
1167
+ <svg
1168
+ class="text-primary h-3 w-3 shrink-0"
1169
+ fill="none"
1170
+ stroke="currentColor"
1171
+ stroke-linecap="round"
1172
+ stroke-linejoin="round"
1173
+ stroke-width="2.5"
1174
+ viewBox="0 0 24 24"
1175
+ xmlns="http://www.w3.org/2000/svg"
1176
+ >
1177
+ <polyline points="20 6 9 17 4 12" />
1178
+ </svg>
1179
+ }
1180
+ @if (item.isExpandable) {
1181
+ <span class="shrink-0 text-xs opacity-40">{{ item.node.children!.length }}</span>
1182
+ @if (item.isExpanded) {
1183
+ <lucide-icon class="w-3.5 shrink-0 opacity-50" [img]="ChevronDownIcon" size="14" />
1184
+ } @else {
1185
+ <lucide-icon class="w-3.5 shrink-0 opacity-50" [img]="ChevronRightIcon" size="14" />
1186
+ }
1187
+ }
1188
+ </button>
1189
+ </li>
1190
+ } @empty {
1191
+ <li class="px-3 py-1.5 text-sm opacity-50">无匹配字段</li>
1192
+ }
1193
+ </ul>
1194
+ </div>
1195
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: TreeItemDirective, selector: "[rxdbTreeItem]", exportAs: ["rxdbTreeItem"] }, { kind: "ngmodule", type: LucideAngularModule }, { kind: "component", type: i2.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1196
+ }
1197
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: TreeSelectComponent, decorators: [{
1198
+ type: Component,
1199
+ args: [{
1200
+ selector: 'rxdb-tree-select',
1201
+ standalone: true,
1202
+ imports: [FormsModule, TreeItemDirective, LucideAngularModule],
1203
+ changeDetection: ChangeDetectionStrategy.OnPush,
1204
+ template: `
1205
+ <!-- 触发按钮 -->
1206
+ <button
1207
+ class="select select-sm text-left"
1208
+ #trigger
1209
+ [attr.popovertarget]="uid"
1210
+ [style.min-width]="minWidth()"
1211
+ aria-haspopup="tree"
1212
+ type="button"
1213
+ >
1214
+ @if (selectedNode(); as node) {
1215
+ <lucide-icon
1216
+ class="w-3.5 shrink-0"
1217
+ [class]="typeColorMap[node.type] ?? defaultTypeColor"
1218
+ [img]="typeIconMap[node.type] ?? defaultTypeIcon"
1219
+ size="14"
1220
+ />
1221
+ }
1222
+ <span class="min-w-0 grow truncate">{{ displayValue() }}</span>
1223
+ </button>
1224
+
1225
+ <!-- Popover 浮层 -->
1226
+ <div
1227
+ class="bg-base-100 text-base-content rounded-box border-base-300 border shadow-xl"
1228
+ #popoverEl
1229
+ [id]="uid"
1230
+ [style.min-width]="minWidth()"
1231
+ (beforetoggle)="onBeforeToggle($event)"
1232
+ (keydown)="onKeydown($event)"
1233
+ (toggle)="onToggle($event)"
1234
+ popover
1235
+ tabindex="-1"
1236
+ >
1237
+ <!-- 过滤输入框 -->
1238
+ <div class="mb-1 p-1">
1239
+ <input
1240
+ class="input input-sm w-full"
1241
+ #filterInput
1242
+ [attr.aria-activedescendant]="activeItemId()"
1243
+ [attr.aria-controls]="listId"
1244
+ [attr.aria-expanded]="true"
1245
+ [attr.aria-owns]="listId"
1246
+ [ngModel]="filterText()"
1247
+ (ngModelChange)="filterText.set($event)"
1248
+ aria-autocomplete="list"
1249
+ placeholder="搜索字段..."
1250
+ role="combobox"
1251
+ type="text"
1252
+ />
1253
+ </div>
1254
+
1255
+ <!-- 树形列表 -->
1256
+ <ul
1257
+ class="menu menu-sm max-h-60 w-full flex-nowrap overflow-y-auto px-2"
1258
+ [attr.aria-label]="placeholder()"
1259
+ [id]="listId"
1260
+ role="tree"
1261
+ >
1262
+ @for (item of visibleItems(); track trackItem(item)) {
1263
+ <li [style.padding-left.rem]="item.depth * 0.75" role="none">
1264
+ <button
1265
+ class="flex w-full items-center gap-1.5 text-left text-sm"
1266
+ #treeItemRef="rxdbTreeItem"
1267
+ [attr.aria-expanded]="item.isExpandable ? item.isExpanded : null"
1268
+ [attr.aria-selected]="!item.isExpandable ? item.node.value === selected() : null"
1269
+ [class.bg-primary/15]="!item.isExpandable && item.node.value === selected()"
1270
+ [class.font-semibold]="!item.isExpandable && item.node.value === selected()"
1271
+ [class.menu-focus]="treeItemRef.isActive()"
1272
+ [id]="uid + '-item-' + item.node.value"
1273
+ (click)="onItemClick(item)"
1274
+ role="treeitem"
1275
+ rxdbTreeItem
1276
+ type="button"
1277
+ >
1278
+ <lucide-icon
1279
+ class="w-3.5 shrink-0"
1280
+ [class]="typeColorMap[item.node.type] ?? defaultTypeColor"
1281
+ [img]="typeIconMap[item.node.type] ?? defaultTypeIcon"
1282
+ size="14"
1283
+ />
1284
+ <span class="min-w-0 grow truncate">{{ item.node.displayName || item.node.label }}</span>
1285
+ @if (!item.isExpandable && item.node.value === selected()) {
1286
+ <svg
1287
+ class="text-primary h-3 w-3 shrink-0"
1288
+ fill="none"
1289
+ stroke="currentColor"
1290
+ stroke-linecap="round"
1291
+ stroke-linejoin="round"
1292
+ stroke-width="2.5"
1293
+ viewBox="0 0 24 24"
1294
+ xmlns="http://www.w3.org/2000/svg"
1295
+ >
1296
+ <polyline points="20 6 9 17 4 12" />
1297
+ </svg>
1298
+ }
1299
+ @if (item.isExpandable) {
1300
+ <span class="shrink-0 text-xs opacity-40">{{ item.node.children!.length }}</span>
1301
+ @if (item.isExpanded) {
1302
+ <lucide-icon class="w-3.5 shrink-0 opacity-50" [img]="ChevronDownIcon" size="14" />
1303
+ } @else {
1304
+ <lucide-icon class="w-3.5 shrink-0 opacity-50" [img]="ChevronRightIcon" size="14" />
1305
+ }
1306
+ }
1307
+ </button>
1308
+ </li>
1309
+ } @empty {
1310
+ <li class="px-3 py-1.5 text-sm opacity-50">无匹配字段</li>
1311
+ }
1312
+ </ul>
1313
+ </div>
1314
+ `
1315
+ }]
1316
+ }], ctorParameters: () => [], propDecorators: { trigger: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }], popoverEl: [{ type: i0.ViewChild, args: ['popoverEl', { isSignal: true }] }], filterInput: [{ type: i0.ViewChild, args: ['filterInput', { isSignal: true }] }], treeItemDirs: [{ type: i0.ViewChildren, args: [i0.forwardRef(() => TreeItemDirective), { isSignal: true }] }], nodes: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodes", required: true }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], minWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "minWidth", required: false }] }], selectChange: [{ type: i0.Output, args: ["selectChange"] }] } });
1317
+ // ─── 辅助函数 ───────────────────────────────────────────────────────────────────
1318
+ function buildExpandedItems(nodes, expanded, depth) {
1319
+ const result = [];
1320
+ for (const node of nodes) {
1321
+ const isExpandable = !!node.children?.length;
1322
+ const isExpanded = isExpandable && expanded.has(node.value);
1323
+ result.push({ node, depth, isExpandable, isExpanded });
1324
+ if (isExpanded) {
1325
+ result.push(...buildExpandedItems(node.children, expanded, depth + 1));
1326
+ }
1327
+ }
1328
+ return result;
1329
+ }
1330
+ function buildFilteredItems(nodes, filter, depth = 0) {
1331
+ const result = [];
1332
+ for (const node of nodes) {
1333
+ const selfMatches = nodeMatchesFilter(node, filter);
1334
+ const isExpandable = !!node.children?.length;
1335
+ const childMatches = isExpandable && hasMatchingDescendant(node.children, filter);
1336
+ if (selfMatches || childMatches) {
1337
+ result.push({ node, depth, isExpandable, isExpanded: isExpandable && childMatches });
1338
+ if (isExpandable && (selfMatches || childMatches)) {
1339
+ result.push(...buildFilteredItems(node.children, filter, depth + 1));
1340
+ }
1341
+ }
1342
+ }
1343
+ return result;
1344
+ }
1345
+ function nodeMatchesFilter(node, filter) {
1346
+ return (node.label.toLowerCase().includes(filter) ||
1347
+ node.displayName.toLowerCase().includes(filter) ||
1348
+ (node.description?.toLowerCase().includes(filter) ?? false));
1349
+ }
1350
+ function hasMatchingDescendant(nodes, filter) {
1351
+ return nodes.some(n => nodeMatchesFilter(n, filter) || (n.children?.length ? hasMatchingDescendant(n.children, filter) : false));
1352
+ }
1353
+
1354
+ /**
1355
+ * 字段选择器组件
1356
+ *
1357
+ * @description
1358
+ * 使用 Popover API 展示树形字段结构,支持过滤和嵌套关系字段的展开/折叠。
1359
+ * 基于 CDK `ActiveDescendantKeyManager` 提供完整键盘导航和无障碍支持。
1360
+ * 不依赖任何第三方 UI 库。
1361
+ *
1362
+ * 支持两种使用方式:
1363
+ * 1. 直接模板使用:监听 `(fieldChange)` output 事件
1364
+ * 2. 通过 NgComponentOutlet 注入:传入 `fieldChangeFn` 回调 input
1365
+ */
1366
+ class FieldSelectorComponent {
1367
+ fields = input.required(...(ngDevMode ? [{ debugName: "fields" }] : /* istanbul ignore next */ []));
1368
+ selectedField = input('', ...(ngDevMode ? [{ debugName: "selectedField" }] : /* istanbul ignore next */ []));
1369
+ placeholder = input('选择字段', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
1370
+ /** 通过 NgComponentOutlet 注入时使用的回调 input */
1371
+ fieldChangeFn = input(undefined, ...(ngDevMode ? [{ debugName: "fieldChangeFn" }] : /* istanbul ignore next */ []));
1372
+ /** 直接模板使用时的 output 事件 */
1373
+ fieldChange = output();
1374
+ fieldTree = computed(() => buildFieldTree(this.fields()), ...(ngDevMode ? [{ debugName: "fieldTree" }] : /* istanbul ignore next */ []));
1375
+ onSelect(field) {
1376
+ this.fieldChange.emit(field);
1377
+ this.fieldChangeFn()?.(field);
1378
+ }
1379
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FieldSelectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1380
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.5", type: FieldSelectorComponent, isStandalone: true, selector: "rxdb-field-selector", inputs: { fields: { classPropertyName: "fields", publicName: "fields", isSignal: true, isRequired: true, transformFunction: null }, selectedField: { classPropertyName: "selectedField", publicName: "selectedField", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, fieldChangeFn: { classPropertyName: "fieldChangeFn", publicName: "fieldChangeFn", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { fieldChange: "fieldChange" }, ngImport: i0, template: `
1381
+ <rxdb-tree-select
1382
+ [nodes]="fieldTree()"
1383
+ [placeholder]="placeholder()"
1384
+ [selected]="selectedField()"
1385
+ (selectChange)="onSelect($event)"
1386
+ minWidth="14rem"
1387
+ />
1388
+ `, isInline: true, dependencies: [{ kind: "component", type: TreeSelectComponent, selector: "rxdb-tree-select", inputs: ["nodes", "selected", "placeholder", "minWidth"], outputs: ["selectChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1389
+ }
1390
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FieldSelectorComponent, decorators: [{
1391
+ type: Component,
1392
+ args: [{
1393
+ selector: 'rxdb-field-selector',
1394
+ standalone: true,
1395
+ imports: [TreeSelectComponent],
1396
+ template: `
1397
+ <rxdb-tree-select
1398
+ [nodes]="fieldTree()"
1399
+ [placeholder]="placeholder()"
1400
+ [selected]="selectedField()"
1401
+ (selectChange)="onSelect($event)"
1402
+ minWidth="14rem"
1403
+ />
1404
+ `,
1405
+ changeDetection: ChangeDetectionStrategy.OnPush
1406
+ }]
1407
+ }], propDecorators: { fields: [{ type: i0.Input, args: [{ isSignal: true, alias: "fields", required: true }] }], selectedField: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedField", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], fieldChangeFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "fieldChangeFn", required: false }] }], fieldChange: [{ type: i0.Output, args: ["fieldChange"] }] } });
1408
+
1409
+ let _uid = 0;
1410
+ /**
1411
+ * 基于 Popover API 的下拉选择组件
1412
+ *
1413
+ * @description
1414
+ * 使用浏览器原生 Popover API 渲染选项列表,在 top layer 渲染,
1415
+ * 确保在 `overflow: auto/hidden` 容器内也不会被裁剪。
1416
+ * 支持关键字过滤、键盘导航(↑↓ 移动、Enter 选中、Escape 关闭)。
1417
+ * 打开弹框自动高亮第一项,搜索后也自动高亮第一条结果。
1418
+ */
1419
+ class PopoverSelectComponent {
1420
+ trigger = viewChild.required('trigger');
1421
+ popoverEl = viewChild.required('popoverEl');
1422
+ filterInput = viewChild('filterInput', ...(ngDevMode ? [{ debugName: "filterInput" }] : /* istanbul ignore next */ []));
1423
+ uid = `rxdb-ps-${++_uid}`;
1424
+ listId = `${this.uid}-list`;
1425
+ filterText = signal('', ...(ngDevMode ? [{ debugName: "filterText" }] : /* istanbul ignore next */ []));
1426
+ activeIndex = signal(0, ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
1427
+ options = input.required(...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
1428
+ selected = input('', ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
1429
+ placeholder = input('请选择', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
1430
+ minWidth = input('8rem', ...(ngDevMode ? [{ debugName: "minWidth" }] : /* istanbul ignore next */ []));
1431
+ selectChange = output();
1432
+ currentLabel = computed(() => findOptionLabel(this.options(), this.selected(), this.placeholder()), ...(ngDevMode ? [{ debugName: "currentLabel" }] : /* istanbul ignore next */ []));
1433
+ filteredOptions = computed(() => {
1434
+ const filter = this.filterText().toLowerCase().trim();
1435
+ if (!filter)
1436
+ return this.options();
1437
+ return this.options().filter(opt => opt.label.toLowerCase().includes(filter));
1438
+ }, ...(ngDevMode ? [{ debugName: "filteredOptions" }] : /* istanbul ignore next */ []));
1439
+ activeItemId = computed(() => {
1440
+ const idx = this.activeIndex();
1441
+ const opts = this.filteredOptions();
1442
+ return idx >= 0 && idx < opts.length ? `${this.uid}-opt-${opts[idx].value}` : null;
1443
+ }, ...(ngDevMode ? [{ debugName: "activeItemId" }] : /* istanbul ignore next */ []));
1444
+ constructor() {
1445
+ effect(() => {
1446
+ this.filteredOptions();
1447
+ untracked(() => this.activeIndex.set(0));
1448
+ });
1449
+ }
1450
+ onBeforeToggle(event) {
1451
+ const te = event;
1452
+ if (te.newState === 'open') {
1453
+ applyPopoverPosition(this.popoverEl().nativeElement, this.trigger().nativeElement.getBoundingClientRect());
1454
+ this.filterText.set('');
1455
+ }
1456
+ }
1457
+ onToggle(event) {
1458
+ if (event.newState === 'open') {
1459
+ this.filterInput()?.nativeElement.focus();
1460
+ this.activeIndex.set(0);
1461
+ }
1462
+ }
1463
+ onKeydown(event) {
1464
+ const opts = this.filteredOptions();
1465
+ const len = opts.length;
1466
+ switch (event.key) {
1467
+ case 'ArrowDown': {
1468
+ event.preventDefault();
1469
+ if (len > 0) {
1470
+ this.activeIndex.update(i => (i + 1) % len);
1471
+ this.scrollActiveIntoView();
1472
+ }
1473
+ break;
1474
+ }
1475
+ case 'ArrowUp': {
1476
+ event.preventDefault();
1477
+ if (len > 0) {
1478
+ this.activeIndex.update(i => (i - 1 + len) % len);
1479
+ this.scrollActiveIntoView();
1480
+ }
1481
+ break;
1482
+ }
1483
+ case 'Enter': {
1484
+ event.preventDefault();
1485
+ const opt = opts[this.activeIndex()];
1486
+ if (opt)
1487
+ this.onSelect(opt.value);
1488
+ break;
1489
+ }
1490
+ case 'Escape': {
1491
+ event.preventDefault();
1492
+ this.popoverEl().nativeElement.hidePopover();
1493
+ this.trigger().nativeElement.focus();
1494
+ break;
1495
+ }
1496
+ case 'Tab': {
1497
+ this.popoverEl().nativeElement.hidePopover();
1498
+ break;
1499
+ }
1500
+ }
1501
+ }
1502
+ onSelect(value) {
1503
+ this.selectChange.emit(value);
1504
+ this.popoverEl().nativeElement.hidePopover();
1505
+ this.trigger().nativeElement.focus();
1506
+ }
1507
+ scrollActiveIntoView() {
1508
+ const opts = this.filteredOptions();
1509
+ const opt = opts[this.activeIndex()];
1510
+ if (!opt)
1511
+ return;
1512
+ document.getElementById(`${this.uid}-opt-${opt.value}`)?.scrollIntoView({ block: 'nearest' });
1513
+ }
1514
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: PopoverSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1515
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: PopoverSelectComponent, isStandalone: true, selector: "rxdb-popover-select", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: true, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, minWidth: { classPropertyName: "minWidth", publicName: "minWidth", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectChange: "selectChange" }, viewQueries: [{ propertyName: "trigger", first: true, predicate: ["trigger"], descendants: true, isSignal: true }, { propertyName: "popoverEl", first: true, predicate: ["popoverEl"], descendants: true, isSignal: true }, { propertyName: "filterInput", first: true, predicate: ["filterInput"], descendants: true, isSignal: true }], ngImport: i0, template: `
1516
+ <button
1517
+ class="select select-sm text-left"
1518
+ #trigger
1519
+ [attr.popovertarget]="uid"
1520
+ [style.min-width]="minWidth()"
1521
+ type="button"
1522
+ >
1523
+ {{ currentLabel() }}
1524
+ </button>
1525
+ <div
1526
+ class="bg-base-100 rounded-box border-base-300 border shadow-xl"
1527
+ #popoverEl
1528
+ [id]="uid"
1529
+ [style.min-width]="minWidth()"
1530
+ (beforetoggle)="onBeforeToggle($event)"
1531
+ (keydown)="onKeydown($event)"
1532
+ (toggle)="onToggle($event)"
1533
+ popover
1534
+ tabindex="-1"
1535
+ >
1536
+ <div class="mb-1 p-2">
1537
+ <input
1538
+ class="input input-sm w-full"
1539
+ #filterInput
1540
+ [attr.aria-activedescendant]="activeItemId()"
1541
+ [attr.aria-controls]="listId"
1542
+ [attr.aria-expanded]="true"
1543
+ [ngModel]="filterText()"
1544
+ (ngModelChange)="filterText.set($event)"
1545
+ aria-autocomplete="list"
1546
+ placeholder="搜索..."
1547
+ role="combobox"
1548
+ type="text"
1549
+ />
1550
+ </div>
1551
+ <ul class="menu menu-sm max-h-60 w-full flex-nowrap overflow-y-auto px-2" [id]="listId" role="listbox">
1552
+ @for (option of filteredOptions(); track option.value; let i = $index) {
1553
+ <li role="none">
1554
+ <button
1555
+ class="flex w-full items-center text-left text-sm"
1556
+ [attr.aria-selected]="option.value === selected()"
1557
+ [class.bg-primary/15]="option.value === selected()"
1558
+ [class.font-semibold]="option.value === selected()"
1559
+ [class.menu-focus]="activeIndex() === i"
1560
+ [id]="uid + '-opt-' + option.value"
1561
+ (click)="onSelect(option.value)"
1562
+ role="option"
1563
+ type="button"
1564
+ >
1565
+ <span class="grow">{{ option.label }}</span>
1566
+ @if (option.value === selected()) {
1567
+ <svg
1568
+ class="text-primary h-3 w-3 shrink-0"
1569
+ fill="none"
1570
+ stroke="currentColor"
1571
+ stroke-linecap="round"
1572
+ stroke-linejoin="round"
1573
+ stroke-width="2.5"
1574
+ viewBox="0 0 24 24"
1575
+ xmlns="http://www.w3.org/2000/svg"
1576
+ >
1577
+ <polyline points="20 6 9 17 4 12" />
1578
+ </svg>
1579
+ }
1580
+ </button>
1581
+ </li>
1582
+ } @empty {
1583
+ <li class="px-3 py-1.5 text-sm opacity-50">无匹配项</li>
1584
+ }
1585
+ </ul>
1586
+ </div>
1587
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1588
+ }
1589
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: PopoverSelectComponent, decorators: [{
1590
+ type: Component,
1591
+ args: [{
1592
+ selector: 'rxdb-popover-select',
1593
+ standalone: true,
1594
+ imports: [FormsModule],
1595
+ template: `
1596
+ <button
1597
+ class="select select-sm text-left"
1598
+ #trigger
1599
+ [attr.popovertarget]="uid"
1600
+ [style.min-width]="minWidth()"
1601
+ type="button"
1602
+ >
1603
+ {{ currentLabel() }}
1604
+ </button>
1605
+ <div
1606
+ class="bg-base-100 rounded-box border-base-300 border shadow-xl"
1607
+ #popoverEl
1608
+ [id]="uid"
1609
+ [style.min-width]="minWidth()"
1610
+ (beforetoggle)="onBeforeToggle($event)"
1611
+ (keydown)="onKeydown($event)"
1612
+ (toggle)="onToggle($event)"
1613
+ popover
1614
+ tabindex="-1"
1615
+ >
1616
+ <div class="mb-1 p-2">
1617
+ <input
1618
+ class="input input-sm w-full"
1619
+ #filterInput
1620
+ [attr.aria-activedescendant]="activeItemId()"
1621
+ [attr.aria-controls]="listId"
1622
+ [attr.aria-expanded]="true"
1623
+ [ngModel]="filterText()"
1624
+ (ngModelChange)="filterText.set($event)"
1625
+ aria-autocomplete="list"
1626
+ placeholder="搜索..."
1627
+ role="combobox"
1628
+ type="text"
1629
+ />
1630
+ </div>
1631
+ <ul class="menu menu-sm max-h-60 w-full flex-nowrap overflow-y-auto px-2" [id]="listId" role="listbox">
1632
+ @for (option of filteredOptions(); track option.value; let i = $index) {
1633
+ <li role="none">
1634
+ <button
1635
+ class="flex w-full items-center text-left text-sm"
1636
+ [attr.aria-selected]="option.value === selected()"
1637
+ [class.bg-primary/15]="option.value === selected()"
1638
+ [class.font-semibold]="option.value === selected()"
1639
+ [class.menu-focus]="activeIndex() === i"
1640
+ [id]="uid + '-opt-' + option.value"
1641
+ (click)="onSelect(option.value)"
1642
+ role="option"
1643
+ type="button"
1644
+ >
1645
+ <span class="grow">{{ option.label }}</span>
1646
+ @if (option.value === selected()) {
1647
+ <svg
1648
+ class="text-primary h-3 w-3 shrink-0"
1649
+ fill="none"
1650
+ stroke="currentColor"
1651
+ stroke-linecap="round"
1652
+ stroke-linejoin="round"
1653
+ stroke-width="2.5"
1654
+ viewBox="0 0 24 24"
1655
+ xmlns="http://www.w3.org/2000/svg"
1656
+ >
1657
+ <polyline points="20 6 9 17 4 12" />
1658
+ </svg>
1659
+ }
1660
+ </button>
1661
+ </li>
1662
+ } @empty {
1663
+ <li class="px-3 py-1.5 text-sm opacity-50">无匹配项</li>
1664
+ }
1665
+ </ul>
1666
+ </div>
1667
+ `,
1668
+ changeDetection: ChangeDetectionStrategy.OnPush
1669
+ }]
1670
+ }], ctorParameters: () => [], propDecorators: { trigger: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }], popoverEl: [{ type: i0.ViewChild, args: ['popoverEl', { isSignal: true }] }], filterInput: [{ type: i0.ViewChild, args: ['filterInput', { isSignal: true }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: true }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], minWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "minWidth", required: false }] }], selectChange: [{ type: i0.Output, args: ["selectChange"] }] } });
1671
+
1672
+ /**
1673
+ * 操作符选择器组件
1674
+ *
1675
+ * @description
1676
+ * 基于 {@link PopoverSelectComponent} 提供操作符选择功能,
1677
+ * 自动根据字段类型或元数据筛选可用操作符。
1678
+ * 支持搜索过滤和键盘导航。
1679
+ *
1680
+ * 支持两种使用方式:
1681
+ * 1. 直接模板使用:监听 `(operatorChange)` output 事件
1682
+ * 2. 通过 NgComponentOutlet 注入:传入 `operatorChangeFn` 回调 input
1683
+ */
1684
+ class OperatorSelectorComponent {
1685
+ registry = getDefaultOperatorRegistry();
1686
+ operatorOptions = computed(() => {
1687
+ const metadata = this.fieldMetadata();
1688
+ const operators = metadata ? this.registry.getForField(metadata) : this.registry.getForType(this.fieldType());
1689
+ return operators.map(op => ({ value: op.key, label: op.label }));
1690
+ }, ...(ngDevMode ? [{ debugName: "operatorOptions" }] : /* istanbul ignore next */ []));
1691
+ fieldMetadata = input(undefined, ...(ngDevMode ? [{ debugName: "fieldMetadata" }] : /* istanbul ignore next */ []));
1692
+ fieldType = input('string', ...(ngDevMode ? [{ debugName: "fieldType" }] : /* istanbul ignore next */ []));
1693
+ selectedOperator = input('=', ...(ngDevMode ? [{ debugName: "selectedOperator" }] : /* istanbul ignore next */ []));
1694
+ /** 通过 NgComponentOutlet 注入时使用的回调 input */
1695
+ operatorChangeFn = input(undefined, ...(ngDevMode ? [{ debugName: "operatorChangeFn" }] : /* istanbul ignore next */ []));
1696
+ /** 直接模板使用时的 output 事件 */
1697
+ operatorChange = output();
1698
+ onOperatorChange(op) {
1699
+ this.operatorChange.emit(op);
1700
+ this.operatorChangeFn()?.(op);
1701
+ }
1702
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: OperatorSelectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1703
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.5", type: OperatorSelectorComponent, isStandalone: true, selector: "rxdb-operator-selector", inputs: { fieldMetadata: { classPropertyName: "fieldMetadata", publicName: "fieldMetadata", isSignal: true, isRequired: false, transformFunction: null }, fieldType: { classPropertyName: "fieldType", publicName: "fieldType", isSignal: true, isRequired: false, transformFunction: null }, selectedOperator: { classPropertyName: "selectedOperator", publicName: "selectedOperator", isSignal: true, isRequired: false, transformFunction: null }, operatorChangeFn: { classPropertyName: "operatorChangeFn", publicName: "operatorChangeFn", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { operatorChange: "operatorChange" }, ngImport: i0, template: `
1704
+ <rxdb-popover-select
1705
+ [options]="operatorOptions()"
1706
+ [selected]="selectedOperator()"
1707
+ (selectChange)="onOperatorChange($event)"
1708
+ minWidth="8rem"
1709
+ placeholder="选择操作符"
1710
+ />
1711
+ `, isInline: true, dependencies: [{ kind: "component", type: PopoverSelectComponent, selector: "rxdb-popover-select", inputs: ["options", "selected", "placeholder", "minWidth"], outputs: ["selectChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1712
+ }
1713
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: OperatorSelectorComponent, decorators: [{
1714
+ type: Component,
1715
+ args: [{
1716
+ selector: 'rxdb-operator-selector',
1717
+ standalone: true,
1718
+ imports: [PopoverSelectComponent],
1719
+ template: `
1720
+ <rxdb-popover-select
1721
+ [options]="operatorOptions()"
1722
+ [selected]="selectedOperator()"
1723
+ (selectChange)="onOperatorChange($event)"
1724
+ minWidth="8rem"
1725
+ placeholder="选择操作符"
1726
+ />
1727
+ `,
1728
+ changeDetection: ChangeDetectionStrategy.OnPush
1729
+ }]
1730
+ }], propDecorators: { fieldMetadata: [{ type: i0.Input, args: [{ isSignal: true, alias: "fieldMetadata", required: false }] }], fieldType: [{ type: i0.Input, args: [{ isSignal: true, alias: "fieldType", required: false }] }], selectedOperator: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedOperator", required: false }] }], operatorChangeFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "operatorChangeFn", required: false }] }], operatorChange: [{ type: i0.Output, args: ["operatorChange"] }] } });
1731
+
1732
+ /**
1733
+ * 值输入组件
1734
+ *
1735
+ * @description
1736
+ * 根据字段类型和操作符显示合适的输入控件:
1737
+ * - string: 文本输入框
1738
+ * - number: 数字输入框
1739
+ * - boolean: 切换开关
1740
+ * - date: 日期选择器
1741
+ * - enum: 下拉选择器
1742
+ * - array: 逗号分隔的文本输入
1743
+ * - range: 范围输入(双输入框或范围选择器)
1744
+ * - null/notNull 操作符: 无输入
1745
+ * - exists/notExists 操作符: 子查询构建器
1746
+ */
1747
+ class ValueInputComponent {
1748
+ previousInputType = null;
1749
+ subqueryComponentRef;
1750
+ subquerySubscription;
1751
+ subqueryLoading = false;
1752
+ injector = inject(Injector);
1753
+ /** UUID 格式校验错误(基于 currentValue 派生,inputType 不是 uuid 时自动为空) */
1754
+ uuidError = computed(() => {
1755
+ if (this.inputType() !== 'uuid')
1756
+ return '';
1757
+ const val = String(this.currentValue() ?? '').trim();
1758
+ return val && !UUID_RE.test(val) ? '请输入合法的 UUID 格式' : '';
1759
+ }, ...(ngDevMode ? [{ debugName: "uuidError" }] : /* istanbul ignore next */ []));
1760
+ subqueryContainer;
1761
+ fieldType = input('string', ...(ngDevMode ? [{ debugName: "fieldType" }] : /* istanbul ignore next */ []));
1762
+ operator = input('=', ...(ngDevMode ? [{ debugName: "operator" }] : /* istanbul ignore next */ []));
1763
+ value = input('', ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
1764
+ enumOptions = input([], ...(ngDevMode ? [{ debugName: "enumOptions" }] : /* istanbul ignore next */ []));
1765
+ relationFields = input([], ...(ngDevMode ? [{ debugName: "relationFields" }] : /* istanbul ignore next */ []));
1766
+ where = input(...(ngDevMode ? [undefined, { debugName: "where" }] : /* istanbul ignore next */ []));
1767
+ valueChange = output();
1768
+ whereChange = output();
1769
+ errorMessage = input('', ...(ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []));
1770
+ hasError = computed(() => !!this.errorMessage(), ...(ngDevMode ? [{ debugName: "hasError" }] : /* istanbul ignore next */ []));
1771
+ valueChangeFn = input(undefined, ...(ngDevMode ? [{ debugName: "valueChangeFn" }] : /* istanbul ignore next */ []));
1772
+ whereChangeFn = input(undefined, ...(ngDevMode ? [{ debugName: "whereChangeFn" }] : /* istanbul ignore next */ []));
1773
+ /** 信号状态 —— 替代原先的可变类属性 */
1774
+ currentValue = signal('', ...(ngDevMode ? [{ debugName: "currentValue" }] : /* istanbul ignore next */ []));
1775
+ rangeMin = signal(null, ...(ngDevMode ? [{ debugName: "rangeMin" }] : /* istanbul ignore next */ []));
1776
+ rangeMax = signal(null, ...(ngDevMode ? [{ debugName: "rangeMax" }] : /* istanbul ignore next */ []));
1777
+ arrayInputValue = signal('', ...(ngDevMode ? [{ debugName: "arrayInputValue" }] : /* istanbul ignore next */ []));
1778
+ currentDateStr = signal('', ...(ngDevMode ? [{ debugName: "currentDateStr" }] : /* istanbul ignore next */ []));
1779
+ dateRangeStart = signal('', ...(ngDevMode ? [{ debugName: "dateRangeStart" }] : /* istanbul ignore next */ []));
1780
+ dateRangeEnd = signal('', ...(ngDevMode ? [{ debugName: "dateRangeEnd" }] : /* istanbul ignore next */ []));
1781
+ inputType = computed(() => getInputType(this.fieldType(), this.operator(), this.enumOptions()), ...(ngDevMode ? [{ debugName: "inputType" }] : /* istanbul ignore next */ []));
1782
+ enumSelectOptions = computed(() => this.enumOptions().map(opt => ({ value: String(opt), label: String(opt) })), ...(ngDevMode ? [{ debugName: "enumSelectOptions" }] : /* istanbul ignore next */ []));
1783
+ constructor() {
1784
+ effect(() => {
1785
+ const currentType = this.inputType();
1786
+ if (this.previousInputType !== null && this.previousInputType !== currentType) {
1787
+ const defaultVal = getDefaultValueForType(currentType);
1788
+ this.currentValue.set(defaultVal);
1789
+ this.valueChange.emit(defaultVal);
1790
+ this.valueChangeFn()?.(defaultVal);
1791
+ }
1792
+ this.previousInputType = currentType;
1793
+ });
1794
+ afterNextRender(() => {
1795
+ effect(() => {
1796
+ const isSubquery = this.inputType() === 'subquery';
1797
+ const hasRelationFields = this.relationFields().length > 0;
1798
+ if (isSubquery && hasRelationFields) {
1799
+ this.loadSubqueryComponent();
1800
+ }
1801
+ else {
1802
+ this.destroySubqueryComponent();
1803
+ }
1804
+ }, { injector: this.injector });
1805
+ }, { injector: this.injector });
1806
+ }
1807
+ ngOnDestroy() {
1808
+ this.destroySubqueryComponent();
1809
+ }
1810
+ ngOnInit() {
1811
+ const value = this.value();
1812
+ if (this.inputType() === 'range' && Array.isArray(value)) {
1813
+ if (this.fieldType() === 'date') {
1814
+ this.dateRangeStart.set(String(value[0] ?? ''));
1815
+ this.dateRangeEnd.set(String(value[1] ?? ''));
1816
+ }
1817
+ else {
1818
+ this.rangeMin.set(value[0]);
1819
+ this.rangeMax.set(value[1]);
1820
+ }
1821
+ }
1822
+ else if (this.inputType() === 'array' && Array.isArray(value)) {
1823
+ this.arrayInputValue.set(value.join(', '));
1824
+ }
1825
+ else if (this.inputType() === 'enum-array') {
1826
+ this.currentValue.set(Array.isArray(value) ? value : []);
1827
+ }
1828
+ else if (this.inputType() === 'date') {
1829
+ this.currentDateStr.set(typeof value === 'string' ? value : '');
1830
+ }
1831
+ else if (this.inputType() === 'uuid') {
1832
+ this.currentValue.set(typeof value === 'string' ? value : '');
1833
+ }
1834
+ else {
1835
+ this.currentValue.set(this.fieldType() === 'number' && value === '' ? null : value);
1836
+ }
1837
+ }
1838
+ /** 通用值变更(string / boolean…) */
1839
+ onValueChange(value) {
1840
+ this.currentValue.set(value);
1841
+ this.valueChange.emit(value);
1842
+ this.valueChangeFn()?.(value);
1843
+ }
1844
+ /** UUID 值变更(带格式校验) */
1845
+ onUuidChange(value) {
1846
+ this.currentValue.set(value);
1847
+ const trimmed = value.trim();
1848
+ this.valueChange.emit(trimmed.toLowerCase() || '');
1849
+ this.valueChangeFn()?.(trimmed.toLowerCase() || '');
1850
+ }
1851
+ /** 数字输入变更 */
1852
+ onNumberInputChange(value) {
1853
+ const num = value === '' ? null : Number(value);
1854
+ this.currentValue.set(num);
1855
+ this.valueChange.emit(num);
1856
+ this.valueChangeFn()?.(num);
1857
+ }
1858
+ /** 枚举多选变更 */
1859
+ onEnumArrayChange(event) {
1860
+ const select = event.target;
1861
+ const selected = Array.from(select.selectedOptions).map(o => o.value);
1862
+ this.currentValue.set(selected);
1863
+ this.valueChange.emit(selected);
1864
+ this.valueChangeFn()?.(selected);
1865
+ }
1866
+ /** 枚举选项是否选中 */
1867
+ isEnumSelected(opt) {
1868
+ const val = this.currentValue();
1869
+ return Array.isArray(val) && val.includes(opt);
1870
+ }
1871
+ /** 枚举值选中(来自 PopoverSelectComponent) */
1872
+ onEnumSelect(value) {
1873
+ this.currentValue.set(value);
1874
+ this.valueChange.emit(value);
1875
+ this.valueChangeFn()?.(value);
1876
+ }
1877
+ /** 原生日期输入变更 */
1878
+ onNativeDateChange(value) {
1879
+ this.currentDateStr.set(value);
1880
+ this.valueChange.emit(value || '');
1881
+ this.valueChangeFn()?.(value || '');
1882
+ }
1883
+ /** 日期范围起始变更 */
1884
+ onDateRangeStartChange(value) {
1885
+ this.dateRangeStart.set(value);
1886
+ this.onDateRangePartChange();
1887
+ }
1888
+ /** 日期范围结束变更 */
1889
+ onDateRangeEndChange(value) {
1890
+ this.dateRangeEnd.set(value);
1891
+ this.onDateRangePartChange();
1892
+ }
1893
+ onDateRangePartChange() {
1894
+ const start = this.dateRangeStart();
1895
+ const end = this.dateRangeEnd();
1896
+ if (start && end) {
1897
+ this.valueChange.emit([start, end]);
1898
+ this.valueChangeFn()?.([start, end]);
1899
+ }
1900
+ else {
1901
+ this.valueChange.emit([]);
1902
+ this.valueChangeFn()?.([]);
1903
+ }
1904
+ }
1905
+ /** 范围最小值变更 */
1906
+ onRangeMinChange(value) {
1907
+ this.rangeMin.set(value === '' ? null : Number(value));
1908
+ this.onRangeChange();
1909
+ }
1910
+ /** 范围最大值变更 */
1911
+ onRangeMaxChange(value) {
1912
+ this.rangeMax.set(value === '' ? null : Number(value));
1913
+ this.onRangeChange();
1914
+ }
1915
+ onRangeChange() {
1916
+ const min = this.rangeMin();
1917
+ const max = this.rangeMax();
1918
+ if (min != null && max != null) {
1919
+ this.valueChange.emit([min, max]);
1920
+ this.valueChangeFn()?.([min, max]);
1921
+ }
1922
+ else {
1923
+ this.valueChange.emit([]);
1924
+ this.valueChangeFn()?.([]);
1925
+ }
1926
+ }
1927
+ /** 数组输入值变更(逗号分隔字符串) */
1928
+ onArrayInputChange(value) {
1929
+ this.arrayInputValue.set(value);
1930
+ const result = parseCommaSeparatedInput(value, this.fieldType());
1931
+ this.valueChange.emit(result);
1932
+ this.valueChangeFn()?.(result);
1933
+ }
1934
+ handleWhereChange(where) {
1935
+ const typedWhere = where;
1936
+ this.whereChange.emit(typedWhere);
1937
+ this.whereChangeFn()?.(typedWhere);
1938
+ }
1939
+ async loadSubqueryComponent() {
1940
+ if (!this.subqueryContainer || this.subqueryComponentRef || this.subqueryLoading)
1941
+ return;
1942
+ this.subqueryLoading = true;
1943
+ try {
1944
+ const { SubqueryBuilderComponent } = await Promise.resolve().then(function () { return subqueryBuilder_component; });
1945
+ this.subqueryComponentRef = this.subqueryContainer.createComponent(SubqueryBuilderComponent);
1946
+ this.subqueryComponentRef.setInput('fields', this.relationFields());
1947
+ this.subqueryComponentRef.setInput('initialQuery', this.where());
1948
+ this.subquerySubscription = this.subqueryComponentRef.instance.queryChange.subscribe((query) => {
1949
+ this.handleWhereChange(query);
1950
+ });
1951
+ }
1952
+ catch (error) {
1953
+ console.error('[ValueInput] Failed to load SubqueryBuilderComponent:', error);
1954
+ }
1955
+ finally {
1956
+ this.subqueryLoading = false;
1957
+ }
1958
+ }
1959
+ destroySubqueryComponent() {
1960
+ this.subquerySubscription?.unsubscribe();
1961
+ this.subquerySubscription = undefined;
1962
+ if (this.subqueryComponentRef) {
1963
+ this.subqueryComponentRef.destroy();
1964
+ this.subqueryComponentRef = undefined;
1965
+ }
1966
+ }
1967
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ValueInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1968
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: ValueInputComponent, isStandalone: true, selector: "rxdb-value-input", inputs: { fieldType: { classPropertyName: "fieldType", publicName: "fieldType", isSignal: true, isRequired: false, transformFunction: null }, operator: { classPropertyName: "operator", publicName: "operator", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, enumOptions: { classPropertyName: "enumOptions", publicName: "enumOptions", isSignal: true, isRequired: false, transformFunction: null }, relationFields: { classPropertyName: "relationFields", publicName: "relationFields", isSignal: true, isRequired: false, transformFunction: null }, where: { classPropertyName: "where", publicName: "where", isSignal: true, isRequired: false, transformFunction: null }, errorMessage: { classPropertyName: "errorMessage", publicName: "errorMessage", isSignal: true, isRequired: false, transformFunction: null }, valueChangeFn: { classPropertyName: "valueChangeFn", publicName: "valueChangeFn", isSignal: true, isRequired: false, transformFunction: null }, whereChangeFn: { classPropertyName: "whereChangeFn", publicName: "whereChangeFn", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange", whereChange: "whereChange" }, viewQueries: [{ propertyName: "subqueryContainer", first: true, predicate: ["subqueryContainer"], descendants: true, read: ViewContainerRef }], ngImport: i0, template: `
1969
+ @switch (inputType()) {
1970
+ @case ('none') {
1971
+ <span class="text-base-content/50 px-2 text-sm italic">无需输入值</span>
1972
+ }
1973
+ @case ('uuid') {
1974
+ <div
1975
+ [attr.data-tip]="uuidError() || errorMessage() || null"
1976
+ [class.tooltip-top]="!!uuidError() || hasError()"
1977
+ [class.tooltip]="!!uuidError() || hasError()"
1978
+ >
1979
+ <input
1980
+ class="input input-sm font-mono"
1981
+ [class.input-error]="hasError() || !!uuidError()"
1982
+ [value]="currentValue()"
1983
+ (input)="onUuidChange($any($event.target).value)"
1984
+ placeholder="输入 UUID"
1985
+ style="min-width: 16rem"
1986
+ type="text"
1987
+ />
1988
+ </div>
1989
+ }
1990
+ @case ('subquery') {
1991
+ <div class="subquery-container w-full">
1992
+ @if (!relationFields() || relationFields().length === 0) {
1993
+ <span class="text-base-content/50 px-2 text-sm italic">存在/不存在(无子条件)</span>
1994
+ } @else {
1995
+ <div #subqueryContainer></div>
1996
+ }
1997
+ </div>
1998
+ }
1999
+ @case ('boolean') {
2000
+ <input
2001
+ class="toggle toggle-sm"
2002
+ [checked]="!!currentValue()"
2003
+ (change)="onValueChange($any($event.target).checked)"
2004
+ type="checkbox"
2005
+ />
2006
+ }
2007
+ @case ('enum') {
2008
+ <rxdb-popover-select
2009
+ [options]="enumSelectOptions()"
2010
+ [selected]="currentValue()?.toString() ?? ''"
2011
+ (selectChange)="onEnumSelect($event)"
2012
+ minWidth="12rem"
2013
+ placeholder="选择值"
2014
+ />
2015
+ }
2016
+ @case ('enum-array') {
2017
+ <div [attr.data-tip]="errorMessage() || null" [class.tooltip-top]="hasError()" [class.tooltip]="hasError()">
2018
+ <select
2019
+ class="select select-sm"
2020
+ [class.select-error]="hasError()"
2021
+ (change)="onEnumArrayChange($event)"
2022
+ multiple
2023
+ style="min-width: 12rem; min-height: 6rem"
2024
+ >
2025
+ @for (opt of enumOptions(); track opt) {
2026
+ <option [selected]="isEnumSelected(opt)" [value]="opt">{{ opt }}</option>
2027
+ }
2028
+ </select>
2029
+ </div>
2030
+ }
2031
+ @case ('range') {
2032
+ @if (fieldType() === 'date') {
2033
+ <div
2034
+ class="flex items-center gap-2"
2035
+ [attr.data-tip]="errorMessage() || null"
2036
+ [class.tooltip-top]="hasError()"
2037
+ [class.tooltip]="hasError()"
2038
+ >
2039
+ <input
2040
+ class="input input-sm"
2041
+ [class.input-error]="hasError()"
2042
+ [value]="dateRangeStart()"
2043
+ (change)="onDateRangeStartChange($any($event.target).value)"
2044
+ style="width: 9rem"
2045
+ type="date"
2046
+ />
2047
+ <span class="text-base-content/50">至</span>
2048
+ <input
2049
+ class="input input-sm"
2050
+ [class.input-error]="hasError()"
2051
+ [value]="dateRangeEnd()"
2052
+ (change)="onDateRangeEndChange($any($event.target).value)"
2053
+ style="width: 9rem"
2054
+ type="date"
2055
+ />
2056
+ </div>
2057
+ } @else {
2058
+ <div
2059
+ class="flex items-center gap-2"
2060
+ [attr.data-tip]="errorMessage() || null"
2061
+ [class.tooltip-top]="hasError()"
2062
+ [class.tooltip]="hasError()"
2063
+ >
2064
+ <input
2065
+ class="input input-sm"
2066
+ [class.input-error]="hasError()"
2067
+ [value]="rangeMin() ?? ''"
2068
+ (input)="onRangeMinChange($any($event.target).value)"
2069
+ placeholder="最小值"
2070
+ style="width: 7rem"
2071
+ type="number"
2072
+ />
2073
+ <span class="text-base-content/50">至</span>
2074
+ <input
2075
+ class="input input-sm"
2076
+ [class.input-error]="hasError()"
2077
+ [value]="rangeMax() ?? ''"
2078
+ (input)="onRangeMaxChange($any($event.target).value)"
2079
+ placeholder="最大值"
2080
+ style="width: 7rem"
2081
+ type="number"
2082
+ />
2083
+ </div>
2084
+ }
2085
+ }
2086
+ @case ('number') {
2087
+ <div [attr.data-tip]="errorMessage() || null" [class.tooltip-top]="hasError()" [class.tooltip]="hasError()">
2088
+ <input
2089
+ class="input input-sm"
2090
+ [class.input-error]="hasError()"
2091
+ [value]="currentValue() ?? ''"
2092
+ (input)="onNumberInputChange($any($event.target).value)"
2093
+ placeholder="输入数值"
2094
+ type="number"
2095
+ />
2096
+ </div>
2097
+ }
2098
+ @case ('date') {
2099
+ <div [attr.data-tip]="errorMessage() || null" [class.tooltip-top]="hasError()" [class.tooltip]="hasError()">
2100
+ <input
2101
+ class="input input-sm"
2102
+ [class.input-error]="hasError()"
2103
+ [value]="currentDateStr()"
2104
+ (change)="onNativeDateChange($any($event.target).value)"
2105
+ style="width: 10rem"
2106
+ type="date"
2107
+ />
2108
+ </div>
2109
+ }
2110
+ @case ('array') {
2111
+ <div [attr.data-tip]="errorMessage() || null" [class.tooltip-top]="hasError()" [class.tooltip]="hasError()">
2112
+ <input
2113
+ class="input input-sm"
2114
+ [class.input-error]="hasError()"
2115
+ [value]="arrayInputValue()"
2116
+ (input)="onArrayInputChange($any($event.target).value)"
2117
+ placeholder="输入多个值,用逗号分隔"
2118
+ style="width: 100%; min-width: 12rem"
2119
+ type="text"
2120
+ />
2121
+ </div>
2122
+ }
2123
+ @default {
2124
+ <div [attr.data-tip]="errorMessage() || null" [class.tooltip-top]="hasError()" [class.tooltip]="hasError()">
2125
+ <input
2126
+ class="input input-sm"
2127
+ [class.input-error]="hasError()"
2128
+ [value]="currentValue() ?? ''"
2129
+ (input)="onValueChange($any($event.target).value)"
2130
+ placeholder="输入值"
2131
+ style="width: 100%; min-width: 12rem"
2132
+ type="text"
2133
+ />
2134
+ </div>
2135
+ }
2136
+ }
2137
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: PopoverSelectComponent, selector: "rxdb-popover-select", inputs: ["options", "selected", "placeholder", "minWidth"], outputs: ["selectChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2138
+ }
2139
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ValueInputComponent, decorators: [{
2140
+ type: Component,
2141
+ args: [{
2142
+ selector: 'rxdb-value-input',
2143
+ standalone: true,
2144
+ imports: [CommonModule, PopoverSelectComponent],
2145
+ template: `
2146
+ @switch (inputType()) {
2147
+ @case ('none') {
2148
+ <span class="text-base-content/50 px-2 text-sm italic">无需输入值</span>
2149
+ }
2150
+ @case ('uuid') {
2151
+ <div
2152
+ [attr.data-tip]="uuidError() || errorMessage() || null"
2153
+ [class.tooltip-top]="!!uuidError() || hasError()"
2154
+ [class.tooltip]="!!uuidError() || hasError()"
2155
+ >
2156
+ <input
2157
+ class="input input-sm font-mono"
2158
+ [class.input-error]="hasError() || !!uuidError()"
2159
+ [value]="currentValue()"
2160
+ (input)="onUuidChange($any($event.target).value)"
2161
+ placeholder="输入 UUID"
2162
+ style="min-width: 16rem"
2163
+ type="text"
2164
+ />
2165
+ </div>
2166
+ }
2167
+ @case ('subquery') {
2168
+ <div class="subquery-container w-full">
2169
+ @if (!relationFields() || relationFields().length === 0) {
2170
+ <span class="text-base-content/50 px-2 text-sm italic">存在/不存在(无子条件)</span>
2171
+ } @else {
2172
+ <div #subqueryContainer></div>
2173
+ }
2174
+ </div>
2175
+ }
2176
+ @case ('boolean') {
2177
+ <input
2178
+ class="toggle toggle-sm"
2179
+ [checked]="!!currentValue()"
2180
+ (change)="onValueChange($any($event.target).checked)"
2181
+ type="checkbox"
2182
+ />
2183
+ }
2184
+ @case ('enum') {
2185
+ <rxdb-popover-select
2186
+ [options]="enumSelectOptions()"
2187
+ [selected]="currentValue()?.toString() ?? ''"
2188
+ (selectChange)="onEnumSelect($event)"
2189
+ minWidth="12rem"
2190
+ placeholder="选择值"
2191
+ />
2192
+ }
2193
+ @case ('enum-array') {
2194
+ <div [attr.data-tip]="errorMessage() || null" [class.tooltip-top]="hasError()" [class.tooltip]="hasError()">
2195
+ <select
2196
+ class="select select-sm"
2197
+ [class.select-error]="hasError()"
2198
+ (change)="onEnumArrayChange($event)"
2199
+ multiple
2200
+ style="min-width: 12rem; min-height: 6rem"
2201
+ >
2202
+ @for (opt of enumOptions(); track opt) {
2203
+ <option [selected]="isEnumSelected(opt)" [value]="opt">{{ opt }}</option>
2204
+ }
2205
+ </select>
2206
+ </div>
2207
+ }
2208
+ @case ('range') {
2209
+ @if (fieldType() === 'date') {
2210
+ <div
2211
+ class="flex items-center gap-2"
2212
+ [attr.data-tip]="errorMessage() || null"
2213
+ [class.tooltip-top]="hasError()"
2214
+ [class.tooltip]="hasError()"
2215
+ >
2216
+ <input
2217
+ class="input input-sm"
2218
+ [class.input-error]="hasError()"
2219
+ [value]="dateRangeStart()"
2220
+ (change)="onDateRangeStartChange($any($event.target).value)"
2221
+ style="width: 9rem"
2222
+ type="date"
2223
+ />
2224
+ <span class="text-base-content/50">至</span>
2225
+ <input
2226
+ class="input input-sm"
2227
+ [class.input-error]="hasError()"
2228
+ [value]="dateRangeEnd()"
2229
+ (change)="onDateRangeEndChange($any($event.target).value)"
2230
+ style="width: 9rem"
2231
+ type="date"
2232
+ />
2233
+ </div>
2234
+ } @else {
2235
+ <div
2236
+ class="flex items-center gap-2"
2237
+ [attr.data-tip]="errorMessage() || null"
2238
+ [class.tooltip-top]="hasError()"
2239
+ [class.tooltip]="hasError()"
2240
+ >
2241
+ <input
2242
+ class="input input-sm"
2243
+ [class.input-error]="hasError()"
2244
+ [value]="rangeMin() ?? ''"
2245
+ (input)="onRangeMinChange($any($event.target).value)"
2246
+ placeholder="最小值"
2247
+ style="width: 7rem"
2248
+ type="number"
2249
+ />
2250
+ <span class="text-base-content/50">至</span>
2251
+ <input
2252
+ class="input input-sm"
2253
+ [class.input-error]="hasError()"
2254
+ [value]="rangeMax() ?? ''"
2255
+ (input)="onRangeMaxChange($any($event.target).value)"
2256
+ placeholder="最大值"
2257
+ style="width: 7rem"
2258
+ type="number"
2259
+ />
2260
+ </div>
2261
+ }
2262
+ }
2263
+ @case ('number') {
2264
+ <div [attr.data-tip]="errorMessage() || null" [class.tooltip-top]="hasError()" [class.tooltip]="hasError()">
2265
+ <input
2266
+ class="input input-sm"
2267
+ [class.input-error]="hasError()"
2268
+ [value]="currentValue() ?? ''"
2269
+ (input)="onNumberInputChange($any($event.target).value)"
2270
+ placeholder="输入数值"
2271
+ type="number"
2272
+ />
2273
+ </div>
2274
+ }
2275
+ @case ('date') {
2276
+ <div [attr.data-tip]="errorMessage() || null" [class.tooltip-top]="hasError()" [class.tooltip]="hasError()">
2277
+ <input
2278
+ class="input input-sm"
2279
+ [class.input-error]="hasError()"
2280
+ [value]="currentDateStr()"
2281
+ (change)="onNativeDateChange($any($event.target).value)"
2282
+ style="width: 10rem"
2283
+ type="date"
2284
+ />
2285
+ </div>
2286
+ }
2287
+ @case ('array') {
2288
+ <div [attr.data-tip]="errorMessage() || null" [class.tooltip-top]="hasError()" [class.tooltip]="hasError()">
2289
+ <input
2290
+ class="input input-sm"
2291
+ [class.input-error]="hasError()"
2292
+ [value]="arrayInputValue()"
2293
+ (input)="onArrayInputChange($any($event.target).value)"
2294
+ placeholder="输入多个值,用逗号分隔"
2295
+ style="width: 100%; min-width: 12rem"
2296
+ type="text"
2297
+ />
2298
+ </div>
2299
+ }
2300
+ @default {
2301
+ <div [attr.data-tip]="errorMessage() || null" [class.tooltip-top]="hasError()" [class.tooltip]="hasError()">
2302
+ <input
2303
+ class="input input-sm"
2304
+ [class.input-error]="hasError()"
2305
+ [value]="currentValue() ?? ''"
2306
+ (input)="onValueChange($any($event.target).value)"
2307
+ placeholder="输入值"
2308
+ style="width: 100%; min-width: 12rem"
2309
+ type="text"
2310
+ />
2311
+ </div>
2312
+ }
2313
+ }
2314
+ `,
2315
+ changeDetection: ChangeDetectionStrategy.OnPush
2316
+ }]
2317
+ }], ctorParameters: () => [], propDecorators: { subqueryContainer: [{
2318
+ type: ViewChild,
2319
+ args: ['subqueryContainer', { read: ViewContainerRef }]
2320
+ }], fieldType: [{ type: i0.Input, args: [{ isSignal: true, alias: "fieldType", required: false }] }], operator: [{ type: i0.Input, args: [{ isSignal: true, alias: "operator", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], enumOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "enumOptions", required: false }] }], relationFields: [{ type: i0.Input, args: [{ isSignal: true, alias: "relationFields", required: false }] }], where: [{ type: i0.Input, args: [{ isSignal: true, alias: "where", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }], whereChange: [{ type: i0.Output, args: ["whereChange"] }], errorMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorMessage", required: false }] }], valueChangeFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueChangeFn", required: false }] }], whereChangeFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "whereChangeFn", required: false }] }] } });
2321
+
2322
+ /**
2323
+ * 默认主题(原生 HTML 实现)
2324
+ *
2325
+ * @description
2326
+ * 不依赖任何第三方 UI 库,使用标准 HTML 表单元素。
2327
+ * 适合轻量级场景或作为自定义主题的参考实现。
2328
+ *
2329
+ * @example
2330
+ * ```typescript
2331
+ * // 默认主题无需额外配置,开箱即用
2332
+ * imports: [QueryBuilderComponent]
2333
+ * ```
2334
+ */
2335
+ const DEFAULT_QUERY_BUILDER_THEME = {
2336
+ fieldSelector: FieldSelectorComponent,
2337
+ operatorSelector: OperatorSelectorComponent,
2338
+ valueInput: ValueInputComponent
2339
+ };
2340
+
2341
+ /**
2342
+ * QueryBuilder 主题注入 Token
2343
+ *
2344
+ * @example
2345
+ * ```typescript
2346
+ * // 在应用根模块提供自定义主题
2347
+ * providers: [provideQueryBuilderTheme(QUERY_BUILDER_XXX_THEME)]
2348
+ * ```
2349
+ */
2350
+ const QUERY_BUILDER_THEME = new InjectionToken('QUERY_BUILDER_THEME');
2351
+ /**
2352
+ * 提供自定义主题
2353
+ */
2354
+ function provideQueryBuilderTheme(theme) {
2355
+ return { provide: QUERY_BUILDER_THEME, useValue: theme };
2356
+ }
2357
+
2358
+ /**
2359
+ * 单条查询规则组件
2360
+ *
2361
+ * @description
2362
+ * 显示一条查询规则的编辑界面:
2363
+ * - 字段选择器
2364
+ * - 操作符选择器
2365
+ * - 值输入框
2366
+ * - 删除按钮
2367
+ */
2368
+ class QueryRuleComponent {
2369
+ existsOperators = new Set(['exists', 'notExists']);
2370
+ noValueOperators = new Set(['null', 'notNull']);
2371
+ /** 注入的主题(回退到默认主题) */
2372
+ theme = inject(QUERY_BUILDER_THEME, { optional: true }) ?? DEFAULT_QUERY_BUILDER_THEME;
2373
+ rule = input.required(...(ngDevMode ? [{ debugName: "rule" }] : /* istanbul ignore next */ []));
2374
+ fields = input.required(...(ngDevMode ? [{ debugName: "fields" }] : /* istanbul ignore next */ []));
2375
+ errors = input([], ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
2376
+ update = output();
2377
+ remove = output();
2378
+ fieldAsString = computed(() => this.rule().field, ...(ngDevMode ? [{ debugName: "fieldAsString" }] : /* istanbul ignore next */ []));
2379
+ ruleErrors = computed(() => {
2380
+ const seen = new Set();
2381
+ return this.errors().filter(e => {
2382
+ if (e.field !== this.rule().field)
2383
+ return false;
2384
+ if (seen.has(e.message))
2385
+ return false;
2386
+ seen.add(e.message);
2387
+ return true;
2388
+ });
2389
+ }, ...(ngDevMode ? [{ debugName: "ruleErrors" }] : /* istanbul ignore next */ []));
2390
+ errorTooltip = computed(() => this.ruleErrors()
2391
+ .map(e => e.message)
2392
+ .join(' | '), ...(ngDevMode ? [{ debugName: "errorTooltip" }] : /* istanbul ignore next */ []));
2393
+ isExistsOperator = computed(() => this.existsOperators.has(this.rule().operator), ...(ngDevMode ? [{ debugName: "isExistsOperator" }] : /* istanbul ignore next */ []));
2394
+ isNoValueOperator = computed(() => this.noValueOperators.has(this.rule().operator), ...(ngDevMode ? [{ debugName: "isNoValueOperator" }] : /* istanbul ignore next */ []));
2395
+ /** 子查询面板是否展开(已有 where 规则时自动展开) */
2396
+ showSubquery = signal(false, ...(ngDevMode ? [{ debugName: "showSubquery" }] : /* istanbul ignore next */ []));
2397
+ /**
2398
+ * 当规则的 where 已有内容时自动展开。
2399
+ * 使用 computed 包装监听 rule().where,在显示按钮前检查。
2400
+ */
2401
+ subqueryVisible = computed(() => {
2402
+ const where = this.rule().where;
2403
+ if (where && where.rules && where.rules.length > 0) {
2404
+ return true;
2405
+ }
2406
+ return this.showSubquery();
2407
+ }, ...(ngDevMode ? [{ debugName: "subqueryVisible" }] : /* istanbul ignore next */ []));
2408
+ currentField = computed(() => {
2409
+ const fieldName = this.rule().field;
2410
+ return this.fields().find(f => f.name === fieldName);
2411
+ }, ...(ngDevMode ? [{ debugName: "currentField" }] : /* istanbul ignore next */ []));
2412
+ currentFieldType = computed(() => this.currentField()?.type ?? 'string', ...(ngDevMode ? [{ debugName: "currentFieldType" }] : /* istanbul ignore next */ []));
2413
+ currentEnumOptions = computed(() => this.currentField()?.enum ?? [], ...(ngDevMode ? [{ debugName: "currentEnumOptions" }] : /* istanbul ignore next */ []));
2414
+ /** 当前字段的关系目标实体字段列表 */
2415
+ currentRelationFields = computed(() => {
2416
+ return this.currentField()?.relationFields ?? [];
2417
+ }, ...(ngDevMode ? [{ debugName: "currentRelationFields" }] : /* istanbul ignore next */ []));
2418
+ /** 规则的 where 子查询 */
2419
+ ruleWhere = computed(() => this.rule().where, ...(ngDevMode ? [{ debugName: "ruleWhere" }] : /* istanbul ignore next */ []));
2420
+ /** NgComponentOutlet inputs:字段选择器(fieldChangeCb 是 lazy 引用,声明顺序不影响运行时)*/
2421
+ fieldSelectorInputs = computed(() => ({
2422
+ fields: this.fields(),
2423
+ selectedField: this.fieldAsString(),
2424
+ fieldChangeFn: this.fieldChangeCb
2425
+ }), ...(ngDevMode ? [{ debugName: "fieldSelectorInputs" }] : /* istanbul ignore next */ []));
2426
+ /** NgComponentOutlet inputs:操作符选择器 */
2427
+ operatorSelectorInputs = computed(() => ({
2428
+ fieldMetadata: this.currentField(),
2429
+ fieldType: this.currentFieldType(),
2430
+ selectedOperator: this.rule().operator,
2431
+ operatorChangeFn: this.operatorChangeCb
2432
+ }), ...(ngDevMode ? [{ debugName: "operatorSelectorInputs" }] : /* istanbul ignore next */ []));
2433
+ /** NgComponentOutlet inputs:值输入 */
2434
+ valueInputInputs = computed(() => ({
2435
+ fieldType: this.currentFieldType(),
2436
+ operator: this.rule().operator,
2437
+ value: this.rule().value,
2438
+ enumOptions: this.currentEnumOptions(),
2439
+ relationFields: this.currentRelationFields(),
2440
+ where: this.ruleWhere(),
2441
+ errorMessage: this.errorTooltip(),
2442
+ valueChangeFn: this.valueChangeCb,
2443
+ whereChangeFn: this.whereChangeCb
2444
+ }), ...(ngDevMode ? [{ debugName: "valueInputInputs" }] : /* istanbul ignore next */ []));
2445
+ /** 子条件清空/全部删除时自动收起面板并恢复 +子条件 按钮 */
2446
+ constructor() {
2447
+ effect(() => {
2448
+ const where = this.rule().where;
2449
+ const hasRules = where && where.rules && where.rules.length > 0;
2450
+ if (!hasRules) {
2451
+ untracked(() => this.showSubquery.set(false));
2452
+ }
2453
+ });
2454
+ }
2455
+ // === 公有方法 ===
2456
+ /** 字段变更 */
2457
+ onFieldChange(fieldName) {
2458
+ const field = this.fields().find(f => f.name === fieldName);
2459
+ this.update.emit({
2460
+ id: this.rule().id,
2461
+ updates: {
2462
+ field: fieldName,
2463
+ operator: field?.isRelation ? 'exists'
2464
+ : field?.type === 'keyValue' ? 'null'
2465
+ : '=',
2466
+ value: field?.type === 'boolean' ? false : '',
2467
+ where: undefined // 重置子查询
2468
+ }
2469
+ });
2470
+ }
2471
+ /** 添加子条件:展开面板并创建一条默认子规则 */
2472
+ onAddSubcondition() {
2473
+ this.showSubquery.set(true);
2474
+ const relationFields = this.currentRelationFields();
2475
+ const firstField = relationFields[0];
2476
+ if (firstField) {
2477
+ const where = {
2478
+ id: '',
2479
+ combinator: 'and',
2480
+ rules: [
2481
+ {
2482
+ id: '',
2483
+ field: firstField.name,
2484
+ operator: firstField.isRelation ? 'exists'
2485
+ : firstField.type === 'keyValue' ? 'null'
2486
+ : '=',
2487
+ value: firstField.type === 'boolean' ? false : ''
2488
+ }
2489
+ ]
2490
+ };
2491
+ this.update.emit({ id: this.rule().id, updates: { where } });
2492
+ }
2493
+ }
2494
+ /** 操作符变更 */
2495
+ onOperatorChange(operator) {
2496
+ const updates = { operator };
2497
+ if (this.existsOperators.has(operator) || this.noValueOperators.has(operator)) {
2498
+ updates.value = null;
2499
+ }
2500
+ if (!this.existsOperators.has(operator)) {
2501
+ updates.where = undefined;
2502
+ this.showSubquery.set(false);
2503
+ }
2504
+ this.update.emit({ id: this.rule().id, updates });
2505
+ }
2506
+ /** 值变更 */
2507
+ onValueChange(value) {
2508
+ this.update.emit({
2509
+ id: this.rule().id,
2510
+ updates: { value }
2511
+ });
2512
+ }
2513
+ /** 子查询条件变更 */
2514
+ onWhereChange(where) {
2515
+ this.update.emit({
2516
+ id: this.rule().id,
2517
+ updates: { where }
2518
+ });
2519
+ }
2520
+ /** 删除规则 */
2521
+ onRemove() {
2522
+ this.remove.emit();
2523
+ }
2524
+ // === 私有 callback 字段(computed signal 是懒求值的,this.onXxx 在调用时已初始化)===
2525
+ fieldChangeCb = (field) => this.onFieldChange(field);
2526
+ operatorChangeCb = (op) => this.onOperatorChange(op);
2527
+ valueChangeCb = (value) => this.onValueChange(value);
2528
+ whereChangeCb = (where) => this.onWhereChange(where);
2529
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: QueryRuleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2530
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: QueryRuleComponent, isStandalone: true, selector: "rxdb-query-rule", inputs: { rule: { classPropertyName: "rule", publicName: "rule", isSignal: true, isRequired: true, transformFunction: null }, fields: { classPropertyName: "fields", publicName: "fields", isSignal: true, isRequired: true, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { update: "update", remove: "remove" }, ngImport: i0, template: `
2531
+ <div class="rxdb-query-rule group flex flex-col rounded p-1">
2532
+ <div class="flex flex-wrap items-center gap-2">
2533
+ <ng-container [ngComponentOutlet]="theme.fieldSelector" [ngComponentOutletInputs]="fieldSelectorInputs()" />
2534
+ <ng-container
2535
+ [ngComponentOutlet]="theme.operatorSelector"
2536
+ [ngComponentOutletInputs]="operatorSelectorInputs()"
2537
+ />
2538
+ @if (isExistsOperator()) {
2539
+ @if (!subqueryVisible()) {
2540
+ <button class="btn btn-ghost btn-xs" (click)="onAddSubcondition()" title="添加子条件" type="button">
2541
+ + 子条件(可选)
2542
+ </button>
2543
+ }
2544
+ } @else if (!isNoValueOperator()) {
2545
+ <ng-container [ngComponentOutlet]="theme.valueInput" [ngComponentOutletInputs]="valueInputInputs()" />
2546
+ }
2547
+ <button
2548
+ class="btn btn-ghost btn-xs btn-square text-base-content/30 hover:text-error ml-auto"
2549
+ (click)="onRemove()"
2550
+ title="删除条件"
2551
+ type="button"
2552
+ >
2553
+ ✕
2554
+ </button>
2555
+ </div>
2556
+ @if (isExistsOperator() && subqueryVisible()) {
2557
+ <ng-container [ngComponentOutlet]="theme.valueInput" [ngComponentOutletInputs]="valueInputInputs()" />
2558
+ }
2559
+ </div>
2560
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2561
+ }
2562
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: QueryRuleComponent, decorators: [{
2563
+ type: Component,
2564
+ args: [{
2565
+ selector: 'rxdb-query-rule',
2566
+ standalone: true,
2567
+ imports: [CommonModule, NgComponentOutlet],
2568
+ template: `
2569
+ <div class="rxdb-query-rule group flex flex-col rounded p-1">
2570
+ <div class="flex flex-wrap items-center gap-2">
2571
+ <ng-container [ngComponentOutlet]="theme.fieldSelector" [ngComponentOutletInputs]="fieldSelectorInputs()" />
2572
+ <ng-container
2573
+ [ngComponentOutlet]="theme.operatorSelector"
2574
+ [ngComponentOutletInputs]="operatorSelectorInputs()"
2575
+ />
2576
+ @if (isExistsOperator()) {
2577
+ @if (!subqueryVisible()) {
2578
+ <button class="btn btn-ghost btn-xs" (click)="onAddSubcondition()" title="添加子条件" type="button">
2579
+ + 子条件(可选)
2580
+ </button>
2581
+ }
2582
+ } @else if (!isNoValueOperator()) {
2583
+ <ng-container [ngComponentOutlet]="theme.valueInput" [ngComponentOutletInputs]="valueInputInputs()" />
2584
+ }
2585
+ <button
2586
+ class="btn btn-ghost btn-xs btn-square text-base-content/30 hover:text-error ml-auto"
2587
+ (click)="onRemove()"
2588
+ title="删除条件"
2589
+ type="button"
2590
+ >
2591
+ ✕
2592
+ </button>
2593
+ </div>
2594
+ @if (isExistsOperator() && subqueryVisible()) {
2595
+ <ng-container [ngComponentOutlet]="theme.valueInput" [ngComponentOutletInputs]="valueInputInputs()" />
2596
+ }
2597
+ </div>
2598
+ `,
2599
+ changeDetection: ChangeDetectionStrategy.OnPush
2600
+ }]
2601
+ }], ctorParameters: () => [], propDecorators: { rule: [{ type: i0.Input, args: [{ isSignal: true, alias: "rule", required: true }] }], fields: [{ type: i0.Input, args: [{ isSignal: true, alias: "fields", required: true }] }], errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], update: [{ type: i0.Output, args: ["update"] }], remove: [{ type: i0.Output, args: ["remove"] }] } });
2602
+
2603
+ class QueryDragDropHandler {
2604
+ moveItemFn;
2605
+ state = signal({
2606
+ draggedItemId: null,
2607
+ targetItemId: null,
2608
+ dropMode: null,
2609
+ isValidTarget: false
2610
+ }, ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
2611
+ constructor(moveItemFn) {
2612
+ this.moveItemFn = moveItemFn;
2613
+ }
2614
+ start(itemId) {
2615
+ this.state.set({ draggedItemId: itemId, targetItemId: null, dropMode: null, isValidTarget: false });
2616
+ }
2617
+ over(targetItemId, dropMode, isValid, depth) {
2618
+ this.state.update(current => {
2619
+ if (current.depth !== undefined && depth < current.depth)
2620
+ return current;
2621
+ return { ...current, targetItemId, dropMode, isValidTarget: isValid, depth };
2622
+ });
2623
+ }
2624
+ leave() {
2625
+ this.state.update(s => ({ ...s, targetItemId: null, dropMode: null, isValidTarget: false, depth: undefined }));
2626
+ }
2627
+ drop(itemId, targetGroupId, targetIndex) {
2628
+ try {
2629
+ this.moveItemFn(itemId, targetGroupId, targetIndex);
2630
+ }
2631
+ catch {
2632
+ // 服务层拒绝(如循环嵌套),静默忽略
2633
+ }
2634
+ this.reset();
2635
+ }
2636
+ end() {
2637
+ this.reset();
2638
+ }
2639
+ reset() {
2640
+ this.state.set({ draggedItemId: null, targetItemId: null, dropMode: null, isValidTarget: false });
2641
+ }
2642
+ }
2643
+ function calculateDropMode(clientY, rect, isGroup) {
2644
+ const ratio = (clientY - rect.top) / rect.height;
2645
+ if (isGroup) {
2646
+ if (ratio < 0.25)
2647
+ return 'before';
2648
+ if (ratio > 0.75)
2649
+ return 'after';
2650
+ return 'into';
2651
+ }
2652
+ return ratio < 0.5 ? 'before' : 'after';
2653
+ }
2654
+ /**
2655
+ * 查询规则组组件(递归)
2656
+ *
2657
+ * @description
2658
+ * 显示一组查询规则,支持:
2659
+ * - AND/OR 切换(Teable 风格:行内 conjunction 前缀)
2660
+ * - 添加规则/子组
2661
+ * - 删除组
2662
+ * - 递归嵌套显示
2663
+ */
2664
+ class QueryGroupComponent {
2665
+ group = input.required(...(ngDevMode ? [{ debugName: "group" }] : /* istanbul ignore next */ []));
2666
+ fields = input.required(...(ngDevMode ? [{ debugName: "fields" }] : /* istanbul ignore next */ []));
2667
+ errors = input([], ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
2668
+ depth = input(0, ...(ngDevMode ? [{ debugName: "depth" }] : /* istanbul ignore next */ []));
2669
+ maxDepth = input(5, ...(ngDevMode ? [{ debugName: "maxDepth" }] : /* istanbul ignore next */ []));
2670
+ dragDropHandler = input(...(ngDevMode ? [undefined, { debugName: "dragDropHandler" }] : /* istanbul ignore next */ []));
2671
+ /** 是否允许展开/收起组(默认开启) */
2672
+ allowCollapse = input(true, ...(ngDevMode ? [{ debugName: "allowCollapse" }] : /* istanbul ignore next */ []));
2673
+ addRule = output();
2674
+ addGroup = output();
2675
+ removeItem = output();
2676
+ updateRule = output();
2677
+ updateCombinator = output();
2678
+ /** 当前组是否已折叠 */
2679
+ collapsed = signal(false, ...(ngDevMode ? [{ debugName: "collapsed" }] : /* istanbul ignore next */ []));
2680
+ /** 是否可以添加子组(未达到最大深度) */
2681
+ canAddGroup = computed(() => this.depth() < this.maxDepth() - 1, ...(ngDevMode ? [{ debugName: "canAddGroup" }] : /* istanbul ignore next */ []));
2682
+ /** 切换折叠状态 */
2683
+ toggleCollapse() {
2684
+ this.collapsed.update(v => !v);
2685
+ }
2686
+ /** 切换组合器 */
2687
+ toggleCombinator(combinator) {
2688
+ if (this.group().combinator !== combinator) {
2689
+ this.updateCombinator.emit({ id: this.group().id, combinator });
2690
+ }
2691
+ }
2692
+ /** 添加规则 */
2693
+ onAddRule() {
2694
+ const firstField = this.fields()[0];
2695
+ if (firstField) {
2696
+ this.addRule.emit({
2697
+ parentId: this.group().id,
2698
+ rule: {
2699
+ field: firstField.name,
2700
+ operator: firstField.isRelation ? 'exists'
2701
+ : firstField.type === 'keyValue' ? 'null'
2702
+ : '=',
2703
+ value: firstField.type === 'boolean' ? false : ''
2704
+ }
2705
+ });
2706
+ }
2707
+ }
2708
+ /** 添加子组 */
2709
+ onAddGroup() {
2710
+ this.addGroup.emit(this.group().id);
2711
+ }
2712
+ /** 删除此组 */
2713
+ onRemove() {
2714
+ this.removeItem.emit(this.group().id);
2715
+ }
2716
+ /** 规则更新 */
2717
+ onRuleUpdate(event) {
2718
+ this.updateRule.emit(event);
2719
+ }
2720
+ /** 删除规则 */
2721
+ onRuleRemove(id) {
2722
+ this.removeItem.emit(id);
2723
+ }
2724
+ /** 类型守卫:是否为规则组 */
2725
+ isRuleGroup(item) {
2726
+ return 'combinator' in item && 'rules' in item;
2727
+ }
2728
+ /** 类型转换:转为规则组 */
2729
+ asRuleGroup(item) {
2730
+ return item;
2731
+ }
2732
+ /** 类型转换:转为规则 */
2733
+ asRule(item) {
2734
+ return item;
2735
+ }
2736
+ /** 追踪函数 */
2737
+ trackByItem(item) {
2738
+ return item.id;
2739
+ }
2740
+ isDragging(itemId) {
2741
+ return this.dragDropHandler()?.state().draggedItemId === itemId;
2742
+ }
2743
+ isDropTarget(itemId, mode) {
2744
+ const state = this.dragDropHandler()?.state();
2745
+ return !!state && state.targetItemId === itemId && state.dropMode === mode && state.isValidTarget;
2746
+ }
2747
+ isDropInvalid(itemId) {
2748
+ const state = this.dragDropHandler()?.state();
2749
+ return !!state && state.targetItemId === itemId && !state.isValidTarget && !!state.draggedItemId;
2750
+ }
2751
+ onItemDragStart(event, item) {
2752
+ const handler = this.dragDropHandler();
2753
+ if (!handler || !event.dataTransfer)
2754
+ return;
2755
+ event.stopPropagation();
2756
+ event.dataTransfer.effectAllowed = 'move';
2757
+ event.dataTransfer.setData('text/plain', item.id);
2758
+ const row = event.target.closest('.rxdb-drag-item');
2759
+ if (row) {
2760
+ const rect = row.getBoundingClientRect();
2761
+ event.dataTransfer.setDragImage(row, event.clientX - rect.left, event.clientY - rect.top);
2762
+ }
2763
+ handler.start(item.id);
2764
+ }
2765
+ onItemDragOver(event, item, index, isGroup) {
2766
+ event.preventDefault();
2767
+ event.stopPropagation();
2768
+ const handler = this.dragDropHandler();
2769
+ if (!handler || !event.dataTransfer)
2770
+ return;
2771
+ const rect = event.currentTarget.getBoundingClientRect();
2772
+ const dropMode = calculateDropMode(event.clientY, rect, isGroup);
2773
+ const draggedId = handler.state().draggedItemId;
2774
+ const isValid = !!draggedId && draggedId !== item.id && (dropMode !== 'into' || isGroup);
2775
+ event.dataTransfer.dropEffect = isValid ? 'move' : 'none';
2776
+ handler.over(item.id, dropMode, isValid, this.depth());
2777
+ }
2778
+ onItemDragLeave(event) {
2779
+ const target = event.currentTarget;
2780
+ const related = event.relatedTarget;
2781
+ if (!related || !target.contains(related)) {
2782
+ this.dragDropHandler()?.leave();
2783
+ }
2784
+ }
2785
+ onItemDrop(event, targetItem, targetIndex, isGroup) {
2786
+ event.preventDefault();
2787
+ event.stopPropagation();
2788
+ const handler = this.dragDropHandler();
2789
+ const state = handler?.state();
2790
+ if (!handler || !state?.draggedItemId || !state.isValidTarget || !state.dropMode) {
2791
+ handler?.end();
2792
+ return;
2793
+ }
2794
+ const dropMode = state.dropMode;
2795
+ let finalTargetGroupId;
2796
+ let finalTargetIndex;
2797
+ if (dropMode === 'into' && isGroup) {
2798
+ const subGroup = targetItem;
2799
+ finalTargetGroupId = subGroup.id;
2800
+ const draggedInTarget = subGroup.rules.some(r => r.id === state.draggedItemId);
2801
+ finalTargetIndex = draggedInTarget ? subGroup.rules.length - 1 : subGroup.rules.length;
2802
+ }
2803
+ else {
2804
+ finalTargetGroupId = this.group().id;
2805
+ const rules = this.group().rules;
2806
+ const draggedIndex = rules.findIndex(r => r.id === state.draggedItemId);
2807
+ const isInSameGroup = draggedIndex !== -1;
2808
+ let adjustedIndex = targetIndex;
2809
+ if (isInSameGroup && draggedIndex < targetIndex) {
2810
+ adjustedIndex--;
2811
+ }
2812
+ finalTargetIndex = dropMode === 'before' ? adjustedIndex : adjustedIndex + 1;
2813
+ }
2814
+ handler.drop(state.draggedItemId, finalTargetGroupId, finalTargetIndex);
2815
+ }
2816
+ onItemDragEnd() {
2817
+ this.dragDropHandler()?.end();
2818
+ }
2819
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: QueryGroupComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2820
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: QueryGroupComponent, isStandalone: true, selector: "rxdb-query-group", inputs: { group: { classPropertyName: "group", publicName: "group", isSignal: true, isRequired: true, transformFunction: null }, fields: { classPropertyName: "fields", publicName: "fields", isSignal: true, isRequired: true, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, depth: { classPropertyName: "depth", publicName: "depth", isSignal: true, isRequired: false, transformFunction: null }, maxDepth: { classPropertyName: "maxDepth", publicName: "maxDepth", isSignal: true, isRequired: false, transformFunction: null }, dragDropHandler: { classPropertyName: "dragDropHandler", publicName: "dragDropHandler", isSignal: true, isRequired: false, transformFunction: null }, allowCollapse: { classPropertyName: "allowCollapse", publicName: "allowCollapse", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { addRule: "addRule", addGroup: "addGroup", removeItem: "removeItem", updateRule: "updateRule", updateCombinator: "updateCombinator" }, ngImport: i0, template: `
2821
+ <div class="rxdb-query-group border-base-300 my-1 rounded-lg border p-2" [class.bg-base-200]="depth() % 2 === 1">
2822
+ <!-- 组合器 + 操作按钮(同一行) -->
2823
+ <div class="flex items-center gap-2" [class.mb-2]="!collapsed()">
2824
+ <div class="join">
2825
+ <button
2826
+ class="btn btn-xs join-item"
2827
+ [class.btn-ghost]="group().combinator !== 'and'"
2828
+ [class.btn-primary]="group().combinator === 'and'"
2829
+ (click)="toggleCombinator('and')"
2830
+ type="button"
2831
+ >
2832
+ AND
2833
+ </button>
2834
+ <button
2835
+ class="btn btn-xs join-item"
2836
+ [class.btn-ghost]="group().combinator !== 'or'"
2837
+ [class.btn-primary]="group().combinator === 'or'"
2838
+ (click)="toggleCombinator('or')"
2839
+ type="button"
2840
+ >
2841
+ OR
2842
+ </button>
2843
+ </div>
2844
+
2845
+ @if (allowCollapse()) {
2846
+ <button
2847
+ class="btn btn-ghost btn-xs btn-square"
2848
+ [title]="collapsed() ? '展开' : '收起'"
2849
+ (click)="toggleCollapse()"
2850
+ type="button"
2851
+ >
2852
+ <svg
2853
+ class="h-3 w-3 transition-transform duration-200"
2854
+ [class.-rotate-90]="collapsed()"
2855
+ fill="none"
2856
+ stroke="currentColor"
2857
+ stroke-linecap="round"
2858
+ stroke-linejoin="round"
2859
+ stroke-width="2.5"
2860
+ viewBox="0 0 24 24"
2861
+ xmlns="http://www.w3.org/2000/svg"
2862
+ >
2863
+ <polyline points="6 9 12 15 18 9" />
2864
+ </svg>
2865
+ </button>
2866
+ }
2867
+
2868
+ <div class="flex-1"></div>
2869
+
2870
+ @if (!collapsed()) {
2871
+ <button class="btn btn-ghost btn-xs" (click)="onAddRule()" type="button">+ 条件</button>
2872
+
2873
+ @if (canAddGroup()) {
2874
+ <button class="btn btn-ghost btn-xs" (click)="onAddGroup()" type="button">+ 分组</button>
2875
+ }
2876
+
2877
+ @if (depth() > 0) {
2878
+ <button
2879
+ class="btn btn-ghost btn-xs btn-square text-base-content/40 hover:text-error"
2880
+ (click)="onRemove()"
2881
+ title="删除组"
2882
+ type="button"
2883
+ >
2884
+ ✕
2885
+ </button>
2886
+ }
2887
+ }
2888
+ </div>
2889
+
2890
+ <!-- 规则列表 -->
2891
+ @if (!collapsed()) {
2892
+ <div class="space-y-1">
2893
+ @for (item of group().rules; track trackByItem(item); let idx = $index) {
2894
+ @if (isRuleGroup(item)) {
2895
+ <div
2896
+ class="rxdb-drag-item"
2897
+ [class.dragging]="isDragging(item.id)"
2898
+ [class.drop-invalid]="isDropInvalid(item.id)"
2899
+ [class.drop-target-after]="isDropTarget(item.id, 'after')"
2900
+ [class.drop-target-before]="isDropTarget(item.id, 'before')"
2901
+ [class.drop-target-into]="isDropTarget(item.id, 'into')"
2902
+ (dragend)="onItemDragEnd()"
2903
+ (dragleave)="onItemDragLeave($event)"
2904
+ (dragover)="onItemDragOver($event, item, idx, true)"
2905
+ (drop)="onItemDrop($event, item, idx, true)"
2906
+ >
2907
+ <div class="flex items-start gap-1">
2908
+ @if (dragDropHandler()) {
2909
+ <span
2910
+ class="btn btn-ghost btn-xs btn-square text-base-content/30 hover:text-base-content/60 mt-1 cursor-grab"
2911
+ (dragstart)="onItemDragStart($event, item)"
2912
+ draggable="true"
2913
+ >⠿</span
2914
+ >
2915
+ }
2916
+ <div class="min-w-0 flex-1">
2917
+ <rxdb-query-group
2918
+ [allowCollapse]="allowCollapse()"
2919
+ [depth]="depth() + 1"
2920
+ [dragDropHandler]="dragDropHandler()"
2921
+ [errors]="errors()"
2922
+ [fields]="fields()"
2923
+ [group]="asRuleGroup(item)"
2924
+ [maxDepth]="maxDepth()"
2925
+ (addGroup)="addGroup.emit($event)"
2926
+ (addRule)="addRule.emit($event)"
2927
+ (removeItem)="removeItem.emit($event)"
2928
+ (updateCombinator)="updateCombinator.emit($event)"
2929
+ (updateRule)="updateRule.emit($event)"
2930
+ />
2931
+ </div>
2932
+ </div>
2933
+ </div>
2934
+ } @else {
2935
+ <div
2936
+ class="rxdb-drag-item"
2937
+ [class.dragging]="isDragging(item.id)"
2938
+ [class.drop-invalid]="isDropInvalid(item.id)"
2939
+ [class.drop-target-after]="isDropTarget(item.id, 'after')"
2940
+ [class.drop-target-before]="isDropTarget(item.id, 'before')"
2941
+ (dragend)="onItemDragEnd()"
2942
+ (dragleave)="onItemDragLeave($event)"
2943
+ (dragover)="onItemDragOver($event, item, idx, false)"
2944
+ (drop)="onItemDrop($event, item, idx, false)"
2945
+ >
2946
+ <div class="flex items-center gap-1">
2947
+ @if (dragDropHandler()) {
2948
+ <span
2949
+ class="btn btn-ghost btn-xs btn-square text-base-content/30 hover:text-base-content/60 cursor-grab"
2950
+ (dragstart)="onItemDragStart($event, item)"
2951
+ draggable="true"
2952
+ >⠿</span
2953
+ >
2954
+ }
2955
+ <div class="min-w-0 flex-1">
2956
+ <rxdb-query-rule
2957
+ [errors]="errors()"
2958
+ [fields]="fields()"
2959
+ [rule]="asRule(item)"
2960
+ (remove)="onRuleRemove(asRule(item).id)"
2961
+ (update)="onRuleUpdate($event)"
2962
+ />
2963
+ </div>
2964
+ </div>
2965
+ </div>
2966
+ }
2967
+ }
2968
+ </div>
2969
+
2970
+ <!-- 空状态 -->
2971
+ @if (group().rules.length === 0) {
2972
+ <div class="text-base-content/50 py-2 text-center text-sm select-none">暂无条件</div>
2973
+ }
2974
+ }
2975
+ </div>
2976
+ `, isInline: true, styles: [".rxdb-drag-item{position:relative}.rxdb-drag-item.dragging{opacity:.4}.rxdb-drag-item.drop-target-before:before{content:\"\";position:absolute;top:-2px;left:0;right:0;height:3px;background:#0083e0;background:oklch(.6 .2 250);border-radius:2px;z-index:1}.rxdb-drag-item.drop-target-after:after{content:\"\";position:absolute;bottom:-2px;left:0;right:0;height:3px;background:#0083e0;background:oklch(.6 .2 250);border-radius:2px;z-index:1}.rxdb-drag-item.drop-target-into{outline:2px dashed oklch(.6 .2 250);outline-offset:-2px;border-radius:.5rem;background:#0083e01a;background:oklch(.6 .2 250 / 10%)}.rxdb-drag-item.drop-invalid{outline:2px dashed oklch(.6 .2 30);outline-offset:-2px;border-radius:.5rem}\n"], dependencies: [{ kind: "component", type: QueryGroupComponent, selector: "rxdb-query-group", inputs: ["group", "fields", "errors", "depth", "maxDepth", "dragDropHandler", "allowCollapse"], outputs: ["addRule", "addGroup", "removeItem", "updateRule", "updateCombinator"] }, { kind: "ngmodule", type: CommonModule }, { kind: "component", type: QueryRuleComponent, selector: "rxdb-query-rule", inputs: ["rule", "fields", "errors"], outputs: ["update", "remove"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2977
+ }
2978
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: QueryGroupComponent, decorators: [{
2979
+ type: Component,
2980
+ args: [{ selector: 'rxdb-query-group', standalone: true, imports: [CommonModule, QueryRuleComponent], template: `
2981
+ <div class="rxdb-query-group border-base-300 my-1 rounded-lg border p-2" [class.bg-base-200]="depth() % 2 === 1">
2982
+ <!-- 组合器 + 操作按钮(同一行) -->
2983
+ <div class="flex items-center gap-2" [class.mb-2]="!collapsed()">
2984
+ <div class="join">
2985
+ <button
2986
+ class="btn btn-xs join-item"
2987
+ [class.btn-ghost]="group().combinator !== 'and'"
2988
+ [class.btn-primary]="group().combinator === 'and'"
2989
+ (click)="toggleCombinator('and')"
2990
+ type="button"
2991
+ >
2992
+ AND
2993
+ </button>
2994
+ <button
2995
+ class="btn btn-xs join-item"
2996
+ [class.btn-ghost]="group().combinator !== 'or'"
2997
+ [class.btn-primary]="group().combinator === 'or'"
2998
+ (click)="toggleCombinator('or')"
2999
+ type="button"
3000
+ >
3001
+ OR
3002
+ </button>
3003
+ </div>
3004
+
3005
+ @if (allowCollapse()) {
3006
+ <button
3007
+ class="btn btn-ghost btn-xs btn-square"
3008
+ [title]="collapsed() ? '展开' : '收起'"
3009
+ (click)="toggleCollapse()"
3010
+ type="button"
3011
+ >
3012
+ <svg
3013
+ class="h-3 w-3 transition-transform duration-200"
3014
+ [class.-rotate-90]="collapsed()"
3015
+ fill="none"
3016
+ stroke="currentColor"
3017
+ stroke-linecap="round"
3018
+ stroke-linejoin="round"
3019
+ stroke-width="2.5"
3020
+ viewBox="0 0 24 24"
3021
+ xmlns="http://www.w3.org/2000/svg"
3022
+ >
3023
+ <polyline points="6 9 12 15 18 9" />
3024
+ </svg>
3025
+ </button>
3026
+ }
3027
+
3028
+ <div class="flex-1"></div>
3029
+
3030
+ @if (!collapsed()) {
3031
+ <button class="btn btn-ghost btn-xs" (click)="onAddRule()" type="button">+ 条件</button>
3032
+
3033
+ @if (canAddGroup()) {
3034
+ <button class="btn btn-ghost btn-xs" (click)="onAddGroup()" type="button">+ 分组</button>
3035
+ }
3036
+
3037
+ @if (depth() > 0) {
3038
+ <button
3039
+ class="btn btn-ghost btn-xs btn-square text-base-content/40 hover:text-error"
3040
+ (click)="onRemove()"
3041
+ title="删除组"
3042
+ type="button"
3043
+ >
3044
+ ✕
3045
+ </button>
3046
+ }
3047
+ }
3048
+ </div>
3049
+
3050
+ <!-- 规则列表 -->
3051
+ @if (!collapsed()) {
3052
+ <div class="space-y-1">
3053
+ @for (item of group().rules; track trackByItem(item); let idx = $index) {
3054
+ @if (isRuleGroup(item)) {
3055
+ <div
3056
+ class="rxdb-drag-item"
3057
+ [class.dragging]="isDragging(item.id)"
3058
+ [class.drop-invalid]="isDropInvalid(item.id)"
3059
+ [class.drop-target-after]="isDropTarget(item.id, 'after')"
3060
+ [class.drop-target-before]="isDropTarget(item.id, 'before')"
3061
+ [class.drop-target-into]="isDropTarget(item.id, 'into')"
3062
+ (dragend)="onItemDragEnd()"
3063
+ (dragleave)="onItemDragLeave($event)"
3064
+ (dragover)="onItemDragOver($event, item, idx, true)"
3065
+ (drop)="onItemDrop($event, item, idx, true)"
3066
+ >
3067
+ <div class="flex items-start gap-1">
3068
+ @if (dragDropHandler()) {
3069
+ <span
3070
+ class="btn btn-ghost btn-xs btn-square text-base-content/30 hover:text-base-content/60 mt-1 cursor-grab"
3071
+ (dragstart)="onItemDragStart($event, item)"
3072
+ draggable="true"
3073
+ >⠿</span
3074
+ >
3075
+ }
3076
+ <div class="min-w-0 flex-1">
3077
+ <rxdb-query-group
3078
+ [allowCollapse]="allowCollapse()"
3079
+ [depth]="depth() + 1"
3080
+ [dragDropHandler]="dragDropHandler()"
3081
+ [errors]="errors()"
3082
+ [fields]="fields()"
3083
+ [group]="asRuleGroup(item)"
3084
+ [maxDepth]="maxDepth()"
3085
+ (addGroup)="addGroup.emit($event)"
3086
+ (addRule)="addRule.emit($event)"
3087
+ (removeItem)="removeItem.emit($event)"
3088
+ (updateCombinator)="updateCombinator.emit($event)"
3089
+ (updateRule)="updateRule.emit($event)"
3090
+ />
3091
+ </div>
3092
+ </div>
3093
+ </div>
3094
+ } @else {
3095
+ <div
3096
+ class="rxdb-drag-item"
3097
+ [class.dragging]="isDragging(item.id)"
3098
+ [class.drop-invalid]="isDropInvalid(item.id)"
3099
+ [class.drop-target-after]="isDropTarget(item.id, 'after')"
3100
+ [class.drop-target-before]="isDropTarget(item.id, 'before')"
3101
+ (dragend)="onItemDragEnd()"
3102
+ (dragleave)="onItemDragLeave($event)"
3103
+ (dragover)="onItemDragOver($event, item, idx, false)"
3104
+ (drop)="onItemDrop($event, item, idx, false)"
3105
+ >
3106
+ <div class="flex items-center gap-1">
3107
+ @if (dragDropHandler()) {
3108
+ <span
3109
+ class="btn btn-ghost btn-xs btn-square text-base-content/30 hover:text-base-content/60 cursor-grab"
3110
+ (dragstart)="onItemDragStart($event, item)"
3111
+ draggable="true"
3112
+ >⠿</span
3113
+ >
3114
+ }
3115
+ <div class="min-w-0 flex-1">
3116
+ <rxdb-query-rule
3117
+ [errors]="errors()"
3118
+ [fields]="fields()"
3119
+ [rule]="asRule(item)"
3120
+ (remove)="onRuleRemove(asRule(item).id)"
3121
+ (update)="onRuleUpdate($event)"
3122
+ />
3123
+ </div>
3124
+ </div>
3125
+ </div>
3126
+ }
3127
+ }
3128
+ </div>
3129
+
3130
+ <!-- 空状态 -->
3131
+ @if (group().rules.length === 0) {
3132
+ <div class="text-base-content/50 py-2 text-center text-sm select-none">暂无条件</div>
3133
+ }
3134
+ }
3135
+ </div>
3136
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".rxdb-drag-item{position:relative}.rxdb-drag-item.dragging{opacity:.4}.rxdb-drag-item.drop-target-before:before{content:\"\";position:absolute;top:-2px;left:0;right:0;height:3px;background:#0083e0;background:oklch(.6 .2 250);border-radius:2px;z-index:1}.rxdb-drag-item.drop-target-after:after{content:\"\";position:absolute;bottom:-2px;left:0;right:0;height:3px;background:#0083e0;background:oklch(.6 .2 250);border-radius:2px;z-index:1}.rxdb-drag-item.drop-target-into{outline:2px dashed oklch(.6 .2 250);outline-offset:-2px;border-radius:.5rem;background:#0083e01a;background:oklch(.6 .2 250 / 10%)}.rxdb-drag-item.drop-invalid{outline:2px dashed oklch(.6 .2 30);outline-offset:-2px;border-radius:.5rem}\n"] }]
3137
+ }], propDecorators: { group: [{ type: i0.Input, args: [{ isSignal: true, alias: "group", required: true }] }], fields: [{ type: i0.Input, args: [{ isSignal: true, alias: "fields", required: true }] }], errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], depth: [{ type: i0.Input, args: [{ isSignal: true, alias: "depth", required: false }] }], maxDepth: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDepth", required: false }] }], dragDropHandler: [{ type: i0.Input, args: [{ isSignal: true, alias: "dragDropHandler", required: false }] }], allowCollapse: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowCollapse", required: false }] }], addRule: [{ type: i0.Output, args: ["addRule"] }], addGroup: [{ type: i0.Output, args: ["addGroup"] }], removeItem: [{ type: i0.Output, args: ["removeItem"] }], updateRule: [{ type: i0.Output, args: ["updateRule"] }], updateCombinator: [{ type: i0.Output, args: ["updateCombinator"] }] } });
3138
+
3139
+ /**
3140
+ * Angular Query Builder 主组件
3141
+ *
3142
+ * @description
3143
+ * 提供可视化的查询条件构建界面,支持:
3144
+ * - 传入 SchemaInfo 或 FieldMetadata[] 定义可用字段
3145
+ * - 拖拽重排规则和分组
3146
+ * - 嵌套分组(最大 5 层)
3147
+ * - 实时验证
3148
+ *
3149
+ * @example
3150
+ * ```html
3151
+ * <rxdb-query-builder
3152
+ * [schema]="schema"
3153
+ * (queryChange)="onQueryChange($event)"
3154
+ * />
3155
+ * ```
3156
+ */
3157
+ class QueryBuilderComponent {
3158
+ service;
3159
+ /** 上次应用的 initialQuery 引用,用于检测是否有新查询需要加载 */
3160
+ lastAppliedInitialQuery = undefined;
3161
+ scrollContainer = viewChild('scrollContainer', ...(ngDevMode ? [{ debugName: "scrollContainer" }] : /* istanbul ignore next */ []));
3162
+ /** 根规则组 Signal */
3163
+ rootGroupSignal;
3164
+ /** 验证 Signal */
3165
+ validationSignal;
3166
+ /**
3167
+ * Schema 信息,包含字段定义
3168
+ */
3169
+ schema = input(...(ngDevMode ? [undefined, { debugName: "schema" }] : /* istanbul ignore next */ []));
3170
+ /**
3171
+ * 字段列表(直接传入,优先于 schema)
3172
+ */
3173
+ fields = input([], ...(ngDevMode ? [{ debugName: "fields" }] : /* istanbul ignore next */ []));
3174
+ /**
3175
+ * 初始查询条件(可选)
3176
+ */
3177
+ initialQuery = input(...(ngDevMode ? [undefined, { debugName: "initialQuery" }] : /* istanbul ignore next */ []));
3178
+ /**
3179
+ * 最大嵌套层级(默认 5)
3180
+ */
3181
+ maxDepth = input(5, ...(ngDevMode ? [{ debugName: "maxDepth" }] : /* istanbul ignore next */ []));
3182
+ /**
3183
+ * 内容区域最大高度(超出后滚动),支持任意 CSS 长度值,如 `'400px'`、`'50vh'`
3184
+ */
3185
+ height = input('80vh', ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
3186
+ /**
3187
+ * 是否开启拖拽排序功能(默认开启)
3188
+ */
3189
+ enableDrag = input(true, ...(ngDevMode ? [{ debugName: "enableDrag" }] : /* istanbul ignore next */ []));
3190
+ /**
3191
+ * 是否开启分组展开/收起功能(默认开启)
3192
+ */
3193
+ enableCollapse = input(true, ...(ngDevMode ? [{ debugName: "enableCollapse" }] : /* istanbul ignore next */ []));
3194
+ dragDropHandler;
3195
+ /**
3196
+ * 查询条件变更事件
3197
+ */
3198
+ queryChange = output();
3199
+ /**
3200
+ * 验证状态变更事件
3201
+ */
3202
+ validationChange = output();
3203
+ /** 计算后的字段列表 */
3204
+ fieldsComputed = computed(() => {
3205
+ // 优先使用直接传入的 fields
3206
+ const directFields = this.fields();
3207
+ if (directFields && directFields.length > 0) {
3208
+ return directFields;
3209
+ }
3210
+ // 否则从 schema 获取
3211
+ const schemaInfo = this.schema();
3212
+ return schemaInfo?.fields ?? [];
3213
+ }, ...(ngDevMode ? [{ debugName: "fieldsComputed" }] : /* istanbul ignore next */ []));
3214
+ /** 根规则组 */
3215
+ rootGroup = computed(() => {
3216
+ return this.rootGroupSignal();
3217
+ }, ...(ngDevMode ? [{ debugName: "rootGroup" }] : /* istanbul ignore next */ []));
3218
+ /** 验证错误列表 */
3219
+ validationErrors = computed(() => {
3220
+ const validation = this.validationSignal();
3221
+ return validation?.errors ?? [];
3222
+ }, ...(ngDevMode ? [{ debugName: "validationErrors" }] : /* istanbul ignore next */ []));
3223
+ /** 是否有规则 */
3224
+ hasRules = computed(() => {
3225
+ const group = this.rootGroup();
3226
+ return group && group.rules.length > 0;
3227
+ }, ...(ngDevMode ? [{ debugName: "hasRules" }] : /* istanbul ignore next */ []));
3228
+ constructor() {
3229
+ // 在构造函数中初始化服务(非 reactive context)
3230
+ this.service = createQueryBuilderService({
3231
+ config: {
3232
+ maxNestingLevel: this.maxDepth()
3233
+ }
3234
+ });
3235
+ this.dragDropHandler = new QueryDragDropHandler((itemId, targetGroupId, targetIndex) => this.service.moveItem(itemId, targetGroupId, targetIndex));
3236
+ // 转换 Observable 为 Signal(必须在 reactive context 外部)
3237
+ this.rootGroupSignal = toSignal(this.service.rootGroup$, { requireSync: true });
3238
+ this.validationSignal = toSignal(this.service.validation$, { initialValue: undefined });
3239
+ // 监听字段变化并同步到服务
3240
+ effect(() => {
3241
+ const fields = this.fieldsComputed();
3242
+ if (fields.length > 0) {
3243
+ this.service.setFields(fields);
3244
+ }
3245
+ });
3246
+ // 监听初始查询并加载(引用变化时重新加载,用户修改不影响此 signal 故不会覆盖)
3247
+ effect(() => {
3248
+ const initial = this.initialQuery();
3249
+ if (initial !== undefined && initial !== this.lastAppliedInitialQuery) {
3250
+ this.lastAppliedInitialQuery = initial;
3251
+ this.service.fromRxDBQuery(initial);
3252
+ }
3253
+ });
3254
+ // 监听查询变化并发出事件
3255
+ effect(() => {
3256
+ const group = this.rootGroup();
3257
+ if (group) {
3258
+ const query = this.service.toRxDBQuery();
3259
+ this.queryChange.emit(query);
3260
+ }
3261
+ });
3262
+ // 监听验证变化并发出事件
3263
+ effect(() => {
3264
+ const validation = this.validationSignal();
3265
+ if (validation) {
3266
+ this.validationChange.emit(validation);
3267
+ }
3268
+ });
3269
+ }
3270
+ /** 销毁服务 */
3271
+ ngOnDestroy() {
3272
+ this.service.destroy();
3273
+ }
3274
+ /** 添加第一个规则 */
3275
+ addFirstRule() {
3276
+ const firstField = this.fieldsComputed()[0];
3277
+ if (firstField) {
3278
+ this.service.addRule(undefined, {
3279
+ field: firstField.name,
3280
+ operator: firstField.isRelation ? 'exists'
3281
+ : firstField.type === 'keyValue' ? 'null'
3282
+ : '=',
3283
+ value: firstField.type === 'boolean' ? false : ''
3284
+ });
3285
+ }
3286
+ }
3287
+ /** 清空所有规则 */
3288
+ clearAll() {
3289
+ this.service.clear();
3290
+ }
3291
+ /** 添加规则到指定组 */
3292
+ onAddRule(event) {
3293
+ this.service.addRule(event.parentId, event.rule);
3294
+ this.scrollToBottom();
3295
+ }
3296
+ /** 添加子组到指定组 */
3297
+ onAddGroup(parentId) {
3298
+ this.service.addGroup(parentId);
3299
+ this.scrollToBottom();
3300
+ }
3301
+ /** 删除规则或组 */
3302
+ onRemoveItem(id) {
3303
+ this.service.remove(id);
3304
+ }
3305
+ /** 更新规则 */
3306
+ onUpdateRule(event) {
3307
+ this.service.updateRule(event.id, event.updates);
3308
+ }
3309
+ /** 更新组合器 */
3310
+ onUpdateCombinator(event) {
3311
+ this.service.updateGroupCombinator(event.id, event.combinator);
3312
+ }
3313
+ /** 键盘快捷键处理 */
3314
+ onKeyDown(event) {
3315
+ if (event.key === 'Escape' && this.hasRules()) {
3316
+ event.preventDefault();
3317
+ this.clearAll();
3318
+ return;
3319
+ }
3320
+ }
3321
+ scrollToBottom() {
3322
+ setTimeout(() => {
3323
+ const el = this.scrollContainer()?.nativeElement;
3324
+ el?.scrollTo({ top: el.scrollHeight, behavior: 'smooth' });
3325
+ }, 0);
3326
+ }
3327
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: QueryBuilderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3328
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: QueryBuilderComponent, isStandalone: true, selector: "rxdb-query-builder", inputs: { schema: { classPropertyName: "schema", publicName: "schema", isSignal: true, isRequired: false, transformFunction: null }, fields: { classPropertyName: "fields", publicName: "fields", isSignal: true, isRequired: false, transformFunction: null }, initialQuery: { classPropertyName: "initialQuery", publicName: "initialQuery", isSignal: true, isRequired: false, transformFunction: null }, maxDepth: { classPropertyName: "maxDepth", publicName: "maxDepth", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, enableDrag: { classPropertyName: "enableDrag", publicName: "enableDrag", isSignal: true, isRequired: false, transformFunction: null }, enableCollapse: { classPropertyName: "enableCollapse", publicName: "enableCollapse", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { queryChange: "queryChange", validationChange: "validationChange" }, viewQueries: [{ propertyName: "scrollContainer", first: true, predicate: ["scrollContainer"], descendants: true, isSignal: true }], ngImport: i0, template: `
3329
+ <div
3330
+ class="rxdb-query-builder bg-base-100"
3331
+ (keydown)="onKeyDown($event)"
3332
+ aria-label="查询条件构建器"
3333
+ role="search"
3334
+ tabindex="0"
3335
+ >
3336
+ <div class="overflow-y-auto" #scrollContainer [style.max-height]="height()">
3337
+ <!-- 标题栏 -->
3338
+ <div class="mb-4 flex items-center justify-between">
3339
+ <h3 class="text-base" id="query-builder-title">查询条件</h3>
3340
+ <div class="flex gap-2" aria-label="查询操作" role="toolbar">
3341
+ @if (hasRules()) {
3342
+ <button
3343
+ class="btn btn-ghost btn-sm"
3344
+ (click)="clearAll()"
3345
+ (keydown.enter)="clearAll()"
3346
+ aria-label="清空所有查询条件"
3347
+ title="清空所有查询条件 (Escape)"
3348
+ type="button"
3349
+ >
3350
+ 清空
3351
+ </button>
3352
+ }
3353
+ </div>
3354
+ </div>
3355
+
3356
+ <!-- 空状态提示 -->
3357
+ @if (!hasRules()) {
3358
+ <div class="text-base-content/60 py-8 text-center" aria-live="polite" role="status">
3359
+ <div class="mb-2 flex justify-center" aria-hidden="true">
3360
+ <svg
3361
+ class="size-12 opacity-40"
3362
+ fill="none"
3363
+ stroke="currentColor"
3364
+ stroke-linecap="round"
3365
+ stroke-linejoin="round"
3366
+ stroke-width="2"
3367
+ viewBox="0 0 24 24"
3368
+ xmlns="http://www.w3.org/2000/svg"
3369
+ >
3370
+ <circle cx="11" cy="11" r="8" />
3371
+ <path d="m21 21-4.3-4.3" />
3372
+ </svg>
3373
+ </div>
3374
+ <p class="mb-4 min-w-md">还没有任何查询条件</p>
3375
+ <button class="btn btn-sm" (click)="addFirstRule()" aria-label="添加第一个条件" type="button">
3376
+ 添加第一个条件
3377
+ </button>
3378
+ </div>
3379
+ } @else {
3380
+ <!-- 根规则组 -->
3381
+ <rxdb-query-group
3382
+ [allowCollapse]="enableCollapse()"
3383
+ [depth]="0"
3384
+ [dragDropHandler]="enableDrag() ? dragDropHandler : undefined"
3385
+ [errors]="validationErrors()"
3386
+ [fields]="fieldsComputed()"
3387
+ [group]="rootGroup()!"
3388
+ [maxDepth]="maxDepth()"
3389
+ (addGroup)="onAddGroup($event)"
3390
+ (addRule)="onAddRule($event)"
3391
+ (removeItem)="onRemoveItem($event)"
3392
+ (updateCombinator)="onUpdateCombinator($event)"
3393
+ (updateRule)="onUpdateRule($event)"
3394
+ role="region"
3395
+ />
3396
+ }
3397
+ </div>
3398
+ </div>
3399
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: QueryGroupComponent, selector: "rxdb-query-group", inputs: ["group", "fields", "errors", "depth", "maxDepth", "dragDropHandler", "allowCollapse"], outputs: ["addRule", "addGroup", "removeItem", "updateRule", "updateCombinator"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3400
+ }
3401
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: QueryBuilderComponent, decorators: [{
3402
+ type: Component,
3403
+ args: [{
3404
+ selector: 'rxdb-query-builder',
3405
+ standalone: true,
3406
+ imports: [CommonModule, QueryGroupComponent],
3407
+ template: `
3408
+ <div
3409
+ class="rxdb-query-builder bg-base-100"
3410
+ (keydown)="onKeyDown($event)"
3411
+ aria-label="查询条件构建器"
3412
+ role="search"
3413
+ tabindex="0"
3414
+ >
3415
+ <div class="overflow-y-auto" #scrollContainer [style.max-height]="height()">
3416
+ <!-- 标题栏 -->
3417
+ <div class="mb-4 flex items-center justify-between">
3418
+ <h3 class="text-base" id="query-builder-title">查询条件</h3>
3419
+ <div class="flex gap-2" aria-label="查询操作" role="toolbar">
3420
+ @if (hasRules()) {
3421
+ <button
3422
+ class="btn btn-ghost btn-sm"
3423
+ (click)="clearAll()"
3424
+ (keydown.enter)="clearAll()"
3425
+ aria-label="清空所有查询条件"
3426
+ title="清空所有查询条件 (Escape)"
3427
+ type="button"
3428
+ >
3429
+ 清空
3430
+ </button>
3431
+ }
3432
+ </div>
3433
+ </div>
3434
+
3435
+ <!-- 空状态提示 -->
3436
+ @if (!hasRules()) {
3437
+ <div class="text-base-content/60 py-8 text-center" aria-live="polite" role="status">
3438
+ <div class="mb-2 flex justify-center" aria-hidden="true">
3439
+ <svg
3440
+ class="size-12 opacity-40"
3441
+ fill="none"
3442
+ stroke="currentColor"
3443
+ stroke-linecap="round"
3444
+ stroke-linejoin="round"
3445
+ stroke-width="2"
3446
+ viewBox="0 0 24 24"
3447
+ xmlns="http://www.w3.org/2000/svg"
3448
+ >
3449
+ <circle cx="11" cy="11" r="8" />
3450
+ <path d="m21 21-4.3-4.3" />
3451
+ </svg>
3452
+ </div>
3453
+ <p class="mb-4 min-w-md">还没有任何查询条件</p>
3454
+ <button class="btn btn-sm" (click)="addFirstRule()" aria-label="添加第一个条件" type="button">
3455
+ 添加第一个条件
3456
+ </button>
3457
+ </div>
3458
+ } @else {
3459
+ <!-- 根规则组 -->
3460
+ <rxdb-query-group
3461
+ [allowCollapse]="enableCollapse()"
3462
+ [depth]="0"
3463
+ [dragDropHandler]="enableDrag() ? dragDropHandler : undefined"
3464
+ [errors]="validationErrors()"
3465
+ [fields]="fieldsComputed()"
3466
+ [group]="rootGroup()!"
3467
+ [maxDepth]="maxDepth()"
3468
+ (addGroup)="onAddGroup($event)"
3469
+ (addRule)="onAddRule($event)"
3470
+ (removeItem)="onRemoveItem($event)"
3471
+ (updateCombinator)="onUpdateCombinator($event)"
3472
+ (updateRule)="onUpdateRule($event)"
3473
+ role="region"
3474
+ />
3475
+ }
3476
+ </div>
3477
+ </div>
3478
+ `,
3479
+ changeDetection: ChangeDetectionStrategy.OnPush
3480
+ }]
3481
+ }], ctorParameters: () => [], propDecorators: { scrollContainer: [{ type: i0.ViewChild, args: ['scrollContainer', { isSignal: true }] }], schema: [{ type: i0.Input, args: [{ isSignal: true, alias: "schema", required: false }] }], fields: [{ type: i0.Input, args: [{ isSignal: true, alias: "fields", required: false }] }], initialQuery: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialQuery", required: false }] }], maxDepth: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDepth", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], enableDrag: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableDrag", required: false }] }], enableCollapse: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableCollapse", required: false }] }], queryChange: [{ type: i0.Output, args: ["queryChange"] }], validationChange: [{ type: i0.Output, args: ["validationChange"] }] } });
3482
+
3483
+ const EMPTY_FILTER = { combinator: 'and', rules: [] };
3484
+ /**
3485
+ * 实体列表组件
3486
+ *
3487
+ * 封装了实体列表的完整 UI:工具栏(显示名称、筛选、撤销/重做、新增)+ 数据表格。
3488
+ * 通过 `namespace` 和 `name` 输入定位 RxDB 实体,实现懒加载分页和 CRUD 操作。
3489
+ *
3490
+ * @example
3491
+ * ```html
3492
+ * <rxdb-entity-list [namespace]="namespace()" [name]="name()" />
3493
+ * ```
3494
+ */
3495
+ class EntityListComponent {
3496
+ #m2mDialogRef = null;
3497
+ // ── Service injections ────────────────────────────────────────────────
3498
+ #rxdb = inject(RxDB);
3499
+ #injector = inject(Injector);
3500
+ #dialog = inject(Dialog);
3501
+ #errorHandler = inject(ErrorHandler);
3502
+ // ── Save coalescing ───────────────────────────────────────────────────
3503
+ #pendingChanges = new Map();
3504
+ #flushHandle = false;
3505
+ // ── Entity registry ───────────────────────────────────────────────────
3506
+ #entityClsMap = new Map(this.#rxdb.config.entities.map(cls => {
3507
+ const meta = getEntityMetadata(cls);
3508
+ return [`${meta.namespace}:${meta.name}`, cls];
3509
+ }));
3510
+ #modelInfoMap = new Map(this.#rxdb.config.entities.map(cls => {
3511
+ const meta = getEntityMetadata(cls);
3512
+ return [
3513
+ meta.name,
3514
+ {
3515
+ name: meta.name,
3516
+ displayName: meta.displayName ?? meta.name,
3517
+ entityClass: cls,
3518
+ metadata: meta
3519
+ }
3520
+ ];
3521
+ }));
3522
+ #scrollListMap = new Map();
3523
+ #columnsCache = new Map(this.#rxdb.config.entities.map(cls => {
3524
+ const meta = getEntityMetadata(cls);
3525
+ return [
3526
+ `${meta.namespace}:${meta.name}`,
3527
+ buildEditableColumns(meta, {
3528
+ relatedItemsProviderFactory: (fkField, relatedEntityName) => {
3529
+ const relCls = [...this.#entityClsMap.values()].find(c => {
3530
+ const m = getEntityMetadata(c);
3531
+ return m.name === relatedEntityName;
3532
+ });
3533
+ if (!relCls)
3534
+ return undefined;
3535
+ const relKey = (() => {
3536
+ const m = getEntityMetadata(relCls);
3537
+ return `${m.namespace}:${m.name}`;
3538
+ })();
3539
+ const list = this.#getOrCreateList(relKey, relCls);
3540
+ const strVal = (v) => (typeof v === 'string' && v.length > 0 ? v : undefined);
3541
+ return () => {
3542
+ return list.value().map(inst => ({
3543
+ id: String(inst['id'] ?? ''),
3544
+ displayName: strVal(inst['displayName']) ?? strVal(inst['name']) ?? String(inst['id'] ?? '')
3545
+ }));
3546
+ };
3547
+ }
3548
+ })
3549
+ ];
3550
+ }));
3551
+ #vHistory = this.#rxdb.versionManager.history();
3552
+ #entityKey = computed(() => `${this.namespace()}:${this.name()}`, ...(ngDevMode ? [{ debugName: "#entityKey" }] : /* istanbul ignore next */ []));
3553
+ #currentList = signal(undefined, ...(ngDevMode ? [{ debugName: "#currentList" }] : /* istanbul ignore next */ []));
3554
+ #instances = computed(() => {
3555
+ const list = this.#currentList();
3556
+ return list ? list.value() : [];
3557
+ }, ...(ngDevMode ? [{ debugName: "#instances" }] : /* istanbul ignore next */ []));
3558
+ #entityCls = computed(() => this.#entityClsMap.get(this.#entityKey()), ...(ngDevMode ? [{ debugName: "#entityCls" }] : /* istanbul ignore next */ []));
3559
+ /** 级联新增模式下本地草稿子实体(未保存到DB,由父实体级联保存) */
3560
+ #localDraftItems = signal([], ...(ngDevMode ? [{ debugName: "#localDraftItems" }] : /* istanbul ignore next */ []));
3561
+ // ── Protected view bindings ───────────────────────────────────────────
3562
+ Undo2 = Undo2;
3563
+ Redo2 = Redo2;
3564
+ Funnel = Funnel;
3565
+ /** CDK Overlay 定位策略:优先下方右对齐,其次上方右对齐 */
3566
+ overlayPositions = [
3567
+ {
3568
+ originX: 'end',
3569
+ originY: 'bottom',
3570
+ overlayX: 'end',
3571
+ overlayY: 'top',
3572
+ offsetY: 4
3573
+ },
3574
+ {
3575
+ originX: 'end',
3576
+ originY: 'top',
3577
+ overlayX: 'end',
3578
+ overlayY: 'bottom',
3579
+ offsetY: -4
3580
+ }
3581
+ ];
3582
+ loadMore = computed(() => {
3583
+ const list = this.#currentList();
3584
+ return list ? () => list.loadMore() : undefined;
3585
+ }, ...(ngDevMode ? [{ debugName: "loadMore" }] : /* istanbul ignore next */ []));
3586
+ // ── Route inputs ──────────────────────────────────────────────────────
3587
+ namespace = input.required(...(ngDevMode ? [{ debugName: "namespace" }] : /* istanbul ignore next */ []));
3588
+ name = input.required(...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
3589
+ /** 固定查询条件,与用户筛选条件做 AND 合并(用于关系属性过滤,如 ownerId = currentId) */
3590
+ fixedQuery = input(undefined, ...(ngDevMode ? [{ debugName: "fixedQuery" }] : /* istanbul ignore next */ []));
3591
+ /** 预填充的外键数据(级联新增时传递给子实体对话框) */
3592
+ fixedFormData = input(undefined, ...(ngDevMode ? [{ debugName: "fixedFormData" }] : /* istanbul ignore next */ []));
3593
+ /** 草稿父实体(用于级联新增场景,子实体通过关系API注册到父实体) */
3594
+ draftParentEntity = input(null, ...(ngDevMode ? [{ debugName: "draftParentEntity" }] : /* istanbul ignore next */ []));
3595
+ /** 父实体上关系属性名(如 'children'),用于调用 parent.children$.add(child) */
3596
+ parentRelationName = input(undefined, ...(ngDevMode ? [{ debugName: "parentRelationName" }] : /* istanbul ignore next */ []));
3597
+ /** 创建链路中的实体类型(namespace:name),用于阻断循环创建 */
3598
+ creationChain = input([], ...(ngDevMode ? [{ debugName: "creationChain" }] : /* istanbul ignore next */ []));
3599
+ /** 关系类型(MANY_TO_MANY 时启用选择模式) */
3600
+ relationKind = input(undefined, ...(ngDevMode ? [{ debugName: "relationKind" }] : /* istanbul ignore next */ []));
3601
+ /** 父实体实例(用于 M2M 关系操作,包含 draft 和已保存实体) */
3602
+ parentEntity = input(null, ...(ngDevMode ? [{ debugName: "parentEntity" }] : /* istanbul ignore next */ []));
3603
+ /** 列表模式:default=普通列表, select=选择模式(带 checkbox 列) */
3604
+ mode = input('default', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
3605
+ /** 已关联的实体 ID 集合(选择模式下过滤掉,不再展示) */
3606
+ alreadyLinkedIds = input(new Set(), ...(ngDevMode ? [{ debugName: "alreadyLinkedIds" }] : /* istanbul ignore next */ []));
3607
+ /** 点击「查看」按钮时触发,携带对应行记录 */
3608
+ viewEntity = output();
3609
+ /** 选择模式:确认选择时触发,携带选中的实体实例 */
3610
+ selectionConfirmed = output();
3611
+ /** 选择模式:取消选择时触发 */
3612
+ selectionCancelled = output();
3613
+ // ── Reactive state ────────────────────────────────────────────────────
3614
+ filterQuery = linkedSignal(() => {
3615
+ this.namespace();
3616
+ this.name();
3617
+ return { ...EMPTY_FILTER };
3618
+ }, ...(ngDevMode ? [{ debugName: "filterQuery" }] : /* istanbul ignore next */ []));
3619
+ $isFilterQuery = computed(() => this.filterQuery().rules.length > 0, ...(ngDevMode ? [{ debugName: "$isFilterQuery" }] : /* istanbul ignore next */ []));
3620
+ showFilterPopover = signal(false, ...(ngDevMode ? [{ debugName: "showFilterPopover" }] : /* istanbul ignore next */ []));
3621
+ pendingQuery = linkedSignal(() => {
3622
+ this.namespace();
3623
+ this.name();
3624
+ return { ...EMPTY_FILTER };
3625
+ }, ...(ngDevMode ? [{ debugName: "pendingQuery" }] : /* istanbul ignore next */ []));
3626
+ pendingQueryValid = signal(true, ...(ngDevMode ? [{ debugName: "pendingQueryValid" }] : /* istanbul ignore next */ []));
3627
+ isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
3628
+ displayName = computed(() => {
3629
+ const cls = this.#entityCls();
3630
+ if (!cls)
3631
+ return this.name();
3632
+ const meta = getEntityMetadata(cls);
3633
+ return meta.displayName ?? meta.name;
3634
+ }, ...(ngDevMode ? [{ debugName: "displayName" }] : /* istanbul ignore next */ []));
3635
+ /** 当前实体是否存在于祖先创建链路中(循环创建检测) */
3636
+ isCreateBlocked = computed(() => this.creationChain().includes(this.#entityKey()), ...(ngDevMode ? [{ debugName: "isCreateBlocked" }] : /* istanbul ignore next */ []));
3637
+ isQueryActive = computed(() => this.filterQuery().rules.length > 0, ...(ngDevMode ? [{ debugName: "isQueryActive" }] : /* istanbul ignore next */ []));
3638
+ filteredCount = computed(() => this.#instances().length + this.#localDraftItems().length, ...(ngDevMode ? [{ debugName: "filteredCount" }] : /* istanbul ignore next */ []));
3639
+ isInitialLoading = computed(() => {
3640
+ const list = this.#currentList();
3641
+ return list ? list.isLoading() && list.value().length === 0 : false;
3642
+ }, ...(ngDevMode ? [{ debugName: "isInitialLoading" }] : /* istanbul ignore next */ []));
3643
+ isLoadingMore = computed(() => {
3644
+ const list = this.#currentList();
3645
+ return list ? list.isLoading() && list.value().length > 0 : false;
3646
+ }, ...(ngDevMode ? [{ debugName: "isLoadingMore" }] : /* istanbul ignore next */ []));
3647
+ undoCount = toSignal(this.#vHistory.undoCount$, { initialValue: 0 });
3648
+ redoCount = toSignal(this.#vHistory.redoCount$, { initialValue: 0 });
3649
+ canUndo = computed(() => this.undoCount() > 0, ...(ngDevMode ? [{ debugName: "canUndo" }] : /* istanbul ignore next */ []));
3650
+ canRedo = computed(() => this.redoCount() > 0, ...(ngDevMode ? [{ debugName: "canRedo" }] : /* istanbul ignore next */ []));
3651
+ queryBuilderFields = computed(() => {
3652
+ const cls = this.#entityCls();
3653
+ if (!cls)
3654
+ return [];
3655
+ return organizeFields(extractFieldsFromMetadata(getEntityMetadata(cls), this.#modelInfoMap));
3656
+ }, ...(ngDevMode ? [{ debugName: "queryBuilderFields" }] : /* istanbul ignore next */ []));
3657
+ tableRecords = computed(() => {
3658
+ const dbRecords = this.#instances().map(inst => ({ ...inst }));
3659
+ const draftRecords = this.#localDraftItems().map(inst => ({ ...inst }));
3660
+ let records = [...draftRecords, ...dbRecords];
3661
+ if (this.isSelectMode()) {
3662
+ const linkedIds = this.alreadyLinkedIds();
3663
+ const selIds = this.selectedIds();
3664
+ records = records
3665
+ .filter(r => !linkedIds.has(r['id']))
3666
+ .map(r => ({ ...r, __selected: selIds.has(r['id']) }));
3667
+ }
3668
+ return records;
3669
+ }, ...(ngDevMode ? [{ debugName: "tableRecords" }] : /* istanbul ignore next */ []));
3670
+ tableColumns = computed(() => {
3671
+ const base = this.#columnsCache.get(this.#entityKey()) ?? [actionsColumn('操作', '删除', '查看')];
3672
+ if (this.isSelectMode()) {
3673
+ const withoutActions = base.filter(c => c['field'] !== 'actions');
3674
+ return [{ field: '__selected', title: '', cellType: 'checkbox', width: 50 }, ...withoutActions];
3675
+ }
3676
+ return base;
3677
+ }, ...(ngDevMode ? [{ debugName: "tableColumns" }] : /* istanbul ignore next */ []));
3678
+ createFormFieldConfigs = computed(() => {
3679
+ const cls = this.#entityCls();
3680
+ if (!cls)
3681
+ return [];
3682
+ const meta = getEntityMetadata(cls);
3683
+ return buildFormFields(meta, 'create');
3684
+ }, ...(ngDevMode ? [{ debugName: "createFormFieldConfigs" }] : /* istanbul ignore next */ []));
3685
+ // ── Select mode ───────────────────────────────────────────────────────
3686
+ isSelectMode = computed(() => this.mode() === 'select', ...(ngDevMode ? [{ debugName: "isSelectMode" }] : /* istanbul ignore next */ []));
3687
+ isM2m = computed(() => this.relationKind() === RelationKind.MANY_TO_MANY, ...(ngDevMode ? [{ debugName: "isM2m" }] : /* istanbul ignore next */ []));
3688
+ m2mSelectTpl = viewChild('m2mSelectTpl', ...(ngDevMode ? [{ debugName: "m2mSelectTpl" }] : /* istanbul ignore next */ []));
3689
+ selectedIds = signal(new Set(), ...(ngDevMode ? [{ debugName: "selectedIds" }] : /* istanbul ignore next */ []));
3690
+ selectedCount = computed(() => this.selectedIds().size, ...(ngDevMode ? [{ debugName: "selectedCount" }] : /* istanbul ignore next */ []));
3691
+ m2mAlreadyLinkedIds = computed(() => {
3692
+ const dbIds = this.#instances().map(e => e.id);
3693
+ const draftIds = this.#localDraftItems().map(e => e.id);
3694
+ return new Set([...dbIds, ...draftIds]);
3695
+ }, ...(ngDevMode ? [{ debugName: "m2mAlreadyLinkedIds" }] : /* istanbul ignore next */ []));
3696
+ // ── Constructor ───────────────────────────────────────────────────────
3697
+ constructor() {
3698
+ toObservable(this.#entityKey)
3699
+ .pipe(takeUntilDestroyed())
3700
+ .subscribe(key => {
3701
+ const cls = this.#entityClsMap.get(key);
3702
+ if (!cls) {
3703
+ this.#currentList.set(undefined);
3704
+ return;
3705
+ }
3706
+ this.#currentList.set(this.#getOrCreateList(key, cls));
3707
+ });
3708
+ }
3709
+ // ── Event handlers ────────────────────────────────────────────────────
3710
+ toggleFilterPopover() {
3711
+ this.showFilterPopover.update(v => !v);
3712
+ }
3713
+ applyFilter() {
3714
+ this.filterQuery.set({ ...this.pendingQuery() });
3715
+ this.showFilterPopover.set(false);
3716
+ }
3717
+ resetFilter() {
3718
+ this.pendingQuery.set({ ...EMPTY_FILTER });
3719
+ this.filterQuery.set({ ...EMPTY_FILTER });
3720
+ }
3721
+ onKeydown(e) {
3722
+ const tag = e.target?.tagName;
3723
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT')
3724
+ return;
3725
+ if ((e.ctrlKey || e.metaKey) && e.key === 'z') {
3726
+ if (e.shiftKey) {
3727
+ if (this.canRedo()) {
3728
+ e.preventDefault();
3729
+ void this.redo();
3730
+ }
3731
+ }
3732
+ else {
3733
+ if (this.canUndo()) {
3734
+ e.preventDefault();
3735
+ void this.undo();
3736
+ }
3737
+ }
3738
+ }
3739
+ }
3740
+ onQueryChange(query) {
3741
+ this.pendingQuery.set(query ?? { ...EMPTY_FILTER });
3742
+ }
3743
+ onValidationChange(result) {
3744
+ this.pendingQueryValid.set(result.valid);
3745
+ }
3746
+ onCellChanged(event) {
3747
+ if (!event.field || event.field === 'actions')
3748
+ return;
3749
+ if (event.field === '__selected') {
3750
+ const id = event.record['id'];
3751
+ this.selectedIds.update(set => {
3752
+ const next = new Set(set);
3753
+ if (event.value)
3754
+ next.add(id);
3755
+ else
3756
+ next.delete(id);
3757
+ return next;
3758
+ });
3759
+ return;
3760
+ }
3761
+ const id = event.record['id'];
3762
+ const entityCls = this.#entityCls();
3763
+ let value = event.value;
3764
+ if (entityCls) {
3765
+ const prop = getEntityMetadata(entityCls).propertyMap.get(event.field);
3766
+ if (prop)
3767
+ value = parsePropertyColumnValue(prop.type, event.value);
3768
+ }
3769
+ this.#enqueue(id, { [event.field]: value });
3770
+ }
3771
+ undo() {
3772
+ this.#vHistory.undo();
3773
+ }
3774
+ redo() {
3775
+ this.#vHistory.redo();
3776
+ }
3777
+ async onIconClicked(event) {
3778
+ if (event.name === 'view-action') {
3779
+ this.viewEntity.emit(event.record);
3780
+ return;
3781
+ }
3782
+ if (event.name !== 'delete-action')
3783
+ return;
3784
+ const id = event.record['id'];
3785
+ const draftInst = this.#localDraftItems().find(i => i.id === id);
3786
+ if (draftInst) {
3787
+ this.#localDraftItems.update(items => items.filter(i => i.id !== id));
3788
+ return;
3789
+ }
3790
+ const inst = this.#instances().find(i => i.id === id);
3791
+ if (!inst)
3792
+ return;
3793
+ try {
3794
+ await inst.remove();
3795
+ }
3796
+ catch (e) {
3797
+ this.#errorHandler.handleError(e);
3798
+ }
3799
+ }
3800
+ async onRowDeleted(event) {
3801
+ await this.onIconClicked({ name: 'delete-action', record: event });
3802
+ }
3803
+ onBatchUpdated(mutations) {
3804
+ for (const { recordId, changes } of mutations) {
3805
+ const filtered = Object.fromEntries(Object.entries(changes).filter(([k]) => k && k !== 'actions'));
3806
+ if (Object.keys(filtered).length)
3807
+ this.#enqueue(recordId, filtered);
3808
+ }
3809
+ }
3810
+ openCreateDialog() {
3811
+ if (this.isCreateBlocked())
3812
+ return;
3813
+ const fields = this.createFormFieldConfigs();
3814
+ const cls = this.#entityCls();
3815
+ if (!cls || fields.length === 0)
3816
+ return;
3817
+ const meta = getEntityMetadata(cls);
3818
+ const strVal = (v) => (typeof v === 'string' && v.length > 0 ? v : undefined);
3819
+ const draftParent = this.draftParentEntity();
3820
+ const relatedEntityProvider = (entityName, namespace) => {
3821
+ const relCls = [...this.#entityClsMap.values()].find(c => {
3822
+ const m = getEntityMetadata(c);
3823
+ return m.name === entityName && (!namespace || m.namespace === namespace);
3824
+ });
3825
+ if (!relCls)
3826
+ return [];
3827
+ const relMeta = getEntityMetadata(relCls);
3828
+ const list = this.#getOrCreateList(`${relMeta.namespace}:${relMeta.name}`, relCls);
3829
+ const items = list.value().map(inst => ({
3830
+ id: String(inst['id'] ?? ''),
3831
+ displayName: strVal(inst['displayName']) ?? strVal(inst['name']) ?? String(inst['id'] ?? '')
3832
+ }));
3833
+ if (draftParent) {
3834
+ const parentMeta = getEntityMetadata(draftParent.constructor);
3835
+ if (parentMeta.name === entityName && (!namespace || parentMeta.namespace === namespace)) {
3836
+ const parentId = String(draftParent.id);
3837
+ if (!items.some(i => i.id === parentId)) {
3838
+ items.push({
3839
+ id: parentId,
3840
+ displayName: strVal(draftParent['displayName']) ?? strVal(draftParent['name']) ?? parentId
3841
+ });
3842
+ }
3843
+ }
3844
+ }
3845
+ return items;
3846
+ };
3847
+ void Promise.resolve().then(function () { return entityDetail; }).then(({ EntityDetailComponent }) => {
3848
+ const dialogRef = this.#dialog.open(EntityDetailComponent, {
3849
+ width: '720px',
3850
+ minWidth: '400px',
3851
+ height: '80vh',
3852
+ minHeight: '300px',
3853
+ panelClass: 'entity-detail-dialog',
3854
+ data: {
3855
+ metadata: meta,
3856
+ formFields: fields,
3857
+ formData: {},
3858
+ formMode: 'create',
3859
+ relatedEntityProvider,
3860
+ fixedFormData: this.fixedFormData(),
3861
+ delegateSave: !!this.draftParentEntity(),
3862
+ creationChain: this.creationChain()
3863
+ }
3864
+ });
3865
+ dialogRef.componentInstance.formSubmitted.subscribe((result) => {
3866
+ this.#handleCreateSubmit(result);
3867
+ dialogRef.close();
3868
+ });
3869
+ dialogRef.closed.subscribe(result => {
3870
+ if (result === 'saved')
3871
+ this.#currentList()?.refresh();
3872
+ });
3873
+ });
3874
+ }
3875
+ // ── M2M / Select mode methods ─────────────────────────────────────────
3876
+ openM2mSelectDialog() {
3877
+ if (this.#m2mDialogRef)
3878
+ return;
3879
+ const tpl = this.m2mSelectTpl();
3880
+ if (!tpl)
3881
+ return;
3882
+ const ref = this.#dialog.open(tpl, {
3883
+ width: '720px',
3884
+ minWidth: '400px',
3885
+ height: '70vh',
3886
+ minHeight: '300px',
3887
+ panelClass: 'entity-m2m-select-dialog'
3888
+ });
3889
+ this.#m2mDialogRef = ref;
3890
+ ref.closed.subscribe(() => {
3891
+ this.#m2mDialogRef = null;
3892
+ });
3893
+ }
3894
+ onM2mSelectionConfirmed(entities) {
3895
+ const parent = this.parentEntity();
3896
+ const relName = this.parentRelationName();
3897
+ if (parent && relName) {
3898
+ const relation = parent[relName + '$'];
3899
+ if (relation && typeof relation['add'] === 'function') {
3900
+ relation.add(...entities);
3901
+ }
3902
+ if (!this.draftParentEntity()) {
3903
+ parent
3904
+ .save()
3905
+ .then(() => this.#currentList()?.refresh())
3906
+ .catch(e => this.#errorHandler.handleError(e));
3907
+ }
3908
+ else {
3909
+ this.#localDraftItems.update(items => [...items, ...entities]);
3910
+ }
3911
+ }
3912
+ this.#m2mDialogRef?.close();
3913
+ }
3914
+ m2mCancelSelection() {
3915
+ this.#m2mDialogRef?.close();
3916
+ }
3917
+ confirmSelection() {
3918
+ const ids = this.selectedIds();
3919
+ if (ids.size === 0) {
3920
+ this.selectionCancelled.emit();
3921
+ return;
3922
+ }
3923
+ const entities = this.#instances().filter(e => ids.has(e.id));
3924
+ this.selectionConfirmed.emit(entities);
3925
+ }
3926
+ cancelSelection() {
3927
+ this.selectionCancelled.emit();
3928
+ }
3929
+ // ── Private helpers ───────────────────────────────────────────────────
3930
+ async #handleCreateSubmit(data) {
3931
+ const cls = this.#entityCls();
3932
+ if (!cls)
3933
+ return;
3934
+ const parent = this.draftParentEntity();
3935
+ const relName = this.parentRelationName();
3936
+ if (parent && relName) {
3937
+ const child = new cls(data);
3938
+ const relation = parent[relName + '$'];
3939
+ if (relation && typeof relation.add === 'function') {
3940
+ relation.add(child);
3941
+ }
3942
+ this.#localDraftItems.update(items => [...items, child]);
3943
+ return;
3944
+ }
3945
+ try {
3946
+ const inst = new cls(data);
3947
+ await inst.save();
3948
+ this.#currentList()?.refresh();
3949
+ }
3950
+ catch (e) {
3951
+ this.#errorHandler.handleError(e);
3952
+ }
3953
+ }
3954
+ #getOrCreateList(key, cls) {
3955
+ const cached = this.#scrollListMap.get(key);
3956
+ if (cached)
3957
+ return cached;
3958
+ const options = computed(() => {
3959
+ const userFilter = this.filterQuery();
3960
+ const fixed = this.fixedQuery();
3961
+ let where;
3962
+ if (!fixed || fixed.rules.length === 0) {
3963
+ where = userFilter;
3964
+ }
3965
+ else if (userFilter.rules.length === 0) {
3966
+ where = fixed;
3967
+ }
3968
+ else {
3969
+ where = { combinator: 'and', rules: [...fixed.rules, ...userFilter.rules] };
3970
+ }
3971
+ return {
3972
+ where: where,
3973
+ orderBy: [{ field: 'id', sort: 'desc' }]
3974
+ };
3975
+ }, ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
3976
+ const list = runInInjectionContext(this.#injector, () => new InfiniteScrollingList(this.#rxdb, cls, options));
3977
+ this.#scrollListMap.set(key, list);
3978
+ return list;
3979
+ }
3980
+ #enqueue(recordId, changes) {
3981
+ const cur = this.#pendingChanges.get(recordId) ?? {};
3982
+ this.#pendingChanges.set(recordId, { ...cur, ...changes });
3983
+ if (this.#flushHandle)
3984
+ return;
3985
+ this.#flushHandle = true;
3986
+ queueMicrotask(() => {
3987
+ this.#flushHandle = false;
3988
+ const snapshot = [...this.#pendingChanges.entries()];
3989
+ this.#pendingChanges.clear();
3990
+ void this.#flushPending(snapshot);
3991
+ });
3992
+ }
3993
+ async #flushPending(pending) {
3994
+ if (!pending.length)
3995
+ return;
3996
+ const EntityClass = this.#entityCls();
3997
+ if (!EntityClass)
3998
+ return;
3999
+ const toUpdate = new Set();
4000
+ const prevMap = new Map();
4001
+ const draftUpdated = new Set();
4002
+ for (const [id, changes] of pending) {
4003
+ let inst = this.#instances().find(i => i.id === id);
4004
+ const isDraft = !inst;
4005
+ if (!inst) {
4006
+ inst = this.#localDraftItems().find(i => i.id === id);
4007
+ }
4008
+ if (!inst)
4009
+ continue;
4010
+ const prev = {};
4011
+ const actual = {};
4012
+ let hasChange = false;
4013
+ for (const k of Object.keys(changes)) {
4014
+ if (inst[k] !== changes[k]) {
4015
+ prev[k] = inst[k];
4016
+ actual[k] = changes[k];
4017
+ hasChange = true;
4018
+ }
4019
+ }
4020
+ if (!hasChange)
4021
+ continue;
4022
+ prevMap.set(inst, prev);
4023
+ Object.assign(inst, actual);
4024
+ if (isDraft) {
4025
+ draftUpdated.add(inst);
4026
+ }
4027
+ else {
4028
+ toUpdate.add(inst);
4029
+ }
4030
+ }
4031
+ // 草稿实体直接更新 signal 触发重渲染(不写DB)
4032
+ if (draftUpdated.size) {
4033
+ this.#localDraftItems.update(items => [...items]);
4034
+ }
4035
+ if (!toUpdate.size)
4036
+ return;
4037
+ const updateMap = new Map([[EntityClass, toUpdate]]);
4038
+ try {
4039
+ await this.#rxdb.entityManager.mutations({ create: new Map(), update: updateMap, remove: new Map() });
4040
+ }
4041
+ catch (e) {
4042
+ this.#errorHandler.handleError(e);
4043
+ for (const [inst, prev] of prevMap)
4044
+ Object.assign(inst, prev);
4045
+ }
4046
+ }
4047
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: EntityListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4048
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: EntityListComponent, isStandalone: true, selector: "rxdb-entity-list", inputs: { namespace: { classPropertyName: "namespace", publicName: "namespace", isSignal: true, isRequired: true, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, fixedQuery: { classPropertyName: "fixedQuery", publicName: "fixedQuery", isSignal: true, isRequired: false, transformFunction: null }, fixedFormData: { classPropertyName: "fixedFormData", publicName: "fixedFormData", isSignal: true, isRequired: false, transformFunction: null }, draftParentEntity: { classPropertyName: "draftParentEntity", publicName: "draftParentEntity", isSignal: true, isRequired: false, transformFunction: null }, parentRelationName: { classPropertyName: "parentRelationName", publicName: "parentRelationName", isSignal: true, isRequired: false, transformFunction: null }, creationChain: { classPropertyName: "creationChain", publicName: "creationChain", isSignal: true, isRequired: false, transformFunction: null }, relationKind: { classPropertyName: "relationKind", publicName: "relationKind", isSignal: true, isRequired: false, transformFunction: null }, parentEntity: { classPropertyName: "parentEntity", publicName: "parentEntity", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, alreadyLinkedIds: { classPropertyName: "alreadyLinkedIds", publicName: "alreadyLinkedIds", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { viewEntity: "viewEntity", selectionConfirmed: "selectionConfirmed", selectionCancelled: "selectionCancelled" }, host: { listeners: { "keydown": "onKeydown($event)" }, styleAttribute: "display:flex;flex-direction:column;height:100%;overflow:hidden" }, viewQueries: [{ propertyName: "m2mSelectTpl", first: true, predicate: ["m2mSelectTpl"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- \u9876\u680F\uFF1A\u5B9E\u4F53\u540D\u79F0 + \u8BA1\u6570 + \u7B5B\u9009\u6309\u94AE + \u65B0\u589E\u6309\u94AE -->\n<div class=\"border-base-300 flex shrink-0 items-center justify-between border-b px-4 py-2\">\n <div class=\"flex items-center gap-2 text-sm\">\n <span class=\"font-medium\">{{ displayName() }}</span>\n @if ($isFilterQuery()) {\n <span class=\"badge badge-ghost badge-sm\">{{ filteredCount() }} \u6761\u8BB0\u5F55</span>\n }\n </div>\n <div class=\"flex items-center gap-2\">\n @if (!isSelectMode()) {\n <!-- Undo / Redo -->\n <button\n class=\"btn btn-xs btn-ghost\"\n [disabled]=\"!canUndo()\"\n (click)=\"undo()\"\n aria-label=\"\u64A4\u9500 (Ctrl+Z)\"\n type=\"button\"\n >\n <lucide-icon class=\"h-3.5 w-3.5\" [img]=\"Undo2\" />\n @if (undoCount(); as c) {\n <span class=\"badge badge-xs\">{{ c }}</span>\n }\n </button>\n <button\n class=\"btn btn-xs btn-ghost\"\n [disabled]=\"!canRedo()\"\n (click)=\"redo()\"\n aria-label=\"\u91CD\u505A (Ctrl+Shift+Z)\"\n type=\"button\"\n >\n <lucide-icon class=\"h-3.5 w-3.5\" [img]=\"Redo2\" />\n @if (redoCount(); as c) {\n <span class=\"badge badge-xs\">{{ c }}</span>\n }\n </button>\n <div class=\"divider divider-horizontal mx-0.5 h-5 self-center\"></div>\n }\n <!-- \u7B5B\u9009 popover \u89E6\u53D1\u5668 -->\n <div>\n <button\n class=\"btn btn-ghost btn-sm gap-1\"\n #filterTrigger=\"cdkOverlayOrigin\"\n [class.btn-active]=\"isQueryActive()\"\n (click)=\"toggleFilterPopover()\"\n cdkOverlayOrigin\n type=\"button\"\n >\n <lucide-icon class=\"h-4 w-4\" [img]=\"Funnel\" />\n \u7B5B\u9009\n @if (isQueryActive()) {\n <span class=\"badge badge-primary badge-xs\">{{ filterQuery().rules.length }}</span>\n }\n </button>\n <ng-template\n [cdkConnectedOverlayBackdropClass]=\"'cdk-overlay-transparent-backdrop'\"\n [cdkConnectedOverlayHasBackdrop]=\"true\"\n [cdkConnectedOverlayOpen]=\"showFilterPopover()\"\n [cdkConnectedOverlayOrigin]=\"filterTrigger\"\n [cdkConnectedOverlayPositions]=\"overlayPositions\"\n (backdropClick)=\"showFilterPopover.set(false)\"\n cdkConnectedOverlay\n >\n @if (queryBuilderFields().length > 0) {\n <div\n class=\"border-base-300 bg-base-100 min-w-max rounded-lg border p-3 shadow-lg\"\n (click)=\"$event.stopPropagation()\"\n aria-hidden=\"true\"\n >\n <rxdb-query-builder\n [fields]=\"queryBuilderFields()\"\n [initialQuery]=\"$any(filterQuery())\"\n (queryChange)=\"onQueryChange($event)\"\n (validationChange)=\"onValidationChange($event)\"\n />\n <div class=\"mt-3 flex justify-end gap-2\">\n <button class=\"btn btn-ghost btn-sm\" (click)=\"resetFilter()\" type=\"button\">\u91CD\u7F6E</button>\n <button\n class=\"btn btn-primary btn-sm\"\n [disabled]=\"!pendingQueryValid()\"\n (click)=\"applyFilter()\"\n type=\"button\"\n >\n \u786E\u5B9A\n </button>\n </div>\n </div>\n }\n </ng-template>\n </div>\n @if (!isCreateBlocked()) {\n @if (isSelectMode()) {\n <button class=\"btn btn-sm btn-outline\" (click)=\"openCreateDialog()\" type=\"button\">+ \u65B0\u5EFA</button>\n } @else if (isM2m()) {\n <button class=\"btn btn-primary btn-sm\" (click)=\"openM2mSelectDialog()\" type=\"button\">+ \u6DFB\u52A0</button>\n } @else {\n <button class=\"btn btn-primary btn-sm\" (click)=\"openCreateDialog()\" type=\"button\">+ \u65B0\u589E</button>\n }\n }\n </div>\n</div>\n\n<!-- \u8868\u683C\u4E3B\u4F53 -->\n<div class=\"min-h-0 flex-1\">\n @if (isBrowser) {\n @for (key of [namespace() + ':' + name()]; track key) {\n <rxdb-query-table\n [columns]=\"tableColumns()\"\n [filteredCount]=\"filteredCount()\"\n [loading]=\"isInitialLoading()\"\n [loadingMore]=\"isLoadingMore()\"\n [loadMore]=\"loadMore()\"\n [queryActive]=\"isQueryActive()\"\n [records]=\"tableRecords()\"\n (batchUpdated)=\"onBatchUpdated($event)\"\n (cellChanged)=\"onCellChanged($event)\"\n (iconClicked)=\"onIconClicked($event)\"\n (rowDeleted)=\"onRowDeleted($event)\"\n />\n }\n }\n</div>\n\n<!-- Select mode footer -->\n@if (isSelectMode()) {\n <div class=\"border-base-300 flex shrink-0 items-center justify-between border-t px-4 py-3\">\n @if (selectedCount() > 0) {\n <span class=\"text-base-content/70 text-sm\">\u5DF2\u9009 {{ selectedCount() }} \u9879</span>\n } @else {\n <span class=\"text-base-content/40 text-sm\">\u8BF7\u9009\u62E9\u8981\u5173\u8054\u7684{{ displayName() }}</span>\n }\n <div class=\"flex gap-2\">\n <button class=\"btn btn-sm\" (click)=\"cancelSelection()\" type=\"button\">\u53D6\u6D88</button>\n <button\n class=\"btn btn-primary btn-sm\"\n [disabled]=\"selectedCount() === 0\"\n (click)=\"confirmSelection()\"\n type=\"button\"\n >\n \u786E\u8BA4\u6DFB\u52A0\n </button>\n </div>\n </div>\n}\n\n<!-- M2M Selection Dialog Template -->\n<ng-template #m2mSelectTpl>\n <rxdb-entity-dialog [title]=\"'\u6DFB\u52A0' + displayName()\" (closeRequested)=\"m2mCancelSelection()\">\n <rxdb-entity-list\n [alreadyLinkedIds]=\"m2mAlreadyLinkedIds()\"\n [mode]=\"'select'\"\n [name]=\"name()\"\n [namespace]=\"namespace()\"\n (selectionCancelled)=\"m2mCancelSelection()\"\n (selectionConfirmed)=\"onM2mSelectionConfirmed($event)\"\n />\n </rxdb-entity-dialog>\n</ng-template>\n", dependencies: [{ kind: "component", type: i0.forwardRef(() => EntityListComponent), selector: "rxdb-entity-list", inputs: ["namespace", "name", "fixedQuery", "fixedFormData", "draftParentEntity", "parentRelationName", "creationChain", "relationKind", "parentEntity", "mode", "alreadyLinkedIds"], outputs: ["viewEntity", "selectionConfirmed", "selectionCancelled"] }, { kind: "component", type: i0.forwardRef(() => QueryTableComponent), selector: "rxdb-query-table", inputs: ["records", "columns", "idField", "nonClearableFields", "tableOptions", "cellErrorDetector", "isDarkMode", "cellClearable", "queryActive", "totalCount", "filteredCount", "loading", "loadingMore", "loadMore"], outputs: ["cellChanged", "rowDeleted", "iconClicked", "batchUpdated", "rowReordered", "scrollNearBottom"] }, { kind: "component", type: i0.forwardRef(() => QueryBuilderComponent), selector: "rxdb-query-builder", inputs: ["schema", "fields", "initialQuery", "maxDepth", "height", "enableDrag", "enableCollapse"], outputs: ["queryChange", "validationChange"] }, { kind: "ngmodule", type: i0.forwardRef(() => LucideAngularModule) }, { kind: "component", type: i0.forwardRef(() => i2.LucideAngularComponent), selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }, { kind: "directive", type: i0.forwardRef(() => CdkConnectedOverlay), selector: "[cdk-connected-overlay], [connected-overlay], [cdkConnectedOverlay]", inputs: ["cdkConnectedOverlayOrigin", "cdkConnectedOverlayPositions", "cdkConnectedOverlayPositionStrategy", "cdkConnectedOverlayOffsetX", "cdkConnectedOverlayOffsetY", "cdkConnectedOverlayWidth", "cdkConnectedOverlayHeight", "cdkConnectedOverlayMinWidth", "cdkConnectedOverlayMinHeight", "cdkConnectedOverlayBackdropClass", "cdkConnectedOverlayPanelClass", "cdkConnectedOverlayViewportMargin", "cdkConnectedOverlayScrollStrategy", "cdkConnectedOverlayOpen", "cdkConnectedOverlayDisableClose", "cdkConnectedOverlayTransformOriginOn", "cdkConnectedOverlayHasBackdrop", "cdkConnectedOverlayLockPosition", "cdkConnectedOverlayFlexibleDimensions", "cdkConnectedOverlayGrowAfterOpen", "cdkConnectedOverlayPush", "cdkConnectedOverlayDisposeOnNavigation", "cdkConnectedOverlayUsePopover", "cdkConnectedOverlayMatchWidth", "cdkConnectedOverlay"], outputs: ["backdropClick", "positionChange", "attach", "detach", "overlayKeydown", "overlayOutsideClick"], exportAs: ["cdkConnectedOverlay"] }, { kind: "directive", type: i0.forwardRef(() => CdkOverlayOrigin), selector: "[cdk-overlay-origin], [overlay-origin], [cdkOverlayOrigin]", exportAs: ["cdkOverlayOrigin"] }, { kind: "component", type: i0.forwardRef(() => EntityDialogComponent), selector: "rxdb-entity-dialog", inputs: ["title"], outputs: ["closeRequested"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4049
+ }
4050
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: EntityListComponent, decorators: [{
4051
+ type: Component,
4052
+ args: [{ selector: 'rxdb-entity-list', host: {
4053
+ style: 'display:flex;flex-direction:column;height:100%;overflow:hidden'
4054
+ }, changeDetection: ChangeDetectionStrategy.OnPush, imports: [
4055
+ QueryTableComponent,
4056
+ QueryBuilderComponent,
4057
+ LucideAngularModule,
4058
+ CdkConnectedOverlay,
4059
+ CdkOverlayOrigin,
4060
+ forwardRef(() => EntityListComponent),
4061
+ forwardRef(() => EntityDialogComponent)
4062
+ ], template: "<!-- \u9876\u680F\uFF1A\u5B9E\u4F53\u540D\u79F0 + \u8BA1\u6570 + \u7B5B\u9009\u6309\u94AE + \u65B0\u589E\u6309\u94AE -->\n<div class=\"border-base-300 flex shrink-0 items-center justify-between border-b px-4 py-2\">\n <div class=\"flex items-center gap-2 text-sm\">\n <span class=\"font-medium\">{{ displayName() }}</span>\n @if ($isFilterQuery()) {\n <span class=\"badge badge-ghost badge-sm\">{{ filteredCount() }} \u6761\u8BB0\u5F55</span>\n }\n </div>\n <div class=\"flex items-center gap-2\">\n @if (!isSelectMode()) {\n <!-- Undo / Redo -->\n <button\n class=\"btn btn-xs btn-ghost\"\n [disabled]=\"!canUndo()\"\n (click)=\"undo()\"\n aria-label=\"\u64A4\u9500 (Ctrl+Z)\"\n type=\"button\"\n >\n <lucide-icon class=\"h-3.5 w-3.5\" [img]=\"Undo2\" />\n @if (undoCount(); as c) {\n <span class=\"badge badge-xs\">{{ c }}</span>\n }\n </button>\n <button\n class=\"btn btn-xs btn-ghost\"\n [disabled]=\"!canRedo()\"\n (click)=\"redo()\"\n aria-label=\"\u91CD\u505A (Ctrl+Shift+Z)\"\n type=\"button\"\n >\n <lucide-icon class=\"h-3.5 w-3.5\" [img]=\"Redo2\" />\n @if (redoCount(); as c) {\n <span class=\"badge badge-xs\">{{ c }}</span>\n }\n </button>\n <div class=\"divider divider-horizontal mx-0.5 h-5 self-center\"></div>\n }\n <!-- \u7B5B\u9009 popover \u89E6\u53D1\u5668 -->\n <div>\n <button\n class=\"btn btn-ghost btn-sm gap-1\"\n #filterTrigger=\"cdkOverlayOrigin\"\n [class.btn-active]=\"isQueryActive()\"\n (click)=\"toggleFilterPopover()\"\n cdkOverlayOrigin\n type=\"button\"\n >\n <lucide-icon class=\"h-4 w-4\" [img]=\"Funnel\" />\n \u7B5B\u9009\n @if (isQueryActive()) {\n <span class=\"badge badge-primary badge-xs\">{{ filterQuery().rules.length }}</span>\n }\n </button>\n <ng-template\n [cdkConnectedOverlayBackdropClass]=\"'cdk-overlay-transparent-backdrop'\"\n [cdkConnectedOverlayHasBackdrop]=\"true\"\n [cdkConnectedOverlayOpen]=\"showFilterPopover()\"\n [cdkConnectedOverlayOrigin]=\"filterTrigger\"\n [cdkConnectedOverlayPositions]=\"overlayPositions\"\n (backdropClick)=\"showFilterPopover.set(false)\"\n cdkConnectedOverlay\n >\n @if (queryBuilderFields().length > 0) {\n <div\n class=\"border-base-300 bg-base-100 min-w-max rounded-lg border p-3 shadow-lg\"\n (click)=\"$event.stopPropagation()\"\n aria-hidden=\"true\"\n >\n <rxdb-query-builder\n [fields]=\"queryBuilderFields()\"\n [initialQuery]=\"$any(filterQuery())\"\n (queryChange)=\"onQueryChange($event)\"\n (validationChange)=\"onValidationChange($event)\"\n />\n <div class=\"mt-3 flex justify-end gap-2\">\n <button class=\"btn btn-ghost btn-sm\" (click)=\"resetFilter()\" type=\"button\">\u91CD\u7F6E</button>\n <button\n class=\"btn btn-primary btn-sm\"\n [disabled]=\"!pendingQueryValid()\"\n (click)=\"applyFilter()\"\n type=\"button\"\n >\n \u786E\u5B9A\n </button>\n </div>\n </div>\n }\n </ng-template>\n </div>\n @if (!isCreateBlocked()) {\n @if (isSelectMode()) {\n <button class=\"btn btn-sm btn-outline\" (click)=\"openCreateDialog()\" type=\"button\">+ \u65B0\u5EFA</button>\n } @else if (isM2m()) {\n <button class=\"btn btn-primary btn-sm\" (click)=\"openM2mSelectDialog()\" type=\"button\">+ \u6DFB\u52A0</button>\n } @else {\n <button class=\"btn btn-primary btn-sm\" (click)=\"openCreateDialog()\" type=\"button\">+ \u65B0\u589E</button>\n }\n }\n </div>\n</div>\n\n<!-- \u8868\u683C\u4E3B\u4F53 -->\n<div class=\"min-h-0 flex-1\">\n @if (isBrowser) {\n @for (key of [namespace() + ':' + name()]; track key) {\n <rxdb-query-table\n [columns]=\"tableColumns()\"\n [filteredCount]=\"filteredCount()\"\n [loading]=\"isInitialLoading()\"\n [loadingMore]=\"isLoadingMore()\"\n [loadMore]=\"loadMore()\"\n [queryActive]=\"isQueryActive()\"\n [records]=\"tableRecords()\"\n (batchUpdated)=\"onBatchUpdated($event)\"\n (cellChanged)=\"onCellChanged($event)\"\n (iconClicked)=\"onIconClicked($event)\"\n (rowDeleted)=\"onRowDeleted($event)\"\n />\n }\n }\n</div>\n\n<!-- Select mode footer -->\n@if (isSelectMode()) {\n <div class=\"border-base-300 flex shrink-0 items-center justify-between border-t px-4 py-3\">\n @if (selectedCount() > 0) {\n <span class=\"text-base-content/70 text-sm\">\u5DF2\u9009 {{ selectedCount() }} \u9879</span>\n } @else {\n <span class=\"text-base-content/40 text-sm\">\u8BF7\u9009\u62E9\u8981\u5173\u8054\u7684{{ displayName() }}</span>\n }\n <div class=\"flex gap-2\">\n <button class=\"btn btn-sm\" (click)=\"cancelSelection()\" type=\"button\">\u53D6\u6D88</button>\n <button\n class=\"btn btn-primary btn-sm\"\n [disabled]=\"selectedCount() === 0\"\n (click)=\"confirmSelection()\"\n type=\"button\"\n >\n \u786E\u8BA4\u6DFB\u52A0\n </button>\n </div>\n </div>\n}\n\n<!-- M2M Selection Dialog Template -->\n<ng-template #m2mSelectTpl>\n <rxdb-entity-dialog [title]=\"'\u6DFB\u52A0' + displayName()\" (closeRequested)=\"m2mCancelSelection()\">\n <rxdb-entity-list\n [alreadyLinkedIds]=\"m2mAlreadyLinkedIds()\"\n [mode]=\"'select'\"\n [name]=\"name()\"\n [namespace]=\"namespace()\"\n (selectionCancelled)=\"m2mCancelSelection()\"\n (selectionConfirmed)=\"onM2mSelectionConfirmed($event)\"\n />\n </rxdb-entity-dialog>\n</ng-template>\n" }]
4063
+ }], ctorParameters: () => [], propDecorators: { namespace: [{ type: i0.Input, args: [{ isSignal: true, alias: "namespace", required: true }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }], fixedQuery: [{ type: i0.Input, args: [{ isSignal: true, alias: "fixedQuery", required: false }] }], fixedFormData: [{ type: i0.Input, args: [{ isSignal: true, alias: "fixedFormData", required: false }] }], draftParentEntity: [{ type: i0.Input, args: [{ isSignal: true, alias: "draftParentEntity", required: false }] }], parentRelationName: [{ type: i0.Input, args: [{ isSignal: true, alias: "parentRelationName", required: false }] }], creationChain: [{ type: i0.Input, args: [{ isSignal: true, alias: "creationChain", required: false }] }], relationKind: [{ type: i0.Input, args: [{ isSignal: true, alias: "relationKind", required: false }] }], parentEntity: [{ type: i0.Input, args: [{ isSignal: true, alias: "parentEntity", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], alreadyLinkedIds: [{ type: i0.Input, args: [{ isSignal: true, alias: "alreadyLinkedIds", required: false }] }], viewEntity: [{ type: i0.Output, args: ["viewEntity"] }], selectionConfirmed: [{ type: i0.Output, args: ["selectionConfirmed"] }], selectionCancelled: [{ type: i0.Output, args: ["selectionCancelled"] }], m2mSelectTpl: [{ type: i0.ViewChild, args: ['m2mSelectTpl', { isSignal: true }] }], onKeydown: [{
4064
+ type: HostListener,
4065
+ args: ['keydown', ['$event']]
4066
+ }] } });
4067
+
4068
+ let nextDetailId = 0;
4069
+ class EntityDetailComponent {
4070
+ #dialogRef = inject(DialogRef, { optional: true });
4071
+ #dialogData = inject(DIALOG_DATA, { optional: true });
4072
+ #rxdb = inject(RxDB, { optional: true });
4073
+ #errorHandler = inject(ErrorHandler);
4074
+ /** 创建模式下的草稿实体实例(立即创建,不保存) */
4075
+ #draftEntity = null;
4076
+ // 根据 namespace + name 查找对应实体类
4077
+ #entityCls = computed(() => {
4078
+ const rxdb = this.#rxdb;
4079
+ if (!rxdb)
4080
+ return null;
4081
+ const ns = this.namespace();
4082
+ const nm = this.name();
4083
+ if (!ns || !nm)
4084
+ return null;
4085
+ return (rxdb.config.entities.find(cls => {
4086
+ const meta = getEntityMetadata(cls);
4087
+ return meta.namespace === ns && meta.name === nm;
4088
+ }) ?? null);
4089
+ }, ...(ngDevMode ? [{ debugName: "#entityCls" }] : /* istanbul ignore next */ []));
4090
+ /** Dialog 传入的实体类(根据 metadata 查找) */
4091
+ #entityClsFromDialog = computed(() => {
4092
+ const rxdb = this.#rxdb;
4093
+ const meta = this.#dialogData?.metadata;
4094
+ if (!rxdb || !meta)
4095
+ return null;
4096
+ return (rxdb.config.entities.find(cls => {
4097
+ const m = getEntityMetadata(cls);
4098
+ return m.namespace === meta.namespace && m.name === meta.name;
4099
+ }) ?? null);
4100
+ }, ...(ngDevMode ? [{ debugName: "#entityClsFromDialog" }] : /* istanbul ignore next */ []));
4101
+ #metadataFromRouteInputs = computed(() => {
4102
+ const cls = this.#entityCls();
4103
+ return cls ? getEntityMetadata(cls) : null;
4104
+ }, ...(ngDevMode ? [{ debugName: "#metadataFromRouteInputs" }] : /* istanbul ignore next */ []));
4105
+ #formFieldsFromRouteInputs = computed(() => {
4106
+ const meta = this.#metadataFromRouteInputs();
4107
+ if (!meta)
4108
+ return null;
4109
+ return buildFormFields(meta, this.formModeValue);
4110
+ }, ...(ngDevMode ? [{ debugName: "#formFieldsFromRouteInputs" }] : /* istanbul ignore next */ []));
4111
+ #entityInstance = toSignal(toObservable(computed(() => ({ cls: this.#entityCls(), id: this.entityId() }))).pipe(switchMap(({ cls, id }) => {
4112
+ if (!cls || !id || !this.#rxdb)
4113
+ return of(null);
4114
+ return this.#rxdb.entityManager.getRepository(cls).get(id);
4115
+ })), { initialValue: null });
4116
+ // 路由参数输入(与 namespace/name/entityId 路由参数绑定)
4117
+ namespace = input(...(ngDevMode ? [undefined, { debugName: "namespace" }] : /* istanbul ignore next */ []));
4118
+ name = input(...(ngDevMode ? [undefined, { debugName: "name" }] : /* istanbul ignore next */ []));
4119
+ entityId = input(...(ngDevMode ? [undefined, { debugName: "entityId" }] : /* istanbul ignore next */ []));
4120
+ /** 唯一的 radio group name,避免多个 EntityDetailComponent 实例之间的 radio 冲突 */
4121
+ tabGroupName = `detail-tabs-${nextDetailId++}`;
4122
+ // 直接绑定输入
4123
+ metadata = input(undefined, ...(ngDevMode ? [{ debugName: "metadata" }] : /* istanbul ignore next */ []));
4124
+ formFields = input(...(ngDevMode ? [undefined, { debugName: "formFields" }] : /* istanbul ignore next */ []));
4125
+ formData = input(...(ngDevMode ? [undefined, { debugName: "formData" }] : /* istanbul ignore next */ []));
4126
+ formMode = input(undefined, ...(ngDevMode ? [{ debugName: "formMode" }] : /* istanbul ignore next */ []));
4127
+ relatedEntityProvider = input(...(ngDevMode ? [undefined, { debugName: "relatedEntityProvider" }] : /* istanbul ignore next */ []));
4128
+ formSubmitted = output();
4129
+ formCancelled = output();
4130
+ fieldChanged = output();
4131
+ validationErrors = output();
4132
+ /** 是否使用外部保存模式(create 模式下,保存/取消按钮在 tabs 下面,而非 form 内) */
4133
+ isCreateMode = computed(() => this.formModeValue === 'create', ...(ngDevMode ? [{ debugName: "isCreateMode" }] : /* istanbul ignore next */ []));
4134
+ /** 当前表单编辑数据(create 模式下跟踪用户输入) */
4135
+ draftFormData = signal({}, ...(ngDevMode ? [{ debugName: "draftFormData" }] : /* istanbul ignore next */ []));
4136
+ /** 暴露草稿实体给模板(关系 tab 的 entity-list 需要) */
4137
+ draftEntitySignal = signal(null, ...(ngDevMode ? [{ debugName: "draftEntitySignal" }] : /* istanbul ignore next */ []));
4138
+ /** 父实体(草稿或已保存实例),用于关系 tab 的 entity-list */
4139
+ parentEntityForRelation = computed(() => {
4140
+ return this.draftEntitySignal() ?? this.#entityInstance() ?? null;
4141
+ }, ...(ngDevMode ? [{ debugName: "parentEntityForRelation" }] : /* istanbul ignore next */ []));
4142
+ /** 弹框标题 */
4143
+ dialogTitle = computed(() => {
4144
+ const meta = this.metadataValue;
4145
+ const mode = this.formModeValue;
4146
+ const name = meta?.displayName ?? meta?.name ?? '';
4147
+ return mode === 'create' ? `新建${name}` : name;
4148
+ }, ...(ngDevMode ? [{ debugName: "dialogTitle" }] : /* istanbul ignore next */ []));
4149
+ /** 创建链路:当前实体 + 祖先链路,传递给子 entity-list 以阻断循环创建 */
4150
+ creationChain = computed(() => {
4151
+ const parentChain = this.#dialogData?.creationChain ?? [];
4152
+ if (!this.isCreateMode())
4153
+ return parentChain;
4154
+ const meta = this.metadataValue;
4155
+ if (!meta)
4156
+ return parentChain;
4157
+ const key = `${meta.namespace}:${meta.name}`;
4158
+ return parentChain.includes(key) ? parentChain : [...parentChain, key];
4159
+ }, ...(ngDevMode ? [{ debugName: "creationChain" }] : /* istanbul ignore next */ []));
4160
+ /** 当前活动关系 tab 的 relationName(用于子实体注册关系) */
4161
+ activeRelationName = computed(() => {
4162
+ const tab = this.activeTab();
4163
+ return tab?.type === 'table' ? tab.relationName : undefined;
4164
+ }, ...(ngDevMode ? [{ debugName: "activeRelationName" }] : /* istanbul ignore next */ []));
4165
+ tabs = computed(() => {
4166
+ const meta = this.metadataValue;
4167
+ if (!meta)
4168
+ return [];
4169
+ const chain = this.creationChain();
4170
+ return buildDetailTabs(meta).filter(tab => {
4171
+ if (tab.type !== 'table')
4172
+ return true;
4173
+ const t = tab;
4174
+ const key = `${t.relatedNamespace}:${t.relatedEntityName}`;
4175
+ return !chain.includes(key);
4176
+ });
4177
+ }, ...(ngDevMode ? [{ debugName: "tabs" }] : /* istanbul ignore next */ []));
4178
+ activeTabKey = linkedSignal({ ...(ngDevMode ? { debugName: "activeTabKey" } : /* istanbul ignore next */ {}), source: this.tabs,
4179
+ computation: tabs => tabs[0]?.key ?? 'basic' });
4180
+ activeTab = computed(() => this.tabs().find(t => t.key === this.activeTabKey()), ...(ngDevMode ? [{ debugName: "activeTab" }] : /* istanbul ignore next */ []));
4181
+ // 公开访问器 - 优先级:直接 input > 路由推导 > dialog data
4182
+ get metadataValue() {
4183
+ return this.metadata() ?? this.#metadataFromRouteInputs() ?? this.#dialogData.metadata;
4184
+ }
4185
+ get formFieldsValue() {
4186
+ const fields = this.formFields() ?? this.#formFieldsFromRouteInputs() ?? this.#dialogData.formFields;
4187
+ const fixed = this.#dialogData?.fixedFormData;
4188
+ if (!fixed)
4189
+ return fields;
4190
+ return fields.map(f => (f.field in fixed ? { ...f, readonly: true } : f));
4191
+ }
4192
+ get formDataValue() {
4193
+ if (this.isCreateMode()) {
4194
+ const draft = this.draftFormData();
4195
+ const fixed = this.#dialogData?.fixedFormData;
4196
+ return fixed ? { ...draft, ...fixed } : draft;
4197
+ }
4198
+ const inst = this.#entityInstance();
4199
+ if (inst)
4200
+ return entityToFormData(inst, this.formFieldsValue ?? []);
4201
+ const base = this.formData() ?? this.#dialogData?.formData ?? {};
4202
+ const fixed = this.#dialogData?.fixedFormData;
4203
+ return fixed ? { ...base, ...fixed } : base;
4204
+ }
4205
+ get formModeValue() {
4206
+ return this.formMode() ?? this.#dialogData?.formMode ?? 'edit';
4207
+ }
4208
+ get relatedEntityProviderValue() {
4209
+ return this.relatedEntityProvider() ?? this.#dialogData?.relatedEntityProvider;
4210
+ }
4211
+ constructor() {
4212
+ if (this.#dialogData?.formMode === 'create' && !this.#dialogData.delegateSave) {
4213
+ const cls = this.#entityClsFromDialog();
4214
+ if (cls) {
4215
+ const fixed = this.#dialogData.fixedFormData;
4216
+ const merged = { ...(this.#dialogData.formData || {}), ...fixed };
4217
+ this.#draftEntity = new cls(merged);
4218
+ this.draftEntitySignal.set(this.#draftEntity);
4219
+ this.draftFormData.set(entityToFormData(this.#draftEntity, this.#dialogData.formFields));
4220
+ }
4221
+ }
4222
+ }
4223
+ selectTab(key) {
4224
+ const targetTab = this.tabs().find(t => t.key === key);
4225
+ if (this.isCreateMode() && targetTab?.type === 'table') {
4226
+ const fields = this.formFieldsValue;
4227
+ const data = this.draftFormData();
4228
+ const fixed = this.#dialogData?.fixedFormData;
4229
+ const merged = fixed ? { ...data, ...fixed } : data;
4230
+ const result = validateForm(fields, merged);
4231
+ if (!result.valid) {
4232
+ this.validationErrors.emit(result);
4233
+ return;
4234
+ }
4235
+ this.#syncDraftEntity();
4236
+ }
4237
+ this.activeTabKey.set(key);
4238
+ }
4239
+ onFormDataChanged(data) {
4240
+ this.draftFormData.set(data);
4241
+ }
4242
+ onFormSubmitted(data) {
4243
+ this.formSubmitted.emit(data);
4244
+ }
4245
+ onFormCancelled() {
4246
+ this.#cleanupDraftEntity();
4247
+ this.formCancelled.emit();
4248
+ this.#dialogRef?.close();
4249
+ }
4250
+ onFieldChanged(event) {
4251
+ if (this.isCreateMode()) {
4252
+ this.draftFormData.update(d => ({ ...d, [event.field]: event.value }));
4253
+ }
4254
+ this.fieldChanged.emit(event);
4255
+ }
4256
+ onValidationErrors(result) {
4257
+ this.validationErrors.emit(result);
4258
+ }
4259
+ /** 外部保存按钮(create 模式) */
4260
+ onSave() {
4261
+ const fields = this.formFieldsValue;
4262
+ const data = this.draftFormData();
4263
+ const fixed = this.#dialogData?.fixedFormData;
4264
+ const merged = fixed ? { ...data, ...fixed } : data;
4265
+ const result = validateForm(fields, merged);
4266
+ if (!result.valid) {
4267
+ this.validationErrors.emit(result);
4268
+ return;
4269
+ }
4270
+ if (this.#draftEntity) {
4271
+ this.#syncDraftEntity();
4272
+ this.#draftEntity
4273
+ .save()
4274
+ .then(() => {
4275
+ this.#draftEntity = null;
4276
+ this.draftEntitySignal.set(null);
4277
+ this.#dialogRef?.close('saved');
4278
+ })
4279
+ .catch(e => this.#errorHandler.handleError(e));
4280
+ }
4281
+ else {
4282
+ this.formSubmitted.emit(merged);
4283
+ this.#dialogRef?.close();
4284
+ }
4285
+ }
4286
+ onCancel() {
4287
+ this.onFormCancelled();
4288
+ }
4289
+ buildFixedQuery(tab) {
4290
+ const entityId = this.#draftEntity?.id ?? this.formDataValue['id'];
4291
+ if (!entityId)
4292
+ return undefined;
4293
+ if (tab.relationKind === RelationKind.ONE_TO_MANY && tab.foreignKeyField) {
4294
+ return { combinator: 'and', rules: [{ field: tab.foreignKeyField, operator: '=', value: entityId }] };
4295
+ }
4296
+ if (tab.relationKind === RelationKind.MANY_TO_MANY && tab.mappedProperty) {
4297
+ return { combinator: 'and', rules: [{ field: `${tab.mappedProperty}.id`, operator: '=', value: entityId }] };
4298
+ }
4299
+ return undefined;
4300
+ }
4301
+ buildFixedFormData(tab) {
4302
+ const entityId = this.#draftEntity?.id ?? this.formDataValue['id'];
4303
+ if (!entityId)
4304
+ return undefined;
4305
+ if (tab.relationKind === RelationKind.ONE_TO_MANY && tab.foreignKeyField) {
4306
+ return { [tab.foreignKeyField]: entityId };
4307
+ }
4308
+ return undefined;
4309
+ }
4310
+ /** 将表单数据同步到草稿实体 */
4311
+ #syncDraftEntity() {
4312
+ if (!this.#draftEntity)
4313
+ return;
4314
+ const data = this.draftFormData();
4315
+ const fixed = this.#dialogData?.fixedFormData;
4316
+ const merged = fixed ? { ...data, ...fixed } : data;
4317
+ Object.assign(this.#draftEntity, merged);
4318
+ }
4319
+ /** 清理草稿实体缓存 */
4320
+ #cleanupDraftEntity() {
4321
+ if (!this.#draftEntity || !this.#rxdb)
4322
+ return;
4323
+ this.#rxdb.entityManager.removeEntityCache(this.#draftEntity);
4324
+ this.#draftEntity = null;
4325
+ this.draftEntitySignal.set(null);
4326
+ }
4327
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: EntityDetailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4328
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: EntityDetailComponent, isStandalone: true, selector: "rxdb-entity-detail", inputs: { namespace: { classPropertyName: "namespace", publicName: "namespace", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, entityId: { classPropertyName: "entityId", publicName: "entityId", isSignal: true, isRequired: false, transformFunction: null }, metadata: { classPropertyName: "metadata", publicName: "metadata", isSignal: true, isRequired: false, transformFunction: null }, formFields: { classPropertyName: "formFields", publicName: "formFields", isSignal: true, isRequired: false, transformFunction: null }, formData: { classPropertyName: "formData", publicName: "formData", isSignal: true, isRequired: false, transformFunction: null }, formMode: { classPropertyName: "formMode", publicName: "formMode", isSignal: true, isRequired: false, transformFunction: null }, relatedEntityProvider: { classPropertyName: "relatedEntityProvider", publicName: "relatedEntityProvider", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { formSubmitted: "formSubmitted", formCancelled: "formCancelled", fieldChanged: "fieldChanged", validationErrors: "validationErrors" }, ngImport: i0, template: "<rxdb-entity-dialog [title]=\"dialogTitle()\" (closeRequested)=\"onCancel()\">\n <!-- Tab Bar -->\n <div class=\"tabs tabs-border flex-1\" role=\"tablist\">\n @for (tab of tabs(); track tab.key) {\n <input\n class=\"tab\"\n [attr.aria-label]=\"tab.label\"\n [checked]=\"tab.key === activeTabKey()\"\n [name]=\"tabGroupName\"\n (change)=\"selectTab(tab.key)\"\n role=\"tab\"\n type=\"radio\"\n />\n <div\n class=\"tab-content border-base-300 bg-base-100\"\n [class.overflow-auto]=\"tab.type === 'form'\"\n [class.overflow-hidden]=\"tab.type === 'table'\"\n [class.p-4]=\"tab.type === 'form'\"\n [hidden]=\"tab.key !== activeTabKey()\"\n >\n @if (tab.type === 'form') {\n <rxdb-entity-form\n [data]=\"formDataValue\"\n [fields]=\"formFieldsValue\"\n [mode]=\"formModeValue\"\n [relatedEntityProvider]=\"relatedEntityProviderValue\"\n [showActions]=\"!isCreateMode()\"\n (fieldChanged)=\"onFieldChanged($event)\"\n (formCancelled)=\"onFormCancelled()\"\n (formSubmitted)=\"onFormSubmitted($event)\"\n (validationErrors)=\"onValidationErrors($event)\"\n />\n }\n @if (tab.type === 'table') {\n <rxdb-entity-list\n [creationChain]=\"creationChain()\"\n [draftParentEntity]=\"draftEntitySignal()\"\n [fixedFormData]=\"buildFixedFormData($any(tab))\"\n [fixedQuery]=\"buildFixedQuery($any(tab))\"\n [name]=\"$any(tab).relatedEntityName\"\n [namespace]=\"$any(tab).relatedNamespace\"\n [parentEntity]=\"parentEntityForRelation()\"\n [parentRelationName]=\"$any(tab).relationName\"\n [relationKind]=\"$any(tab).relationKind\"\n />\n }\n </div>\n }\n </div>\n\n <!-- Save/Cancel buttons for create mode (below tabs) -->\n @if (isCreateMode()) {\n <div class=\"border-base-300 flex shrink-0 justify-end gap-2 border-t px-4 py-3\">\n <button class=\"btn\" (click)=\"onCancel()\" type=\"button\">\u53D6\u6D88</button>\n <button class=\"btn btn-primary\" (click)=\"onSave()\" type=\"button\">\u4FDD\u5B58</button>\n </div>\n }\n</rxdb-entity-dialog>\n", styles: [":host{display:flex;flex-direction:column;height:100%;overflow:hidden}.tabs{grid-template-rows:auto 1fr!important}.tab-content{display:flex!important;flex-direction:column}\n"], dependencies: [{ kind: "component", type: EntityFormComponent, selector: "rxdb-entity-form", inputs: ["fields", "data", "mode", "relatedEntityProvider", "showActions"], outputs: ["fieldChanged", "formSubmitted", "formCancelled", "validationErrors"] }, { kind: "component", type: EntityListComponent, selector: "rxdb-entity-list", inputs: ["namespace", "name", "fixedQuery", "fixedFormData", "draftParentEntity", "parentRelationName", "creationChain", "relationKind", "parentEntity", "mode", "alreadyLinkedIds"], outputs: ["viewEntity", "selectionConfirmed", "selectionCancelled"] }, { kind: "component", type: EntityDialogComponent, selector: "rxdb-entity-dialog", inputs: ["title"], outputs: ["closeRequested"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4329
+ }
4330
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: EntityDetailComponent, decorators: [{
4331
+ type: Component,
4332
+ args: [{ selector: 'rxdb-entity-detail', imports: [EntityFormComponent, EntityListComponent, EntityDialogComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<rxdb-entity-dialog [title]=\"dialogTitle()\" (closeRequested)=\"onCancel()\">\n <!-- Tab Bar -->\n <div class=\"tabs tabs-border flex-1\" role=\"tablist\">\n @for (tab of tabs(); track tab.key) {\n <input\n class=\"tab\"\n [attr.aria-label]=\"tab.label\"\n [checked]=\"tab.key === activeTabKey()\"\n [name]=\"tabGroupName\"\n (change)=\"selectTab(tab.key)\"\n role=\"tab\"\n type=\"radio\"\n />\n <div\n class=\"tab-content border-base-300 bg-base-100\"\n [class.overflow-auto]=\"tab.type === 'form'\"\n [class.overflow-hidden]=\"tab.type === 'table'\"\n [class.p-4]=\"tab.type === 'form'\"\n [hidden]=\"tab.key !== activeTabKey()\"\n >\n @if (tab.type === 'form') {\n <rxdb-entity-form\n [data]=\"formDataValue\"\n [fields]=\"formFieldsValue\"\n [mode]=\"formModeValue\"\n [relatedEntityProvider]=\"relatedEntityProviderValue\"\n [showActions]=\"!isCreateMode()\"\n (fieldChanged)=\"onFieldChanged($event)\"\n (formCancelled)=\"onFormCancelled()\"\n (formSubmitted)=\"onFormSubmitted($event)\"\n (validationErrors)=\"onValidationErrors($event)\"\n />\n }\n @if (tab.type === 'table') {\n <rxdb-entity-list\n [creationChain]=\"creationChain()\"\n [draftParentEntity]=\"draftEntitySignal()\"\n [fixedFormData]=\"buildFixedFormData($any(tab))\"\n [fixedQuery]=\"buildFixedQuery($any(tab))\"\n [name]=\"$any(tab).relatedEntityName\"\n [namespace]=\"$any(tab).relatedNamespace\"\n [parentEntity]=\"parentEntityForRelation()\"\n [parentRelationName]=\"$any(tab).relationName\"\n [relationKind]=\"$any(tab).relationKind\"\n />\n }\n </div>\n }\n </div>\n\n <!-- Save/Cancel buttons for create mode (below tabs) -->\n @if (isCreateMode()) {\n <div class=\"border-base-300 flex shrink-0 justify-end gap-2 border-t px-4 py-3\">\n <button class=\"btn\" (click)=\"onCancel()\" type=\"button\">\u53D6\u6D88</button>\n <button class=\"btn btn-primary\" (click)=\"onSave()\" type=\"button\">\u4FDD\u5B58</button>\n </div>\n }\n</rxdb-entity-dialog>\n", styles: [":host{display:flex;flex-direction:column;height:100%;overflow:hidden}.tabs{grid-template-rows:auto 1fr!important}.tab-content{display:flex!important;flex-direction:column}\n"] }]
4333
+ }], ctorParameters: () => [], propDecorators: { namespace: [{ type: i0.Input, args: [{ isSignal: true, alias: "namespace", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], entityId: [{ type: i0.Input, args: [{ isSignal: true, alias: "entityId", required: false }] }], metadata: [{ type: i0.Input, args: [{ isSignal: true, alias: "metadata", required: false }] }], formFields: [{ type: i0.Input, args: [{ isSignal: true, alias: "formFields", required: false }] }], formData: [{ type: i0.Input, args: [{ isSignal: true, alias: "formData", required: false }] }], formMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "formMode", required: false }] }], relatedEntityProvider: [{ type: i0.Input, args: [{ isSignal: true, alias: "relatedEntityProvider", required: false }] }], formSubmitted: [{ type: i0.Output, args: ["formSubmitted"] }], formCancelled: [{ type: i0.Output, args: ["formCancelled"] }], fieldChanged: [{ type: i0.Output, args: ["fieldChanged"] }], validationErrors: [{ type: i0.Output, args: ["validationErrors"] }] } });
4334
+
4335
+ var entityDetail = /*#__PURE__*/Object.freeze({
4336
+ __proto__: null,
4337
+ EntityDetailComponent: EntityDetailComponent
4338
+ });
4339
+
4340
+ /**
4341
+ * @aiao/rxdb-model-angular
4342
+ *
4343
+ * RxDB 模型 Angular 组件库。
4344
+ * VTable 基础设施请从 `@aiao/rxdb-model` 导入。
4345
+ */
4346
+ // Angular 配置
4347
+
4348
+ /**
4349
+ * 子查询构建器组件
4350
+ *
4351
+ * @description
4352
+ * 用于 EXISTS/NOT EXISTS 操作符的嵌套查询条件。
4353
+ * 支持:
4354
+ * - 添加子条件
4355
+ * - 嵌套规则组
4356
+ * - 清空子条件
4357
+ */
4358
+ class SubqueryBuilderComponent {
4359
+ service;
4360
+ /** initialQuery 是否已应用(只应用一次) */
4361
+ initialQueryApplied = false;
4362
+ /** 根规则组 Signal */
4363
+ rootGroupSignal;
4364
+ /** 验证 Signal */
4365
+ validationSignal;
4366
+ /** 字段列表 */
4367
+ fields = input.required(...(ngDevMode ? [{ debugName: "fields" }] : /* istanbul ignore next */ []));
4368
+ /** 初始查询 */
4369
+ initialQuery = input(...(ngDevMode ? [undefined, { debugName: "initialQuery" }] : /* istanbul ignore next */ []));
4370
+ /** 最大嵌套层级 */
4371
+ maxDepth = input(3, ...(ngDevMode ? [{ debugName: "maxDepth" }] : /* istanbul ignore next */ []));
4372
+ /** 查询变更事件 */
4373
+ queryChange = output();
4374
+ /** 验证变更事件 */
4375
+ validationChange = output();
4376
+ /** 根规则组 */
4377
+ rootGroup = computed(() => {
4378
+ return this.rootGroupSignal();
4379
+ }, ...(ngDevMode ? [{ debugName: "rootGroup" }] : /* istanbul ignore next */ []));
4380
+ /** 是否有规则 */
4381
+ hasRules = computed(() => {
4382
+ const group = this.rootGroup();
4383
+ return group && group.rules.length > 0;
4384
+ }, ...(ngDevMode ? [{ debugName: "hasRules" }] : /* istanbul ignore next */ []));
4385
+ constructor() {
4386
+ // 在构造函数中初始化服务
4387
+ this.service = createQueryBuilderService({
4388
+ config: {
4389
+ maxNestingLevel: this.maxDepth()
4390
+ }
4391
+ });
4392
+ // 转换 Observable 为 Signal
4393
+ this.rootGroupSignal = toSignal(this.service.rootGroup$, { requireSync: true });
4394
+ this.validationSignal = toSignal(this.service.validation$, { initialValue: undefined });
4395
+ // 监听字段变化并同步到服务
4396
+ effect(() => {
4397
+ const fields = this.fields();
4398
+ if (fields.length > 0) {
4399
+ this.service.setFields(fields);
4400
+ }
4401
+ });
4402
+ // 监听初始查询并加载(只应用一次,避免用户修改后被重置)
4403
+ effect(() => {
4404
+ if (!this.initialQueryApplied) {
4405
+ const initial = this.initialQuery();
4406
+ if (initial) {
4407
+ this.initialQueryApplied = true;
4408
+ this.service.fromRxDBQuery(initial);
4409
+ }
4410
+ }
4411
+ });
4412
+ // 监听查询变化并发出事件
4413
+ effect(() => {
4414
+ const group = this.rootGroup();
4415
+ if (group) {
4416
+ const query = this.service.toRxDBQuery();
4417
+ // 如果没有规则,返回 undefined 而不是空的 RuleGroup
4418
+ if (!query.rules || query.rules.length === 0) {
4419
+ this.queryChange.emit(undefined);
4420
+ }
4421
+ else {
4422
+ this.queryChange.emit(query);
4423
+ }
4424
+ }
4425
+ });
4426
+ // 监听验证变化并发出事件
4427
+ effect(() => {
4428
+ const validation = this.validationSignal();
4429
+ if (validation) {
4430
+ this.validationChange.emit(validation);
4431
+ }
4432
+ });
4433
+ // 同步 maxDepth 到 service
4434
+ effect(() => {
4435
+ this.service.setMaxNestingLevel(this.maxDepth());
4436
+ });
4437
+ }
4438
+ /** 销毁服务 */
4439
+ ngOnDestroy() {
4440
+ this.service.destroy();
4441
+ }
4442
+ /** 添加第一条规则 */
4443
+ handleAddFirstRule() {
4444
+ const firstField = this.fields()[0];
4445
+ if (firstField) {
4446
+ this.service.addRule(undefined, {
4447
+ field: firstField.name,
4448
+ operator: firstField.isRelation ? 'exists'
4449
+ : firstField.type === 'keyValue' ? 'null'
4450
+ : '=',
4451
+ value: firstField.type === 'boolean' ? false : ''
4452
+ });
4453
+ }
4454
+ }
4455
+ /** 清空所有规则 */
4456
+ handleClear() {
4457
+ this.service.clear();
4458
+ }
4459
+ /** 添加规则到指定组 */
4460
+ onAddRule(event) {
4461
+ this.service.addRule(event.parentId, event.rule);
4462
+ }
4463
+ /** 添加子组到指定组 */
4464
+ onAddGroup(parentId) {
4465
+ this.service.addGroup(parentId);
4466
+ }
4467
+ /** 删除规则或组 */
4468
+ onRemoveItem(id) {
4469
+ this.service.remove(id);
4470
+ }
4471
+ /** 更新规则 */
4472
+ onUpdateRule(event) {
4473
+ this.service.updateRule(event.id, event.updates);
4474
+ }
4475
+ /** 更新组合器 */
4476
+ onUpdateCombinator(event) {
4477
+ this.service.updateGroupCombinator(event.id, event.combinator);
4478
+ }
4479
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: SubqueryBuilderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4480
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: SubqueryBuilderComponent, isStandalone: true, selector: "rxdb-subquery-builder", inputs: { fields: { classPropertyName: "fields", publicName: "fields", isSignal: true, isRequired: true, transformFunction: null }, initialQuery: { classPropertyName: "initialQuery", publicName: "initialQuery", isSignal: true, isRequired: false, transformFunction: null }, maxDepth: { classPropertyName: "maxDepth", publicName: "maxDepth", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { queryChange: "queryChange", validationChange: "validationChange" }, ngImport: i0, template: `
4481
+ <div class="subquery-builder border-base-300 bg-base-200/50 p-2">
4482
+ @if (!hasRules()) {
4483
+ <div class="text-base-content/60 flex items-center gap-2">
4484
+ <button class="btn btn-ghost btn-xs" (click)="handleAddFirstRule()" type="button">+ 添加子条件</button>
4485
+ <span class="text-xs opacity-50">(可选)</span>
4486
+ </div>
4487
+ } @else {
4488
+ <rxdb-query-group
4489
+ [depth]="0"
4490
+ [fields]="fields()"
4491
+ [group]="rootGroup()!"
4492
+ [maxDepth]="maxDepth()"
4493
+ (addGroup)="onAddGroup($event)"
4494
+ (addRule)="onAddRule($event)"
4495
+ (removeItem)="onRemoveItem($event)"
4496
+ (updateCombinator)="onUpdateCombinator($event)"
4497
+ (updateRule)="onUpdateRule($event)"
4498
+ />
4499
+ <div class="mt-2 flex justify-end">
4500
+ <button class="btn btn-ghost btn-xs text-error" (click)="handleClear()" type="button">清空</button>
4501
+ </div>
4502
+ }
4503
+ </div>
4504
+ `, isInline: true, styles: ["rxdb-query-group ::ng-deep>.rxdb-query-group{border:0;padding:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: QueryGroupComponent, selector: "rxdb-query-group", inputs: ["group", "fields", "errors", "depth", "maxDepth", "dragDropHandler", "allowCollapse"], outputs: ["addRule", "addGroup", "removeItem", "updateRule", "updateCombinator"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4505
+ }
4506
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: SubqueryBuilderComponent, decorators: [{
4507
+ type: Component,
4508
+ args: [{ selector: 'rxdb-subquery-builder', standalone: true, imports: [CommonModule, QueryGroupComponent], template: `
4509
+ <div class="subquery-builder border-base-300 bg-base-200/50 p-2">
4510
+ @if (!hasRules()) {
4511
+ <div class="text-base-content/60 flex items-center gap-2">
4512
+ <button class="btn btn-ghost btn-xs" (click)="handleAddFirstRule()" type="button">+ 添加子条件</button>
4513
+ <span class="text-xs opacity-50">(可选)</span>
4514
+ </div>
4515
+ } @else {
4516
+ <rxdb-query-group
4517
+ [depth]="0"
4518
+ [fields]="fields()"
4519
+ [group]="rootGroup()!"
4520
+ [maxDepth]="maxDepth()"
4521
+ (addGroup)="onAddGroup($event)"
4522
+ (addRule)="onAddRule($event)"
4523
+ (removeItem)="onRemoveItem($event)"
4524
+ (updateCombinator)="onUpdateCombinator($event)"
4525
+ (updateRule)="onUpdateRule($event)"
4526
+ />
4527
+ <div class="mt-2 flex justify-end">
4528
+ <button class="btn btn-ghost btn-xs text-error" (click)="handleClear()" type="button">清空</button>
4529
+ </div>
4530
+ }
4531
+ </div>
4532
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: ["rxdb-query-group ::ng-deep>.rxdb-query-group{border:0;padding:0}\n"] }]
4533
+ }], ctorParameters: () => [], propDecorators: { fields: [{ type: i0.Input, args: [{ isSignal: true, alias: "fields", required: true }] }], initialQuery: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialQuery", required: false }] }], maxDepth: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDepth", required: false }] }], queryChange: [{ type: i0.Output, args: ["queryChange"] }], validationChange: [{ type: i0.Output, args: ["validationChange"] }] } });
4534
+
4535
+ var subqueryBuilder_component = /*#__PURE__*/Object.freeze({
4536
+ __proto__: null,
4537
+ SubqueryBuilderComponent: SubqueryBuilderComponent
4538
+ });
4539
+
4540
+ /**
4541
+ * @aiao/rxdb-model-angular
4542
+ *
4543
+ * Angular 查询构建器组件库
4544
+ *
4545
+ * @description
4546
+ * 提供可视化的 RxDB 查询条件构建组件,支持:
4547
+ * - 从 EntityMetadata 自动解析字段
4548
+ * - 支持多种数据类型和操作符
4549
+ * - AND/OR 条件组合
4550
+ * - 最大 5 层嵌套
4551
+ * - 实时验证
4552
+ *
4553
+ * @example
4554
+ * ```typescript
4555
+ * import { QueryBuilderComponent } from '@aiao/rxdb-model-angular';
4556
+ *
4557
+ * @Component({
4558
+ * imports: [QueryBuilderComponent],
4559
+ * template: `
4560
+ * <rxdb-query-builder
4561
+ * [entity]="userEntity"
4562
+ * (queryChange)="onQueryChange($event)"
4563
+ * />
4564
+ * `
4565
+ * })
4566
+ * export class MyComponent {
4567
+ * userEntity = inject(UserEntity);
4568
+ *
4569
+ * onQueryChange(query: RuleGroup<User>) {
4570
+ * console.log('Query changed:', query);
4571
+ * }
4572
+ * }
4573
+ * ```
4574
+ */
4575
+ // 主组件
4576
+
4577
+ // Entity Detail Angular
4578
+
4579
+ /**
4580
+ * Generated bundle index. Do not edit.
4581
+ */
4582
+
4583
+ export { DEFAULT_QUERY_BUILDER_THEME, ENTITY_TABLE_CONFIG, EntityDetailComponent, EntityDialogComponent, EntityFormComponent, EntityListComponent, EntityTableComponent, FieldSelectorComponent, OperatorSelectorComponent, PopoverSelectComponent, QUERY_BUILDER_THEME, QueryBuilderComponent, QueryGroupComponent, QueryRuleComponent, QueryTableComponent, SubqueryBuilderComponent, TreeItemDirective, TreeSelectComponent, ValueInputComponent, provideQueryBuilderTheme };
4584
+ //# sourceMappingURL=aiao-rxdb-model-angular.mjs.map