@svadmin/ui 0.60.1 → 0.64.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.
- package/dist/components/AutoTable.svelte +8 -0
- package/dist/components/AutoTable.svelte.d.ts +7 -1
- package/dist/components/Cascader.svelte +205 -0
- package/dist/components/Cascader.svelte.d.ts +21 -0
- package/dist/components/DynamicFormList.svelte +214 -0
- package/dist/components/DynamicFormList.svelte.d.ts +62 -0
- package/dist/components/FieldRenderer.svelte +32 -0
- package/dist/components/FilterBuilder.svelte +310 -0
- package/dist/components/FilterBuilder.svelte.d.ts +23 -0
- package/dist/components/InferencerPanel.svelte +66 -6
- package/dist/components/Transfer.svelte +352 -0
- package/dist/components/Transfer.svelte.d.ts +28 -0
- package/dist/components/TreeSelect.svelte +307 -0
- package/dist/components/TreeSelect.svelte.d.ts +22 -0
- package/dist/components/ui/navigation-menu/index.d.ts +11 -0
- package/dist/components/ui/navigation-menu/index.js +13 -0
- package/dist/components/ui/navigation-menu/navigation-menu-content.svelte +21 -0
- package/dist/components/ui/navigation-menu/navigation-menu-content.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu-indicator.svelte +22 -0
- package/dist/components/ui/navigation-menu/navigation-menu-indicator.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu-item.svelte +17 -0
- package/dist/components/ui/navigation-menu/navigation-menu-item.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu-link.svelte +20 -0
- package/dist/components/ui/navigation-menu/navigation-menu-link.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu-list.svelte +17 -0
- package/dist/components/ui/navigation-menu/navigation-menu-list.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu-trigger.svelte +28 -0
- package/dist/components/ui/navigation-menu/navigation-menu-trigger.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu-viewport.svelte +22 -0
- package/dist/components/ui/navigation-menu/navigation-menu-viewport.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu.svelte +24 -0
- package/dist/components/ui/navigation-menu/navigation-menu.svelte.d.ts +4 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +7 -0
- package/package.json +16 -16
|
@@ -121,6 +121,8 @@
|
|
|
121
121
|
sorters?: Sort[];
|
|
122
122
|
/** Custom batch actions to render when rows are selected */
|
|
123
123
|
batchActions?: Snippet<[{ selectedIds: (string | number)[] }]>;
|
|
124
|
+
/** Summary row rendered at table footer */
|
|
125
|
+
summary?: Snippet<[{ data: BaseRecord[]; total: number; visibleColumnsCount: number }]>;
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
let {
|
|
@@ -139,6 +141,7 @@
|
|
|
139
141
|
emptyState,
|
|
140
142
|
expandedRowRender,
|
|
141
143
|
batchActions,
|
|
144
|
+
summary,
|
|
142
145
|
pagination: externalPagination,
|
|
143
146
|
sorters: externalSorters,
|
|
144
147
|
}: Props = $props();
|
|
@@ -1643,6 +1646,11 @@
|
|
|
1643
1646
|
</Table.Row>
|
|
1644
1647
|
{/each}
|
|
1645
1648
|
</Table.Body>
|
|
1649
|
+
{#if summary}
|
|
1650
|
+
<Table.Footer class="bg-muted/30 font-medium">
|
|
1651
|
+
{@render summary({ data: (query.data?.data as BaseRecord[]) ?? [], total: query.data?.total ?? 0, visibleColumnsCount: columns.filter((c) => c.id != null && isColumnVisible(c.id)).length })}
|
|
1652
|
+
</Table.Footer>
|
|
1653
|
+
{/if}
|
|
1646
1654
|
</Table.Root>
|
|
1647
1655
|
</div>
|
|
1648
1656
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FieldDefinition, Sort } from '@svadmin/core';
|
|
1
|
+
import type { BaseRecord, FieldDefinition, Sort } from '@svadmin/core';
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
interface Props {
|
|
4
4
|
resourceName: string;
|
|
@@ -44,6 +44,12 @@ interface Props {
|
|
|
44
44
|
batchActions?: Snippet<[{
|
|
45
45
|
selectedIds: (string | number)[];
|
|
46
46
|
}]>;
|
|
47
|
+
/** Summary row rendered at table footer */
|
|
48
|
+
summary?: Snippet<[{
|
|
49
|
+
data: BaseRecord[];
|
|
50
|
+
total: number;
|
|
51
|
+
visibleColumnsCount: number;
|
|
52
|
+
}]>;
|
|
47
53
|
}
|
|
48
54
|
declare const AutoTable: import("svelte").Component<Props, {}, "">;
|
|
49
55
|
type AutoTable = ReturnType<typeof AutoTable>;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ChevronRight, Check, X, ChevronsUpDown } from '@lucide/svelte';
|
|
3
|
+
import { cn } from '../utils.js';
|
|
4
|
+
import * as Popover from './ui/popover/index.js';
|
|
5
|
+
import { useTranslation } from '@svadmin/core/i18n';
|
|
6
|
+
|
|
7
|
+
export interface CascaderOption {
|
|
8
|
+
value: string | number;
|
|
9
|
+
label: string;
|
|
10
|
+
children?: CascaderOption[];
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
isLeaf?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const i18n = useTranslation();
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
options?: CascaderOption[];
|
|
19
|
+
value?: (string | number)[];
|
|
20
|
+
separator?: string;
|
|
21
|
+
placeholder?: string;
|
|
22
|
+
changeOnSelect?: boolean;
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
allowClear?: boolean;
|
|
25
|
+
class?: string;
|
|
26
|
+
onchange?: (value: (string | number)[] | undefined, selectedOptions: CascaderOption[]) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let {
|
|
30
|
+
options = [],
|
|
31
|
+
value = $bindable(undefined),
|
|
32
|
+
separator = ' / ',
|
|
33
|
+
placeholder,
|
|
34
|
+
changeOnSelect = false,
|
|
35
|
+
disabled = false,
|
|
36
|
+
allowClear = true,
|
|
37
|
+
class: className,
|
|
38
|
+
onchange,
|
|
39
|
+
}: Props = $props();
|
|
40
|
+
|
|
41
|
+
let open = $state(false);
|
|
42
|
+
// Active selected path while navigating panels
|
|
43
|
+
let activePath = $state<(string | number)[]>([]);
|
|
44
|
+
|
|
45
|
+
$effect(() => {
|
|
46
|
+
if (value && Array.isArray(value)) {
|
|
47
|
+
activePath = [...value];
|
|
48
|
+
} else {
|
|
49
|
+
activePath = [];
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Calculate columns based on activePath
|
|
54
|
+
const columns = $derived.by<CascaderOption[][]>(() => {
|
|
55
|
+
const cols: CascaderOption[][] = [options];
|
|
56
|
+
let currentOptions = options;
|
|
57
|
+
|
|
58
|
+
for (const val of activePath) {
|
|
59
|
+
const selectedOpt = currentOptions.find((o) => o.value === val);
|
|
60
|
+
if (selectedOpt && selectedOpt.children && selectedOpt.children.length > 0) {
|
|
61
|
+
cols.push(selectedOpt.children);
|
|
62
|
+
currentOptions = selectedOpt.children;
|
|
63
|
+
} else {
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return cols;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Selected options objects corresponding to value
|
|
72
|
+
const selectedOptions = $derived.by<CascaderOption[]>(() => {
|
|
73
|
+
if (!value || !Array.isArray(value) || value.length === 0) return [];
|
|
74
|
+
const opts: CascaderOption[] = [];
|
|
75
|
+
let currentLevel = options;
|
|
76
|
+
|
|
77
|
+
for (const val of value) {
|
|
78
|
+
const found = currentLevel.find((o) => o.value === val);
|
|
79
|
+
if (found) {
|
|
80
|
+
opts.push(found);
|
|
81
|
+
currentLevel = found.children ?? [];
|
|
82
|
+
} else {
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return opts;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const displayLabel = $derived.by(() => {
|
|
90
|
+
if (selectedOptions.length === 0) return '';
|
|
91
|
+
return selectedOptions.map((o) => o.label).join(separator);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
function handleSelectOption(colIndex: number, option: CascaderOption) {
|
|
95
|
+
if (option.disabled) return;
|
|
96
|
+
|
|
97
|
+
const nextPath = [...activePath.slice(0, colIndex), option.value];
|
|
98
|
+
activePath = nextPath;
|
|
99
|
+
|
|
100
|
+
const hasChildren = Boolean(option.children && option.children.length > 0 && !option.isLeaf);
|
|
101
|
+
|
|
102
|
+
if (!hasChildren || changeOnSelect) {
|
|
103
|
+
value = nextPath;
|
|
104
|
+
const currentSelected = getOptionsForPath(nextPath);
|
|
105
|
+
onchange?.(nextPath, currentSelected);
|
|
106
|
+
if (!hasChildren) {
|
|
107
|
+
open = false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getOptionsForPath(path: (string | number)[]): CascaderOption[] {
|
|
113
|
+
const result: CascaderOption[] = [];
|
|
114
|
+
let currentLevel = options;
|
|
115
|
+
for (const val of path) {
|
|
116
|
+
const found = currentLevel.find((o) => o.value === val);
|
|
117
|
+
if (found) {
|
|
118
|
+
result.push(found);
|
|
119
|
+
currentLevel = found.children ?? [];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function clearAll(event?: MouseEvent) {
|
|
126
|
+
event?.stopPropagation();
|
|
127
|
+
value = undefined;
|
|
128
|
+
activePath = [];
|
|
129
|
+
onchange?.(undefined, []);
|
|
130
|
+
}
|
|
131
|
+
</script>
|
|
132
|
+
|
|
133
|
+
<div class={cn('relative w-full', className)} data-testid="cascader">
|
|
134
|
+
<Popover.Root bind:open>
|
|
135
|
+
<Popover.Trigger class="w-full">
|
|
136
|
+
{#snippet child({ props })}
|
|
137
|
+
<button
|
|
138
|
+
type="button"
|
|
139
|
+
{...props}
|
|
140
|
+
{disabled}
|
|
141
|
+
class={cn(
|
|
142
|
+
'flex min-h-9 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-1.5 text-xs ring-offset-background placeholder:text-muted-foreground focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 text-left',
|
|
143
|
+
open && 'ring-2 ring-ring ring-offset-2'
|
|
144
|
+
)}
|
|
145
|
+
>
|
|
146
|
+
<span class={cn('truncate', displayLabel ? 'text-foreground' : 'text-muted-foreground')}>
|
|
147
|
+
{displayLabel || (placeholder ?? (i18n.t('field.selectPlaceholder', undefined) ?? '请选择...'))}
|
|
148
|
+
</span>
|
|
149
|
+
|
|
150
|
+
<div class="flex items-center gap-1 shrink-0 ml-1 text-muted-foreground">
|
|
151
|
+
{#if allowClear && displayLabel && !disabled}
|
|
152
|
+
<span
|
|
153
|
+
role="button"
|
|
154
|
+
tabindex="0"
|
|
155
|
+
class="hover:text-foreground cursor-pointer p-0.5"
|
|
156
|
+
onclick={clearAll}
|
|
157
|
+
onkeydown={(e) => {
|
|
158
|
+
if (e.key === 'Enter') clearAll(e as never);
|
|
159
|
+
}}
|
|
160
|
+
>
|
|
161
|
+
<X class="h-3.5 w-3.5" />
|
|
162
|
+
</span>
|
|
163
|
+
{/if}
|
|
164
|
+
<ChevronsUpDown class="h-3.5 w-3.5 opacity-50" />
|
|
165
|
+
</div>
|
|
166
|
+
</button>
|
|
167
|
+
{/snippet}
|
|
168
|
+
</Popover.Trigger>
|
|
169
|
+
|
|
170
|
+
<Popover.Content class="w-auto min-w-[180px] p-0 shadow-lg border border-border" align="start">
|
|
171
|
+
<div class="flex divide-x divide-border max-h-64 overflow-x-auto">
|
|
172
|
+
{#each columns as columnOptions, colIndex (colIndex)}
|
|
173
|
+
<div class="w-40 min-w-[140px] max-h-64 overflow-y-auto p-1 space-y-0.5">
|
|
174
|
+
{#each columnOptions as option (option.value)}
|
|
175
|
+
{@const isSelectedInPath = activePath[colIndex] === option.value}
|
|
176
|
+
{@const hasChildren = Boolean(option.children && option.children.length > 0 && !option.isLeaf)}
|
|
177
|
+
<button
|
|
178
|
+
type="button"
|
|
179
|
+
class={cn(
|
|
180
|
+
'flex w-full items-center justify-between rounded-md px-2.5 py-1.5 text-xs text-left transition-colors',
|
|
181
|
+
isSelectedInPath
|
|
182
|
+
? 'bg-primary/10 text-primary font-medium'
|
|
183
|
+
: 'hover:bg-muted/60 text-foreground',
|
|
184
|
+
option.disabled && 'opacity-50 cursor-not-allowed pointer-events-none'
|
|
185
|
+
)}
|
|
186
|
+
onclick={() => handleSelectOption(colIndex, option)}
|
|
187
|
+
>
|
|
188
|
+
<span class="truncate flex-1">{option.label}</span>
|
|
189
|
+
{#if hasChildren}
|
|
190
|
+
<ChevronRight class="h-3.5 w-3.5 shrink-0 opacity-60 ml-1" />
|
|
191
|
+
{:else if isSelectedInPath && value && value[colIndex] === option.value}
|
|
192
|
+
<Check class="h-3.5 w-3.5 shrink-0 text-primary ml-1" />
|
|
193
|
+
{/if}
|
|
194
|
+
</button>
|
|
195
|
+
{:else}
|
|
196
|
+
<div class="py-4 text-center text-xs text-muted-foreground">
|
|
197
|
+
{i18n.t('common.noData', undefined) ?? '无数据'}
|
|
198
|
+
</div>
|
|
199
|
+
{/each}
|
|
200
|
+
</div>
|
|
201
|
+
{/each}
|
|
202
|
+
</div>
|
|
203
|
+
</Popover.Content>
|
|
204
|
+
</Popover.Root>
|
|
205
|
+
</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface CascaderOption {
|
|
2
|
+
value: string | number;
|
|
3
|
+
label: string;
|
|
4
|
+
children?: CascaderOption[];
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
isLeaf?: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface Props {
|
|
9
|
+
options?: CascaderOption[];
|
|
10
|
+
value?: (string | number)[];
|
|
11
|
+
separator?: string;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
changeOnSelect?: boolean;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
allowClear?: boolean;
|
|
16
|
+
class?: string;
|
|
17
|
+
onchange?: (value: (string | number)[] | undefined, selectedOptions: CascaderOption[]) => void;
|
|
18
|
+
}
|
|
19
|
+
declare const Cascader: import("svelte").Component<Props, {}, "value">;
|
|
20
|
+
type Cascader = ReturnType<typeof Cascader>;
|
|
21
|
+
export default Cascader;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
<script lang="ts" generics="T = Record<string, unknown>">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { Plus, Trash2, ArrowUp, ArrowDown, Copy } from '@lucide/svelte';
|
|
4
|
+
import { Button } from './ui/button/index.js';
|
|
5
|
+
import { useTranslation } from '@svadmin/core/i18n';
|
|
6
|
+
import TooltipButton from './TooltipButton.svelte';
|
|
7
|
+
|
|
8
|
+
const i18n = useTranslation();
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
items?: T[];
|
|
12
|
+
label?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
addButtonLabel?: string;
|
|
15
|
+
minItems?: number;
|
|
16
|
+
maxItems?: number;
|
|
17
|
+
defaultItem?: T | (() => T);
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
emptyText?: string;
|
|
20
|
+
onchange?: (items: T[]) => void;
|
|
21
|
+
children?: Snippet<[{
|
|
22
|
+
item: T;
|
|
23
|
+
index: number;
|
|
24
|
+
remove: () => void;
|
|
25
|
+
moveUp: () => void;
|
|
26
|
+
moveDown: () => void;
|
|
27
|
+
duplicate: () => void;
|
|
28
|
+
isFirst: boolean;
|
|
29
|
+
isLast: boolean;
|
|
30
|
+
}]>;
|
|
31
|
+
headerExtra?: Snippet<[{ count: number; add: (item?: T) => void }]>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let {
|
|
35
|
+
items = $bindable([]),
|
|
36
|
+
label,
|
|
37
|
+
description,
|
|
38
|
+
addButtonLabel,
|
|
39
|
+
minItems = 0,
|
|
40
|
+
maxItems = Infinity,
|
|
41
|
+
defaultItem,
|
|
42
|
+
disabled = false,
|
|
43
|
+
emptyText,
|
|
44
|
+
onchange,
|
|
45
|
+
children,
|
|
46
|
+
headerExtra,
|
|
47
|
+
}: Props = $props();
|
|
48
|
+
|
|
49
|
+
function createNewItem(): T {
|
|
50
|
+
if (typeof defaultItem === 'function') {
|
|
51
|
+
return (defaultItem as () => T)();
|
|
52
|
+
}
|
|
53
|
+
if (defaultItem !== undefined) {
|
|
54
|
+
return structuredClone(defaultItem);
|
|
55
|
+
}
|
|
56
|
+
return {} as T;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function add(customItem?: T): void {
|
|
60
|
+
if (disabled || items.length >= maxItems) return;
|
|
61
|
+
const itemToAdd = customItem !== undefined ? structuredClone(customItem) : createNewItem();
|
|
62
|
+
items = [...items, itemToAdd];
|
|
63
|
+
onchange?.(items);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function remove(index: number): void {
|
|
67
|
+
if (disabled || items.length <= minItems || index < 0 || index >= items.length) return;
|
|
68
|
+
const next = [...items];
|
|
69
|
+
next.splice(index, 1);
|
|
70
|
+
items = next;
|
|
71
|
+
onchange?.(items);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function moveUp(index: number): void {
|
|
75
|
+
if (disabled || index <= 0 || index >= items.length) return;
|
|
76
|
+
const next = [...items];
|
|
77
|
+
const temp = next[index - 1];
|
|
78
|
+
next[index - 1] = next[index];
|
|
79
|
+
next[index] = temp;
|
|
80
|
+
items = next;
|
|
81
|
+
onchange?.(items);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function moveDown(index: number): void {
|
|
85
|
+
if (disabled || index < 0 || index >= items.length - 1) return;
|
|
86
|
+
const next = [...items];
|
|
87
|
+
const temp = next[index + 1];
|
|
88
|
+
next[index + 1] = next[index];
|
|
89
|
+
next[index] = temp;
|
|
90
|
+
items = next;
|
|
91
|
+
onchange?.(items);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function duplicate(index: number): void {
|
|
95
|
+
if (disabled || items.length >= maxItems || index < 0 || index >= items.length) return;
|
|
96
|
+
const next = [...items];
|
|
97
|
+
const cloned = structuredClone(next[index]);
|
|
98
|
+
next.splice(index + 1, 0, cloned);
|
|
99
|
+
items = next;
|
|
100
|
+
onchange?.(items);
|
|
101
|
+
}
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
<div class="w-full space-y-3" data-testid="dynamic-form-list">
|
|
105
|
+
{#if label || description || headerExtra}
|
|
106
|
+
<div class="flex items-center justify-between gap-2">
|
|
107
|
+
<div>
|
|
108
|
+
{#if label}
|
|
109
|
+
<div class="text-sm font-medium text-foreground">{label}</div>
|
|
110
|
+
{/if}
|
|
111
|
+
{#if description}
|
|
112
|
+
<div class="text-xs text-muted-foreground">{description}</div>
|
|
113
|
+
{/if}
|
|
114
|
+
</div>
|
|
115
|
+
{#if headerExtra}
|
|
116
|
+
{@render headerExtra({ count: items.length, add })}
|
|
117
|
+
{/if}
|
|
118
|
+
</div>
|
|
119
|
+
{/if}
|
|
120
|
+
|
|
121
|
+
<div class="space-y-2">
|
|
122
|
+
{#each items as item, index (index)}
|
|
123
|
+
{@const isFirst = index === 0}
|
|
124
|
+
{@const isLast = index === items.length - 1}
|
|
125
|
+
<div class="group relative rounded-lg border border-border/70 bg-card p-3 shadow-xs transition-colors hover:border-border">
|
|
126
|
+
<div class="flex items-start gap-3">
|
|
127
|
+
<div class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-muted text-[11px] font-medium text-muted-foreground">
|
|
128
|
+
{index + 1}
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<div class="min-w-0 flex-1">
|
|
132
|
+
{#if children}
|
|
133
|
+
{@render children({
|
|
134
|
+
item,
|
|
135
|
+
index,
|
|
136
|
+
remove: () => remove(index),
|
|
137
|
+
moveUp: () => moveUp(index),
|
|
138
|
+
moveDown: () => moveDown(index),
|
|
139
|
+
duplicate: () => duplicate(index),
|
|
140
|
+
isFirst,
|
|
141
|
+
isLast,
|
|
142
|
+
})}
|
|
143
|
+
{:else}
|
|
144
|
+
<div class="text-sm text-foreground/80">
|
|
145
|
+
{JSON.stringify(item)}
|
|
146
|
+
</div>
|
|
147
|
+
{/if}
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div class="flex shrink-0 items-center gap-0.5 opacity-80 group-hover:opacity-100">
|
|
151
|
+
<TooltipButton
|
|
152
|
+
tooltip={i18n.t('common.moveUp', undefined) ?? '上移'}
|
|
153
|
+
variant="ghost"
|
|
154
|
+
size="icon-sm"
|
|
155
|
+
disabled={disabled || isFirst}
|
|
156
|
+
onclick={() => moveUp(index)}
|
|
157
|
+
>
|
|
158
|
+
<ArrowUp class="h-3.5 w-3.5" />
|
|
159
|
+
</TooltipButton>
|
|
160
|
+
|
|
161
|
+
<TooltipButton
|
|
162
|
+
tooltip={i18n.t('common.moveDown', undefined) ?? '下移'}
|
|
163
|
+
variant="ghost"
|
|
164
|
+
size="icon-sm"
|
|
165
|
+
disabled={disabled || isLast}
|
|
166
|
+
onclick={() => moveDown(index)}
|
|
167
|
+
>
|
|
168
|
+
<ArrowDown class="h-3.5 w-3.5" />
|
|
169
|
+
</TooltipButton>
|
|
170
|
+
|
|
171
|
+
<TooltipButton
|
|
172
|
+
tooltip={i18n.t('common.clone', undefined) ?? '复制'}
|
|
173
|
+
variant="ghost"
|
|
174
|
+
size="icon-sm"
|
|
175
|
+
disabled={disabled || items.length >= maxItems}
|
|
176
|
+
onclick={() => duplicate(index)}
|
|
177
|
+
>
|
|
178
|
+
<Copy class="h-3.5 w-3.5" />
|
|
179
|
+
</TooltipButton>
|
|
180
|
+
|
|
181
|
+
<TooltipButton
|
|
182
|
+
tooltip={i18n.t('common.delete', undefined) ?? '删除'}
|
|
183
|
+
variant="ghost"
|
|
184
|
+
size="icon-sm"
|
|
185
|
+
class="text-muted-foreground hover:text-destructive"
|
|
186
|
+
disabled={disabled || items.length <= minItems}
|
|
187
|
+
onclick={() => remove(index)}
|
|
188
|
+
>
|
|
189
|
+
<Trash2 class="h-3.5 w-3.5" />
|
|
190
|
+
</TooltipButton>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
{:else}
|
|
195
|
+
<div class="rounded-lg border border-dashed border-border/80 p-6 text-center text-xs text-muted-foreground">
|
|
196
|
+
{emptyText ?? (i18n.t('common.noData', undefined) ?? '暂无项目')}
|
|
197
|
+
</div>
|
|
198
|
+
{/each}
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
{#if items.length < maxItems}
|
|
202
|
+
<Button
|
|
203
|
+
type="button"
|
|
204
|
+
variant="outline"
|
|
205
|
+
size="sm"
|
|
206
|
+
class="w-full gap-1.5 border-dashed"
|
|
207
|
+
{disabled}
|
|
208
|
+
onclick={() => add()}
|
|
209
|
+
>
|
|
210
|
+
<Plus class="h-4 w-4" />
|
|
211
|
+
{addButtonLabel ?? (i18n.t('common.add', undefined) ?? '添加一项')}
|
|
212
|
+
</Button>
|
|
213
|
+
{/if}
|
|
214
|
+
</div>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
declare function $$render<T = Record<string, unknown>>(): {
|
|
3
|
+
props: {
|
|
4
|
+
items?: T[];
|
|
5
|
+
label?: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
addButtonLabel?: string;
|
|
8
|
+
minItems?: number;
|
|
9
|
+
maxItems?: number;
|
|
10
|
+
defaultItem?: T | (() => T);
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
emptyText?: string;
|
|
13
|
+
onchange?: (items: T[]) => void;
|
|
14
|
+
children?: Snippet<[{
|
|
15
|
+
item: T;
|
|
16
|
+
index: number;
|
|
17
|
+
remove: () => void;
|
|
18
|
+
moveUp: () => void;
|
|
19
|
+
moveDown: () => void;
|
|
20
|
+
duplicate: () => void;
|
|
21
|
+
isFirst: boolean;
|
|
22
|
+
isLast: boolean;
|
|
23
|
+
}]>;
|
|
24
|
+
headerExtra?: Snippet<[{
|
|
25
|
+
count: number;
|
|
26
|
+
add: (item?: T) => void;
|
|
27
|
+
}]>;
|
|
28
|
+
};
|
|
29
|
+
exports: {
|
|
30
|
+
add: (customItem?: T) => void;
|
|
31
|
+
remove: (index: number) => void;
|
|
32
|
+
moveUp: (index: number) => void;
|
|
33
|
+
moveDown: (index: number) => void;
|
|
34
|
+
duplicate: (index: number) => void;
|
|
35
|
+
};
|
|
36
|
+
bindings: "items";
|
|
37
|
+
slots: {};
|
|
38
|
+
events: {};
|
|
39
|
+
};
|
|
40
|
+
declare class __sveltets_Render<T = Record<string, unknown>> {
|
|
41
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
42
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
43
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
44
|
+
bindings(): "items";
|
|
45
|
+
exports(): {
|
|
46
|
+
add: (customItem?: T | undefined) => void;
|
|
47
|
+
remove: (index: number) => void;
|
|
48
|
+
moveUp: (index: number) => void;
|
|
49
|
+
moveDown: (index: number) => void;
|
|
50
|
+
duplicate: (index: number) => void;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
interface $$IsomorphicComponent {
|
|
54
|
+
new <T = Record<string, unknown>>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
55
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
56
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
57
|
+
<T = Record<string, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
58
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
59
|
+
}
|
|
60
|
+
declare const DynamicFormList: $$IsomorphicComponent;
|
|
61
|
+
type DynamicFormList<T = Record<string, unknown>> = InstanceType<typeof DynamicFormList<T>>;
|
|
62
|
+
export default DynamicFormList;
|
|
@@ -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
|