@svadmin/ui 0.63.0 → 0.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/dist/components/ActivityFeed.svelte +135 -0
  2. package/dist/components/ActivityFeed.svelte.d.ts +21 -0
  3. package/dist/components/ApprovalActionCard.svelte +248 -0
  4. package/dist/components/ApprovalActionCard.svelte.d.ts +22 -0
  5. package/dist/components/AutoTable.svelte +8 -0
  6. package/dist/components/AutoTable.svelte.d.ts +7 -1
  7. package/dist/components/CanvasAnnotation.svelte +389 -0
  8. package/dist/components/CanvasAnnotation.svelte.d.ts +17 -0
  9. package/dist/components/Cascader.svelte +205 -0
  10. package/dist/components/Cascader.svelte.d.ts +21 -0
  11. package/dist/components/ColumnHeaderFilter.svelte +178 -0
  12. package/dist/components/ColumnHeaderFilter.svelte.d.ts +12 -0
  13. package/dist/components/ColumnSettings.svelte +198 -0
  14. package/dist/components/ColumnSettings.svelte.d.ts +19 -0
  15. package/dist/components/DecisionTable.svelte +240 -0
  16. package/dist/components/DecisionTable.svelte.d.ts +22 -0
  17. package/dist/components/DraggableRowTable.svelte +117 -0
  18. package/dist/components/DraggableRowTable.svelte.d.ts +15 -0
  19. package/dist/components/DrawerForm.svelte +110 -26
  20. package/dist/components/DrawerForm.svelte.d.ts +20 -8
  21. package/dist/components/DynamicFormList.svelte +214 -0
  22. package/dist/components/DynamicFormList.svelte.d.ts +62 -0
  23. package/dist/components/EditableTable.svelte +195 -0
  24. package/dist/components/EditableTable.svelte.d.ts +25 -0
  25. package/dist/components/FieldRenderer.svelte +32 -0
  26. package/dist/components/FilterBuilder.svelte +310 -0
  27. package/dist/components/FilterBuilder.svelte.d.ts +23 -0
  28. package/dist/components/GanttChart.svelte +144 -0
  29. package/dist/components/GanttChart.svelte.d.ts +20 -0
  30. package/dist/components/ImageCropper.svelte +122 -0
  31. package/dist/components/ImageCropper.svelte.d.ts +12 -0
  32. package/dist/components/ImportWizard.svelte +336 -0
  33. package/dist/components/ImportWizard.svelte.d.ts +12 -0
  34. package/dist/components/JsonSchemaForm.svelte +149 -0
  35. package/dist/components/JsonSchemaForm.svelte.d.ts +10 -0
  36. package/dist/components/KanbanBoard.svelte +212 -0
  37. package/dist/components/KanbanBoard.svelte.d.ts +30 -0
  38. package/dist/components/MasterDetailView.svelte +134 -0
  39. package/dist/components/MasterDetailView.svelte.d.ts +16 -0
  40. package/dist/components/MediaLibraryModal.svelte +195 -0
  41. package/dist/components/MediaLibraryModal.svelte.d.ts +22 -0
  42. package/dist/components/MentionsInput.svelte +174 -0
  43. package/dist/components/MentionsInput.svelte.d.ts +21 -0
  44. package/dist/components/ModalForm.svelte +113 -28
  45. package/dist/components/ModalForm.svelte.d.ts +20 -8
  46. package/dist/components/MultiTabKeepAlive.svelte +143 -0
  47. package/dist/components/MultiTabKeepAlive.svelte.d.ts +21 -0
  48. package/dist/components/OfflineSyncBanner.svelte +172 -0
  49. package/dist/components/OfflineSyncBanner.svelte.d.ts +19 -0
  50. package/dist/components/PdfDocumentViewer.svelte +255 -0
  51. package/dist/components/PdfDocumentViewer.svelte.d.ts +22 -0
  52. package/dist/components/PivotTable.svelte +140 -0
  53. package/dist/components/PivotTable.svelte.d.ts +16 -0
  54. package/dist/components/PresenceAvatarGroup.svelte +89 -0
  55. package/dist/components/PresenceAvatarGroup.svelte.d.ts +15 -0
  56. package/dist/components/PrintableBill.svelte +149 -0
  57. package/dist/components/PrintableBill.svelte.d.ts +24 -0
  58. package/dist/components/SensitiveDataMask.svelte +125 -0
  59. package/dist/components/SensitiveDataMask.svelte.d.ts +13 -0
  60. package/dist/components/SignaturePad.svelte +264 -0
  61. package/dist/components/SignaturePad.svelte.d.ts +17 -0
  62. package/dist/components/SplitPaneLayout.svelte +113 -0
  63. package/dist/components/SplitPaneLayout.svelte.d.ts +13 -0
  64. package/dist/components/SpreadsheetView.svelte +363 -0
  65. package/dist/components/SpreadsheetView.svelte.d.ts +18 -0
  66. package/dist/components/StepForm.svelte +180 -0
  67. package/dist/components/StepForm.svelte.d.ts +23 -0
  68. package/dist/components/TableSummary.svelte +110 -0
  69. package/dist/components/TableSummary.svelte.d.ts +20 -0
  70. package/dist/components/Transfer.svelte +352 -0
  71. package/dist/components/Transfer.svelte.d.ts +28 -0
  72. package/dist/components/TreeSelect.svelte +307 -0
  73. package/dist/components/TreeSelect.svelte.d.ts +22 -0
  74. package/dist/components/TreeTable.svelte +237 -0
  75. package/dist/components/TreeTable.svelte.d.ts +28 -0
  76. package/dist/components/UnsavedChangesPrompt.svelte +44 -0
  77. package/dist/components/UnsavedChangesPrompt.svelte.d.ts +7 -0
  78. package/dist/components/VersionDiffViewer.svelte +196 -0
  79. package/dist/components/VersionDiffViewer.svelte.d.ts +12 -0
  80. package/dist/components/VirtualTable.svelte +86 -54
  81. package/dist/components/VirtualTable.svelte.d.ts +14 -21
  82. package/dist/components/Watermark.svelte +166 -0
  83. package/dist/components/Watermark.svelte.d.ts +26 -0
  84. package/dist/components/ui/navigation-menu/index.d.ts +11 -0
  85. package/dist/components/ui/navigation-menu/index.js +13 -0
  86. package/dist/components/ui/navigation-menu/navigation-menu-content.svelte +21 -0
  87. package/dist/components/ui/navigation-menu/navigation-menu-content.svelte.d.ts +4 -0
  88. package/dist/components/ui/navigation-menu/navigation-menu-indicator.svelte +22 -0
  89. package/dist/components/ui/navigation-menu/navigation-menu-indicator.svelte.d.ts +4 -0
  90. package/dist/components/ui/navigation-menu/navigation-menu-item.svelte +17 -0
  91. package/dist/components/ui/navigation-menu/navigation-menu-item.svelte.d.ts +4 -0
  92. package/dist/components/ui/navigation-menu/navigation-menu-link.svelte +20 -0
  93. package/dist/components/ui/navigation-menu/navigation-menu-link.svelte.d.ts +4 -0
  94. package/dist/components/ui/navigation-menu/navigation-menu-list.svelte +17 -0
  95. package/dist/components/ui/navigation-menu/navigation-menu-list.svelte.d.ts +4 -0
  96. package/dist/components/ui/navigation-menu/navigation-menu-trigger.svelte +28 -0
  97. package/dist/components/ui/navigation-menu/navigation-menu-trigger.svelte.d.ts +4 -0
  98. package/dist/components/ui/navigation-menu/navigation-menu-viewport.svelte +22 -0
  99. package/dist/components/ui/navigation-menu/navigation-menu-viewport.svelte.d.ts +4 -0
  100. package/dist/components/ui/navigation-menu/navigation-menu.svelte +24 -0
  101. package/dist/components/ui/navigation-menu/navigation-menu.svelte.d.ts +4 -0
  102. package/dist/index.d.ts +68 -3
  103. package/dist/index.js +42 -3
  104. package/package.json +2 -2
@@ -0,0 +1,195 @@
1
+ <script lang="ts">
2
+ import { Button } from './ui/button/index.js';
3
+ import { Plus, Trash2, Save, Loader2 } from '@lucide/svelte';
4
+ import { cn } from '../utils.js';
5
+
6
+ export interface EditableTableColumn {
7
+ key: string;
8
+ label: string;
9
+ type?: 'text' | 'number' | 'select' | 'boolean' | 'date';
10
+ options?: Array<{ label: string; value: string | number }>;
11
+ required?: boolean;
12
+ width?: string;
13
+ readonly?: boolean;
14
+ }
15
+
16
+ interface Props {
17
+ columns: EditableTableColumn[];
18
+ data?: Record<string, unknown>[];
19
+ rowKey?: string;
20
+ allowAdd?: boolean;
21
+ allowDelete?: boolean;
22
+ onchange?: (data: Record<string, unknown>[]) => void;
23
+ onsave?: (data: Record<string, unknown>[]) => void | Promise<void>;
24
+ class?: string;
25
+ }
26
+
27
+ let {
28
+ columns,
29
+ data = $bindable([]),
30
+ rowKey = 'id',
31
+ allowAdd = true,
32
+ allowDelete = true,
33
+ onchange,
34
+ onsave,
35
+ class: className = '',
36
+ }: Props = $props();
37
+
38
+ let isSaving = $state(false);
39
+
40
+ function updateCell(rowIndex: number, columnKey: string, value: unknown) {
41
+ const nextData = [...data];
42
+ nextData[rowIndex] = {
43
+ ...nextData[rowIndex],
44
+ [columnKey]: value,
45
+ };
46
+ data = nextData;
47
+ onchange?.(data);
48
+ }
49
+
50
+ function handleAddRow() {
51
+ const newRow: Record<string, unknown> = {
52
+ [rowKey]: `row_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
53
+ };
54
+ for (const col of columns) {
55
+ if (col.type === 'number') newRow[col.key] = 0;
56
+ else if (col.type === 'boolean') newRow[col.key] = false;
57
+ else newRow[col.key] = '';
58
+ }
59
+ data = [...data, newRow];
60
+ onchange?.(data);
61
+ }
62
+
63
+ function handleDeleteRow(index: number) {
64
+ data = data.filter((_, idx) => idx !== index);
65
+ onchange?.(data);
66
+ }
67
+
68
+ async function handleSave() {
69
+ isSaving = true;
70
+ try {
71
+ await onsave?.(data);
72
+ } finally {
73
+ isSaving = false;
74
+ }
75
+ }
76
+ </script>
77
+
78
+ <div class={cn('space-y-3 rounded-xl border border-border bg-card p-4 shadow-xs text-xs', className)}>
79
+ <div class="flex items-center justify-between gap-2 pb-2 border-b border-border/60">
80
+ <div class="font-semibold text-foreground">
81
+ Editable Grid <span class="text-muted-foreground font-normal">({data.length} records)</span>
82
+ </div>
83
+
84
+ <div class="flex items-center gap-2">
85
+ {#if allowAdd}
86
+ <Button variant="outline" size="sm" class="h-8 text-xs gap-1" onclick={handleAddRow}>
87
+ <Plus class="h-3.5 w-3.5" />
88
+ Add Row
89
+ </Button>
90
+ {/if}
91
+ {#if onsave}
92
+ <Button size="sm" class="h-8 text-xs gap-1 min-w-20" disabled={isSaving} onclick={handleSave}>
93
+ {#if isSaving}
94
+ <Loader2 class="h-3.5 w-3.5 animate-spin" />
95
+ {:else}
96
+ <Save class="h-3.5 w-3.5" />
97
+ {/if}
98
+ Save All
99
+ </Button>
100
+ {/if}
101
+ </div>
102
+ </div>
103
+
104
+ <div class="overflow-x-auto rounded-lg border border-border/60">
105
+ <table class="w-full text-left border-collapse">
106
+ <thead class="bg-muted/40 font-semibold text-muted-foreground border-b border-border/60">
107
+ <tr>
108
+ {#each columns as col (col.key)}
109
+ <th style={col.width ? `width: ${col.width};` : ''} class="px-3 py-2.5">
110
+ {col.label}
111
+ {#if col.required}<span class="text-destructive">*</span>{/if}
112
+ </th>
113
+ {/each}
114
+ {#if allowDelete}
115
+ <th class="w-12 px-3 py-2.5 text-center">Action</th>
116
+ {/if}
117
+ </tr>
118
+ </thead>
119
+ <tbody class="divide-y divide-border/40">
120
+ {#each data as row, rowIndex (row[rowKey] ?? rowIndex)}
121
+ <tr class="hover:bg-muted/20 transition-colors">
122
+ {#each columns as col (col.key)}
123
+ <td class="p-1.5">
124
+ {#if col.readonly}
125
+ <div class="px-2 py-1 text-muted-foreground font-mono truncate">{row[col.key] ?? '—'}</div>
126
+ {:else if col.type === 'select' && col.options}
127
+ <select
128
+ class="h-7.5 w-full rounded-md border border-input bg-background px-2 text-xs focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
129
+ value={String(row[col.key] ?? '')}
130
+ onchange={(e) => updateCell(rowIndex, col.key, e.currentTarget.value)}
131
+ >
132
+ {#each col.options as opt (opt.value)}
133
+ <option value={String(opt.value)}>{opt.label}</option>
134
+ {/each}
135
+ </select>
136
+ {:else if col.type === 'boolean'}
137
+ <div class="flex items-center justify-center py-1">
138
+ <input
139
+ type="checkbox"
140
+ class="h-4 w-4 rounded border-border text-primary focus:ring-primary/20 cursor-pointer"
141
+ checked={Boolean(row[col.key])}
142
+ onchange={(e) => updateCell(rowIndex, col.key, e.currentTarget.checked)}
143
+ />
144
+ </div>
145
+ {:else if col.type === 'number'}
146
+ <input
147
+ type="number"
148
+ class="h-7.5 w-full rounded-md border border-input bg-background px-2 text-xs focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
149
+ value={Number(row[col.key] ?? 0)}
150
+ oninput={(e) => updateCell(rowIndex, col.key, Number(e.currentTarget.value))}
151
+ />
152
+ {:else if col.type === 'date'}
153
+ <input
154
+ type="date"
155
+ class="h-7.5 w-full rounded-md border border-input bg-background px-2 text-xs focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
156
+ value={String(row[col.key] ?? '')}
157
+ oninput={(e) => updateCell(rowIndex, col.key, e.currentTarget.value)}
158
+ />
159
+ {:else}
160
+ <input
161
+ type="text"
162
+ class="h-7.5 w-full rounded-md border border-input bg-background px-2 text-xs focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
163
+ value={String(row[col.key] ?? '')}
164
+ oninput={(e) => updateCell(rowIndex, col.key, e.currentTarget.value)}
165
+ />
166
+ {/if}
167
+ </td>
168
+ {/each}
169
+
170
+ {#if allowDelete}
171
+ <td class="p-1.5 text-center">
172
+ <Button
173
+ variant="ghost"
174
+ size="sm"
175
+ class="h-7 w-7 p-0 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
176
+ onclick={() => handleDeleteRow(rowIndex)}
177
+ >
178
+ <Trash2 class="h-3.5 w-3.5" />
179
+ </Button>
180
+ </td>
181
+ {/if}
182
+ </tr>
183
+ {/each}
184
+
185
+ {#if data.length === 0}
186
+ <tr>
187
+ <td colspan={columns.length + (allowDelete ? 1 : 0)} class="py-8 text-center text-muted-foreground">
188
+ No rows. Click "Add Row" to create the first record.
189
+ </td>
190
+ </tr>
191
+ {/if}
192
+ </tbody>
193
+ </table>
194
+ </div>
195
+ </div>
@@ -0,0 +1,25 @@
1
+ export interface EditableTableColumn {
2
+ key: string;
3
+ label: string;
4
+ type?: 'text' | 'number' | 'select' | 'boolean' | 'date';
5
+ options?: Array<{
6
+ label: string;
7
+ value: string | number;
8
+ }>;
9
+ required?: boolean;
10
+ width?: string;
11
+ readonly?: boolean;
12
+ }
13
+ interface Props {
14
+ columns: EditableTableColumn[];
15
+ data?: Record<string, unknown>[];
16
+ rowKey?: string;
17
+ allowAdd?: boolean;
18
+ allowDelete?: boolean;
19
+ onchange?: (data: Record<string, unknown>[]) => void;
20
+ onsave?: (data: Record<string, unknown>[]) => void | Promise<void>;
21
+ class?: string;
22
+ }
23
+ declare const EditableTable: import("svelte").Component<Props, {}, "data">;
24
+ type EditableTable = ReturnType<typeof EditableTable>;
25
+ export default EditableTable;
@@ -16,6 +16,9 @@
16
16
  import { Button } from './ui/button/index.js';
17
17
  import TooltipButton from './TooltipButton.svelte';
18
18
  import ComboboxField from './ComboboxField.svelte';
19
+ import TreeSelect, { type TreeSelectOption } from './TreeSelect.svelte';
20
+ import Cascader, { type CascaderOption } from './Cascader.svelte';
21
+ import Transfer, { type TransferItem } from './Transfer.svelte';
19
22
  import ArrayField from './fields/ArrayField.svelte';
20
23
  import MediaThumbnail from './content/MediaThumbnail.svelte';
21
24
  import { Plus, X } from '@lucide/svelte';
@@ -263,6 +266,35 @@
263
266
  class="resize-y"
264
267
  />
265
268
 
269
+ {:else if field.type === 'tree-select' || field.type === 'treeselect'}
270
+ <TreeSelect
271
+ options={(field.treeOptions ?? field.options ?? []) as TreeSelectOption[]}
272
+ value={value as string | number | (string | number)[] | undefined}
273
+ multiple={field.multiple}
274
+ disabled={disabled}
275
+ placeholder={i18n.t('field.selectPlaceholder')}
276
+ onchange={(v) => onchange(v)}
277
+ />
278
+
279
+ {:else if field.type === 'cascader'}
280
+ <Cascader
281
+ options={(field.cascaderOptions ?? field.options ?? []) as CascaderOption[]}
282
+ value={value as (string | number)[]}
283
+ separator={field.separator}
284
+ changeOnSelect={field.changeOnSelect}
285
+ disabled={disabled}
286
+ placeholder={i18n.t('field.selectPlaceholder')}
287
+ onchange={(v) => onchange(v)}
288
+ />
289
+
290
+ {:else if field.type === 'transfer'}
291
+ <Transfer
292
+ dataSource={(field.transferData ?? field.options?.map((o: { label: string; value: string | number }) => ({ key: o.value, title: o.label })) ?? []) as TransferItem[]}
293
+ targetKeys={(value as (string | number)[]) ?? []}
294
+ disabled={disabled}
295
+ onchange={(next) => onchange(next)}
296
+ />
297
+
266
298
  {:else if field.type === 'relation' && field.resource}
267
299
  <input type="hidden" name={field.key} value={value == null ? '' : String(value)} />
268
300
  <ComboboxField
@@ -0,0 +1,310 @@
1
+ <script lang="ts">
2
+ import { Plus, Trash2, RotateCcw, Filter as FilterIcon, Check } from '@lucide/svelte';
3
+ import { cn } from '../utils.js';
4
+ import { Button } from './ui/button/index.js';
5
+ import { Input } from './ui/input/index.js';
6
+ import { Select } from './ui/select/index.js';
7
+ import { useTranslation } from '@svadmin/core/i18n';
8
+ import type { CrudOperator, FieldDefinition, FieldFilter, Filter, LogicalFilter } from '@svadmin/core';
9
+
10
+ export interface FilterRuleItem {
11
+ id: string;
12
+ field: string;
13
+ operator: CrudOperator;
14
+ value: unknown;
15
+ }
16
+
17
+ const i18n = useTranslation();
18
+
19
+ interface Props {
20
+ fields?: FieldDefinition[];
21
+ filters?: Filter[];
22
+ logicalOperator?: 'and' | 'or';
23
+ class?: string;
24
+ onApply?: (filters: Filter[]) => void;
25
+ onReset?: () => void;
26
+ }
27
+
28
+ let {
29
+ fields = [],
30
+ filters = $bindable([]),
31
+ logicalOperator = $bindable('and'),
32
+ class: className,
33
+ onApply,
34
+ onReset,
35
+ }: Props = $props();
36
+
37
+ const operatorOptions: { value: CrudOperator; label: string }[] = [
38
+ { value: 'eq', label: '等于 (eq)' },
39
+ { value: 'ne', label: '不等于 (ne)' },
40
+ { value: 'contains', label: '包含 (contains)' },
41
+ { value: 'ncontains', label: '不包含 (ncontains)' },
42
+ { value: 'gt', label: '大于 (>)' },
43
+ { value: 'gte', label: '大于等于 (>=)' },
44
+ { value: 'lt', label: '小于 (<)' },
45
+ { value: 'lte', label: '小于等于 (<=)' },
46
+ { value: 'null', label: '为空 (null)' },
47
+ { value: 'nnull', label: '非空 (not null)' },
48
+ ];
49
+
50
+ let rules = $state<FilterRuleItem[]>([]);
51
+
52
+ // Initialize rules from filters prop
53
+ $effect(() => {
54
+ if (filters && filters.length > 0) {
55
+ const parsedRules: FilterRuleItem[] = [];
56
+ let nextLogical: 'and' | 'or' = 'and';
57
+
58
+ for (let i = 0; i < filters.length; i++) {
59
+ const item = filters[i];
60
+ if ('field' in item) {
61
+ parsedRules.push({
62
+ id: `rule-${i}-${Date.now()}`,
63
+ field: item.field,
64
+ operator: item.operator,
65
+ value: item.value,
66
+ });
67
+ } else if (item.operator === 'and' || item.operator === 'or') {
68
+ nextLogical = item.operator;
69
+ for (let j = 0; j < item.value.length; j++) {
70
+ const sub = item.value[j];
71
+ if ('field' in sub) {
72
+ parsedRules.push({
73
+ id: `rule-${i}-${j}-${Date.now()}`,
74
+ field: sub.field,
75
+ operator: sub.operator,
76
+ value: sub.value,
77
+ });
78
+ }
79
+ }
80
+ }
81
+ }
82
+ rules = parsedRules;
83
+ logicalOperator = nextLogical;
84
+ } else {
85
+ rules = [];
86
+ }
87
+ });
88
+
89
+ const availableFields = $derived(
90
+ fields.filter((f) => f.filterable !== false)
91
+ );
92
+
93
+ export function addRule() {
94
+ const firstField = availableFields[0]?.key ?? 'id';
95
+ const firstFieldDef = availableFields.find((f) => f.key === firstField);
96
+ const defaultOp: CrudOperator = firstFieldDef?.type === 'number' ? 'eq' : 'contains';
97
+
98
+ rules = [
99
+ ...rules,
100
+ {
101
+ id: `rule-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
102
+ field: firstField,
103
+ operator: defaultOp,
104
+ value: '',
105
+ },
106
+ ];
107
+ }
108
+
109
+ export function removeRule(index: number) {
110
+ rules = rules.filter((_, i) => i !== index);
111
+ }
112
+
113
+ export function reset() {
114
+ rules = [];
115
+ filters = [];
116
+ onReset?.();
117
+ }
118
+
119
+ export function apply() {
120
+ const validFieldFilters: FieldFilter[] = rules
121
+ .filter((r) => r.field && (r.operator === 'null' || r.operator === 'nnull' || (r.value !== '' && r.value !== undefined)))
122
+ .map((r) => ({
123
+ field: r.field,
124
+ operator: r.operator,
125
+ value: r.operator === 'null' || r.operator === 'nnull' ? null : r.value,
126
+ }));
127
+
128
+ let compiledFilters: Filter[] = [];
129
+ if (validFieldFilters.length === 1 || logicalOperator === 'and') {
130
+ compiledFilters = validFieldFilters;
131
+ } else if (validFieldFilters.length > 1 && logicalOperator === 'or') {
132
+ const logical: LogicalFilter = {
133
+ operator: 'or',
134
+ value: validFieldFilters,
135
+ };
136
+ compiledFilters = [logical];
137
+ }
138
+
139
+ filters = compiledFilters;
140
+ onApply?.(compiledFilters);
141
+ }
142
+ </script>
143
+
144
+ <div class={cn('w-full rounded-lg border border-border bg-card p-4 shadow-xs space-y-3', className)} data-testid="filter-builder">
145
+ <div class="flex items-center justify-between gap-2 border-b border-border/60 pb-3">
146
+ <div class="flex items-center gap-2">
147
+ <FilterIcon class="h-4 w-4 text-muted-foreground" />
148
+ <span class="text-sm font-medium text-foreground">
149
+ {i18n.t('common.filterBuilder', undefined) ?? '高级筛选'}
150
+ </span>
151
+ {#if rules.length > 1}
152
+ <div class="flex items-center rounded-md border border-border bg-muted/40 p-0.5 text-xs">
153
+ <button
154
+ type="button"
155
+ class={cn(
156
+ 'rounded px-2 py-0.5 text-xs transition-colors',
157
+ logicalOperator === 'and' ? 'bg-background font-semibold text-foreground shadow-xs' : 'text-muted-foreground hover:text-foreground'
158
+ )}
159
+ onclick={() => (logicalOperator = 'and')}
160
+ >
161
+ AND (且)
162
+ </button>
163
+ <button
164
+ type="button"
165
+ class={cn(
166
+ 'rounded px-2 py-0.5 text-xs transition-colors',
167
+ logicalOperator === 'or' ? 'bg-background font-semibold text-foreground shadow-xs' : 'text-muted-foreground hover:text-foreground'
168
+ )}
169
+ onclick={() => (logicalOperator = 'or')}
170
+ >
171
+ OR (或)
172
+ </button>
173
+ </div>
174
+ {/if}
175
+ </div>
176
+
177
+ <div class="flex items-center gap-1.5">
178
+ <Button
179
+ variant="ghost"
180
+ size="sm"
181
+ class="h-8 gap-1 text-xs"
182
+ data-testid="filter-builder-reset"
183
+ onclick={reset}
184
+ >
185
+ <RotateCcw class="h-3.5 w-3.5" />
186
+ {i18n.t('common.reset', undefined) ?? '重置'}
187
+ </Button>
188
+ <Button
189
+ variant="default"
190
+ size="sm"
191
+ class="h-8 gap-1 text-xs"
192
+ data-testid="filter-builder-apply"
193
+ onclick={apply}
194
+ >
195
+ <Check class="h-3.5 w-3.5" />
196
+ {i18n.t('common.confirm', undefined) ?? '应用'}
197
+ </Button>
198
+ </div>
199
+ </div>
200
+
201
+ <div class="space-y-2">
202
+ {#each rules as rule, index (rule.id)}
203
+ {@const fieldDef = availableFields.find((f) => f.key === rule.field)}
204
+ <div class="flex flex-wrap sm:flex-nowrap items-center gap-2 rounded-md bg-muted/20 p-2 border border-border/40">
205
+ <!-- Field select -->
206
+ <div class="w-36 shrink-0">
207
+ <Select
208
+ class="h-8 text-xs"
209
+ value={rule.field}
210
+ onchange={(e: Event) => {
211
+ rule.field = (e.currentTarget as HTMLSelectElement).value;
212
+ }}
213
+ >
214
+ {#each availableFields as f (f.key)}
215
+ <option value={f.key}>{f.label || f.key}</option>
216
+ {/each}
217
+ </Select>
218
+ </div>
219
+
220
+ <!-- Operator select -->
221
+ <div class="w-36 shrink-0">
222
+ <Select
223
+ class="h-8 text-xs"
224
+ value={rule.operator}
225
+ onchange={(e: Event) => {
226
+ rule.operator = (e.currentTarget as HTMLSelectElement).value as CrudOperator;
227
+ }}
228
+ >
229
+ {#each operatorOptions as op (op.value)}
230
+ <option value={op.value}>{op.label}</option>
231
+ {/each}
232
+ </Select>
233
+ </div>
234
+
235
+ <!-- Value input -->
236
+ <div class="flex-1 min-w-[120px]">
237
+ {#if rule.operator === 'null' || rule.operator === 'nnull'}
238
+ <div class="h-8 flex items-center px-3 text-xs text-muted-foreground italic bg-muted/30 rounded-md">
239
+ 无需填值
240
+ </div>
241
+ {:else if fieldDef?.type === 'select' && fieldDef.options}
242
+ <Select
243
+ class="h-8 text-xs"
244
+ value={String(rule.value ?? '')}
245
+ onchange={(e: Event) => {
246
+ rule.value = (e.currentTarget as HTMLSelectElement).value;
247
+ }}
248
+ >
249
+ <option value="">{i18n.t('field.selectPlaceholder', undefined) ?? '请选择'}</option>
250
+ {#each fieldDef.options as opt (opt.value)}
251
+ <option value={String(opt.value)}>{opt.label}</option>
252
+ {/each}
253
+ </Select>
254
+ {:else if fieldDef?.type === 'boolean'}
255
+ <Select
256
+ class="h-8 text-xs"
257
+ value={String(rule.value ?? '')}
258
+ onchange={(e: Event) => {
259
+ const val = (e.currentTarget as HTMLSelectElement).value;
260
+ rule.value = val === 'true' ? true : val === 'false' ? false : '';
261
+ }}
262
+ >
263
+ <option value="">{i18n.t('field.selectPlaceholder', undefined) ?? '请选择'}</option>
264
+ <option value="true">{i18n.t('common.yes', undefined) ?? '是 (true)'}</option>
265
+ <option value="false">{i18n.t('common.no', undefined) ?? '否 (false)'}</option>
266
+ </Select>
267
+ {:else}
268
+ <Input
269
+ type={fieldDef?.type === 'number' ? 'number' : 'text'}
270
+ class="h-8 text-xs"
271
+ placeholder="输入筛选值..."
272
+ value={String(rule.value ?? '')}
273
+ oninput={(e: Event) => {
274
+ const val = (e.currentTarget as HTMLInputElement).value;
275
+ rule.value = fieldDef?.type === 'number' && val !== '' ? Number(val) : val;
276
+ }}
277
+ />
278
+ {/if}
279
+ </div>
280
+
281
+ <!-- Remove button -->
282
+ <Button
283
+ type="button"
284
+ variant="ghost"
285
+ size="icon-sm"
286
+ class="shrink-0 text-muted-foreground hover:text-destructive"
287
+ onclick={() => removeRule(index)}
288
+ >
289
+ <Trash2 class="h-3.5 w-3.5" />
290
+ </Button>
291
+ </div>
292
+ {:else}
293
+ <div class="rounded-md border border-dashed border-border/80 py-4 text-center text-xs text-muted-foreground">
294
+ 暂无筛选条件,点击下方按钮添加规则
295
+ </div>
296
+ {/each}
297
+ </div>
298
+
299
+ <Button
300
+ type="button"
301
+ variant="outline"
302
+ size="sm"
303
+ data-testid="filter-builder-add-rule"
304
+ class="w-full gap-1.5 border-dashed text-xs h-8"
305
+ onclick={addRule}
306
+ >
307
+ <Plus class="h-3.5 w-3.5" />
308
+ {i18n.t('common.addRule', undefined) ?? '添加筛选条件'}
309
+ </Button>
310
+ </div>
@@ -0,0 +1,23 @@
1
+ import type { CrudOperator, FieldDefinition, Filter } from '@svadmin/core';
2
+ export interface FilterRuleItem {
3
+ id: string;
4
+ field: string;
5
+ operator: CrudOperator;
6
+ value: unknown;
7
+ }
8
+ interface Props {
9
+ fields?: FieldDefinition[];
10
+ filters?: Filter[];
11
+ logicalOperator?: 'and' | 'or';
12
+ class?: string;
13
+ onApply?: (filters: Filter[]) => void;
14
+ onReset?: () => void;
15
+ }
16
+ declare const FilterBuilder: import("svelte").Component<Props, {
17
+ addRule: () => void;
18
+ removeRule: (index: number) => void;
19
+ reset: () => void;
20
+ apply: () => void;
21
+ }, "filters" | "logicalOperator">;
22
+ type FilterBuilder = ReturnType<typeof FilterBuilder>;
23
+ export default FilterBuilder;