@h-ai/ui 0.1.0-alpha.23 → 0.1.0-alpha.25
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/README.md +23 -5
- package/dist/components/compounds/Card.svelte +1 -1
- package/dist/components/compounds/DataTable.svelte +29 -7
- package/dist/components/compounds/Modal.svelte +79 -57
- package/dist/components/compounds/Pagination.svelte +30 -16
- package/dist/components/primitives/Input.svelte +23 -1
- package/dist/components/primitives/Select.svelte +2 -2
- package/dist/components/scenes/ai/markdown-parse.js +12 -4
- package/dist/components/scenes/ai/table/AiTableEditor.svelte +4 -2
- package/dist/components/scenes/crud/CrudDetailDrawer.svelte +196 -0
- package/dist/components/scenes/crud/CrudDetailDrawer.svelte.d.ts +44 -0
- package/dist/components/scenes/crud/CrudDetailPanel.svelte +23 -11
- package/dist/components/scenes/crud/CrudDetailPanel.svelte.d.ts +2 -1
- package/dist/components/scenes/crud/CrudEditPanel.svelte +28 -12
- package/dist/components/scenes/crud/CrudEditPanel.svelte.d.ts +2 -1
- package/dist/components/scenes/crud/CrudFilterBar.svelte +22 -18
- package/dist/components/scenes/crud/CrudPage.svelte +23 -13
- package/dist/components/scenes/crud/crud-types.d.ts +1 -1
- package/dist/theme-config.d.ts +60 -4
- package/dist/theme-config.js +77 -6
- package/package.json +2 -2
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component CrudDetailDrawer
|
|
3
|
+
CRUD 详情组件(只读展示),支持抽屉(drawer)与弹窗(modal)两种展示形式
|
|
4
|
+
|
|
5
|
+
使用 Svelte 5 Runes ($props, $derived)
|
|
6
|
+
使用 compounds 组件:Drawer, Modal, Badge
|
|
7
|
+
-->
|
|
8
|
+
<script lang='ts'>
|
|
9
|
+
import type { Snippet } from 'svelte'
|
|
10
|
+
import type { Size } from '../../../types.js'
|
|
11
|
+
import type { CrudFormVariant } from './crud-types.js'
|
|
12
|
+
import { uiM } from '../../../messages.js'
|
|
13
|
+
import Drawer from '../../compounds/Drawer.svelte'
|
|
14
|
+
import Modal from '../../compounds/Modal.svelte'
|
|
15
|
+
import Badge from '../../primitives/Badge.svelte'
|
|
16
|
+
import Button from '../../primitives/Button.svelte'
|
|
17
|
+
|
|
18
|
+
type FieldDef = {
|
|
19
|
+
id: string
|
|
20
|
+
label: string | (() => string)
|
|
21
|
+
type: string
|
|
22
|
+
inDetail?: boolean
|
|
23
|
+
options?: Array<{ label: string, value: string | number | boolean }> | (() => Array<{ label: string, value: string | number | boolean }>)
|
|
24
|
+
render?: (value: unknown, item: Record<string, unknown>) => string
|
|
25
|
+
order?: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let {
|
|
29
|
+
open = $bindable(false),
|
|
30
|
+
item = null,
|
|
31
|
+
fields = [],
|
|
32
|
+
title = '',
|
|
33
|
+
variant = 'drawer' as CrudFormVariant,
|
|
34
|
+
size = '2xl' as Size,
|
|
35
|
+
drawerWidth,
|
|
36
|
+
modalSize = '2xl' as Size | 'full',
|
|
37
|
+
modalWidth,
|
|
38
|
+
modalHeight,
|
|
39
|
+
canEdit = false,
|
|
40
|
+
onedit,
|
|
41
|
+
onclose,
|
|
42
|
+
detailExtra,
|
|
43
|
+
}: {
|
|
44
|
+
open?: boolean
|
|
45
|
+
item?: Record<string, unknown> | null
|
|
46
|
+
fields?: FieldDef[]
|
|
47
|
+
title?: string
|
|
48
|
+
variant?: CrudFormVariant
|
|
49
|
+
size?: Size
|
|
50
|
+
drawerWidth?: string
|
|
51
|
+
modalSize?: Size | 'full'
|
|
52
|
+
modalWidth?: string
|
|
53
|
+
modalHeight?: string
|
|
54
|
+
canEdit?: boolean
|
|
55
|
+
onedit?: () => void
|
|
56
|
+
onclose?: () => void
|
|
57
|
+
detailExtra?: Snippet<[Record<string, unknown>]>
|
|
58
|
+
} = $props()
|
|
59
|
+
|
|
60
|
+
function resolveText(text: string | (() => string)): string {
|
|
61
|
+
return typeof text === 'function' ? text() : text
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function resolveOptions(options?: FieldDef['options']): Array<{ label: string, value: string | number | boolean }> {
|
|
65
|
+
if (!options)
|
|
66
|
+
return []
|
|
67
|
+
return typeof options === 'function' ? options() : options
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function formatValue(field: FieldDef, value: unknown): string {
|
|
71
|
+
if (value === null || value === undefined)
|
|
72
|
+
return uiM('crud_detail_empty')
|
|
73
|
+
|
|
74
|
+
if (field.render && item) {
|
|
75
|
+
return field.render(value, item)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 选项类型:将 value 映射为 label
|
|
79
|
+
if (field.type === 'select' || field.type === 'radio') {
|
|
80
|
+
const opts = resolveOptions(field.options)
|
|
81
|
+
const found = opts.find(o => String(o.value) === String(value))
|
|
82
|
+
return found?.label ?? String(value)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 多选类型
|
|
86
|
+
if (field.type === 'multi-select' && Array.isArray(value)) {
|
|
87
|
+
const opts = resolveOptions(field.options)
|
|
88
|
+
return value.map((v) => {
|
|
89
|
+
const found = opts.find(o => String(o.value) === String(v))
|
|
90
|
+
return found?.label ?? String(v)
|
|
91
|
+
}).join(', ')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 布尔
|
|
95
|
+
if (field.type === 'boolean' || field.type === 'checkbox') {
|
|
96
|
+
return value ? '✓' : '✗'
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 日期/时间
|
|
100
|
+
if (field.type === 'date' && (typeof value === 'string' || typeof value === 'number' || value instanceof Date)) {
|
|
101
|
+
return new Date(value as string | number).toLocaleDateString()
|
|
102
|
+
}
|
|
103
|
+
if (field.type === 'datetime' && (typeof value === 'string' || typeof value === 'number' || value instanceof Date)) {
|
|
104
|
+
return new Date(value as string | number).toLocaleString()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return String(value)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const detailFields = $derived(
|
|
111
|
+
[...fields]
|
|
112
|
+
.filter(f => f.inDetail !== false)
|
|
113
|
+
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
function handleClose() {
|
|
117
|
+
open = false
|
|
118
|
+
onclose?.()
|
|
119
|
+
}
|
|
120
|
+
</script>
|
|
121
|
+
|
|
122
|
+
{#snippet detailBody()}
|
|
123
|
+
{#if item}
|
|
124
|
+
<div class='space-y-4'>
|
|
125
|
+
{#each detailFields as field (field.id)}
|
|
126
|
+
{@const value = item[field.id]}
|
|
127
|
+
<div>
|
|
128
|
+
<div class='text-xs font-medium text-base-content/50 mb-1'>
|
|
129
|
+
{resolveText(field.label)}
|
|
130
|
+
</div>
|
|
131
|
+
<div class='text-sm text-base-content'>
|
|
132
|
+
{#if field.type === 'multi-select' && Array.isArray(value) && value.length > 0}
|
|
133
|
+
<div class='flex flex-wrap gap-1'>
|
|
134
|
+
{#each value as v, index (`${String(v)}:${index}`)}
|
|
135
|
+
{@const opts = resolveOptions(field.options)}
|
|
136
|
+
{@const found = opts.find(o => String(o.value) === String(v))}
|
|
137
|
+
<Badge variant='ghost' size='sm'>{found?.label ?? String(v)}</Badge>
|
|
138
|
+
{/each}
|
|
139
|
+
</div>
|
|
140
|
+
{:else}
|
|
141
|
+
{formatValue(field, value)}
|
|
142
|
+
{/if}
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
{/each}
|
|
146
|
+
|
|
147
|
+
{#if detailExtra && item}
|
|
148
|
+
<div class='border-t border-base-content/5 pt-4'>
|
|
149
|
+
{@render detailExtra(item)}
|
|
150
|
+
</div>
|
|
151
|
+
{/if}
|
|
152
|
+
</div>
|
|
153
|
+
{/if}
|
|
154
|
+
{/snippet}
|
|
155
|
+
|
|
156
|
+
{#snippet footerActions()}
|
|
157
|
+
{#if canEdit}
|
|
158
|
+
<Button variant='primary' onclick={onedit}>
|
|
159
|
+
<span class='icon-[tabler--edit] size-4 mr-1'></span>
|
|
160
|
+
{uiM('crud_edit')}
|
|
161
|
+
</Button>
|
|
162
|
+
{/if}
|
|
163
|
+
<Button variant='ghost' onclick={handleClose}>
|
|
164
|
+
{uiM('crud_close')}
|
|
165
|
+
</Button>
|
|
166
|
+
{/snippet}
|
|
167
|
+
|
|
168
|
+
{#if variant === 'modal'}
|
|
169
|
+
<Modal
|
|
170
|
+
bind:open
|
|
171
|
+
{title}
|
|
172
|
+
size={modalSize}
|
|
173
|
+
width={modalWidth}
|
|
174
|
+
height={modalHeight}
|
|
175
|
+
onclose={handleClose}
|
|
176
|
+
>
|
|
177
|
+
{@render detailBody()}
|
|
178
|
+
|
|
179
|
+
{#snippet footer()}
|
|
180
|
+
{@render footerActions()}
|
|
181
|
+
{/snippet}
|
|
182
|
+
</Modal>
|
|
183
|
+
{:else}
|
|
184
|
+
<Drawer bind:open {title} position='right' {size} width={drawerWidth} onclose={handleClose}>
|
|
185
|
+
{#if item}
|
|
186
|
+
<div class='pb-20'>
|
|
187
|
+
{@render detailBody()}
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<!-- 底部操作栏 -->
|
|
191
|
+
<div class='absolute bottom-0 left-0 right-0 p-4 bg-base-200 border-t border-base-content/10 flex justify-end gap-2'>
|
|
192
|
+
{@render footerActions()}
|
|
193
|
+
</div>
|
|
194
|
+
{/if}
|
|
195
|
+
</Drawer>
|
|
196
|
+
{/if}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { Size } from '../../../types.js';
|
|
3
|
+
import type { CrudFormVariant } from './crud-types.js';
|
|
4
|
+
type FieldDef = {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string | (() => string);
|
|
7
|
+
type: string;
|
|
8
|
+
inDetail?: boolean;
|
|
9
|
+
options?: Array<{
|
|
10
|
+
label: string;
|
|
11
|
+
value: string | number | boolean;
|
|
12
|
+
}> | (() => Array<{
|
|
13
|
+
label: string;
|
|
14
|
+
value: string | number | boolean;
|
|
15
|
+
}>);
|
|
16
|
+
render?: (value: unknown, item: Record<string, unknown>) => string;
|
|
17
|
+
order?: number;
|
|
18
|
+
};
|
|
19
|
+
type $$ComponentProps = {
|
|
20
|
+
open?: boolean;
|
|
21
|
+
item?: Record<string, unknown> | null;
|
|
22
|
+
fields?: FieldDef[];
|
|
23
|
+
title?: string;
|
|
24
|
+
variant?: CrudFormVariant;
|
|
25
|
+
size?: Size;
|
|
26
|
+
drawerWidth?: string;
|
|
27
|
+
modalSize?: Size | 'full';
|
|
28
|
+
modalWidth?: string;
|
|
29
|
+
modalHeight?: string;
|
|
30
|
+
canEdit?: boolean;
|
|
31
|
+
onedit?: () => void;
|
|
32
|
+
onclose?: () => void;
|
|
33
|
+
detailExtra?: Snippet<[Record<string, unknown>]>;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* CrudDetailDrawer
|
|
37
|
+
* CRUD 详情组件(只读展示),支持抽屉(drawer)与弹窗(modal)两种展示形式
|
|
38
|
+
*
|
|
39
|
+
* 使用 Svelte 5 Runes ($props, $derived)
|
|
40
|
+
* 使用 compounds 组件:Drawer, Modal, Badge
|
|
41
|
+
*/
|
|
42
|
+
declare const CrudDetailDrawer: import("svelte").Component<$$ComponentProps, {}, "open">;
|
|
43
|
+
type CrudDetailDrawer = ReturnType<typeof CrudDetailDrawer>;
|
|
44
|
+
export default CrudDetailDrawer;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<script lang='ts'>
|
|
9
9
|
import type { Snippet } from 'svelte'
|
|
10
10
|
import type { Size } from '../../../types.js'
|
|
11
|
-
import type { CrudFormVariant } from './crud-types.js'
|
|
11
|
+
import type { CrudDensity, CrudFormVariant } from './crud-types.js'
|
|
12
12
|
import { uiM } from '../../../messages.js'
|
|
13
13
|
import Drawer from '../../compounds/Drawer.svelte'
|
|
14
14
|
import Modal from '../../compounds/Modal.svelte'
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
open = $bindable(false),
|
|
30
30
|
item = null,
|
|
31
31
|
fields = [],
|
|
32
|
+
density = 'normal' as CrudDensity,
|
|
32
33
|
title = '',
|
|
33
34
|
variant = 'drawer' as CrudFormVariant,
|
|
34
35
|
size = '2xl' as Size,
|
|
@@ -44,6 +45,7 @@
|
|
|
44
45
|
open?: boolean
|
|
45
46
|
item?: Record<string, unknown> | null
|
|
46
47
|
fields?: FieldDef[]
|
|
48
|
+
density?: CrudDensity
|
|
47
49
|
title?: string
|
|
48
50
|
variant?: CrudFormVariant
|
|
49
51
|
size?: Size
|
|
@@ -109,6 +111,16 @@
|
|
|
109
111
|
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),
|
|
110
112
|
)
|
|
111
113
|
|
|
114
|
+
const isCompact = $derived(density === 'compact')
|
|
115
|
+
const contentClass = $derived(isCompact ? 'space-y-3' : 'space-y-4')
|
|
116
|
+
const labelClass = $derived(isCompact ? 'text-[11px] font-medium text-base-content/50 mb-1' : 'text-xs font-medium text-base-content/50 mb-1')
|
|
117
|
+
const valueClass = $derived(isCompact ? 'text-[13px] text-base-content' : 'text-sm text-base-content')
|
|
118
|
+
const badgeSize = $derived(isCompact ? 'xs' : 'sm')
|
|
119
|
+
const detailExtraClass = $derived(isCompact ? 'border-t border-base-content/5 pt-3' : 'border-t border-base-content/5 pt-4')
|
|
120
|
+
const iconClass = $derived(isCompact ? 'size-3.5 mr-1' : 'size-4 mr-1')
|
|
121
|
+
const drawerBodyClass = $derived(isCompact ? 'pb-16' : 'pb-20')
|
|
122
|
+
const drawerFooterClass = $derived(isCompact ? 'absolute bottom-0 left-0 right-0 p-3 bg-base-200 border-t border-base-content/10 flex justify-end gap-2' : 'absolute bottom-0 left-0 right-0 p-4 bg-base-200 border-t border-base-content/10 flex justify-end gap-2')
|
|
123
|
+
|
|
112
124
|
function handleClose() {
|
|
113
125
|
open = false
|
|
114
126
|
onclose?.()
|
|
@@ -117,20 +129,20 @@
|
|
|
117
129
|
|
|
118
130
|
{#snippet detailBody()}
|
|
119
131
|
{#if item}
|
|
120
|
-
<div class=
|
|
132
|
+
<div class={contentClass}>
|
|
121
133
|
{#each detailFields as field (field.id)}
|
|
122
134
|
{@const value = item[field.id]}
|
|
123
135
|
<div>
|
|
124
|
-
<div class=
|
|
136
|
+
<div class={labelClass}>
|
|
125
137
|
{resolveText(field.label)}
|
|
126
138
|
</div>
|
|
127
|
-
<div class=
|
|
139
|
+
<div class={valueClass}>
|
|
128
140
|
{#if field.type === 'multi-select' && Array.isArray(value) && value.length > 0}
|
|
129
141
|
<div class='flex flex-wrap gap-1'>
|
|
130
142
|
{#each value as v, index (`${String(v)}:${index}`)}
|
|
131
143
|
{@const opts = resolveOptions(field.options)}
|
|
132
144
|
{@const found = opts.find(o => String(o.value) === String(v))}
|
|
133
|
-
<Badge variant='ghost' size=
|
|
145
|
+
<Badge variant='ghost' size={badgeSize}>{found?.label ?? String(v)}</Badge>
|
|
134
146
|
{/each}
|
|
135
147
|
</div>
|
|
136
148
|
{:else}
|
|
@@ -141,7 +153,7 @@
|
|
|
141
153
|
{/each}
|
|
142
154
|
|
|
143
155
|
{#if detailExtra && item}
|
|
144
|
-
<div class=
|
|
156
|
+
<div class={detailExtraClass}>
|
|
145
157
|
{@render detailExtra(item)}
|
|
146
158
|
</div>
|
|
147
159
|
{/if}
|
|
@@ -151,12 +163,12 @@
|
|
|
151
163
|
|
|
152
164
|
{#snippet footerActions()}
|
|
153
165
|
{#if canEdit}
|
|
154
|
-
<Button variant='primary' onclick={onedit}>
|
|
155
|
-
<span class='icon-[tabler--edit]
|
|
166
|
+
<Button variant='primary' size='sm' onclick={onedit}>
|
|
167
|
+
<span class='icon-[tabler--edit] {iconClass}'></span>
|
|
156
168
|
{uiM('crud_edit')}
|
|
157
169
|
</Button>
|
|
158
170
|
{/if}
|
|
159
|
-
<Button variant='ghost' onclick={handleClose}>
|
|
171
|
+
<Button variant='ghost' size='sm' onclick={handleClose}>
|
|
160
172
|
{uiM('crud_close')}
|
|
161
173
|
</Button>
|
|
162
174
|
{/snippet}
|
|
@@ -179,12 +191,12 @@
|
|
|
179
191
|
{:else}
|
|
180
192
|
<Drawer bind:open {title} position='right' {size} width={drawerWidth} onclose={handleClose}>
|
|
181
193
|
{#if item}
|
|
182
|
-
<div class=
|
|
194
|
+
<div class={drawerBodyClass}>
|
|
183
195
|
{@render detailBody()}
|
|
184
196
|
</div>
|
|
185
197
|
|
|
186
198
|
<!-- 底部操作栏 -->
|
|
187
|
-
<div class=
|
|
199
|
+
<div class={drawerFooterClass}>
|
|
188
200
|
{@render footerActions()}
|
|
189
201
|
</div>
|
|
190
202
|
{/if}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { Size } from '../../../types.js';
|
|
3
|
-
import type { CrudFormVariant } from './crud-types.js';
|
|
3
|
+
import type { CrudDensity, CrudFormVariant } from './crud-types.js';
|
|
4
4
|
type FieldDef = {
|
|
5
5
|
id: string;
|
|
6
6
|
label: string | (() => string);
|
|
@@ -20,6 +20,7 @@ type $$ComponentProps = {
|
|
|
20
20
|
open?: boolean;
|
|
21
21
|
item?: Record<string, unknown> | null;
|
|
22
22
|
fields?: FieldDef[];
|
|
23
|
+
density?: CrudDensity;
|
|
23
24
|
title?: string;
|
|
24
25
|
variant?: CrudFormVariant;
|
|
25
26
|
size?: Size;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<script lang='ts'>
|
|
9
9
|
import type { Snippet } from 'svelte'
|
|
10
10
|
import type { Size } from '../../../types.js'
|
|
11
|
-
import type { CrudFormVariant } from './crud-types.js'
|
|
11
|
+
import type { CrudDensity, CrudFormVariant } from './crud-types.js'
|
|
12
12
|
import { uiM } from '../../../messages.js'
|
|
13
13
|
import Drawer from '../../compounds/Drawer.svelte'
|
|
14
14
|
import FormField from '../../compounds/FormField.svelte'
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
mode = 'create' as 'create' | 'edit',
|
|
36
36
|
fields = [],
|
|
37
37
|
formData = $bindable<Record<string, unknown>>({}),
|
|
38
|
+
density = 'normal' as CrudDensity,
|
|
38
39
|
title = '',
|
|
39
40
|
variant = 'drawer' as CrudFormVariant,
|
|
40
41
|
size = '2xl' as Size,
|
|
@@ -53,6 +54,7 @@
|
|
|
53
54
|
mode?: 'create' | 'edit'
|
|
54
55
|
fields?: FieldDef[]
|
|
55
56
|
formData?: Record<string, unknown>
|
|
57
|
+
density?: CrudDensity
|
|
56
58
|
title?: string
|
|
57
59
|
variant?: CrudFormVariant
|
|
58
60
|
size?: Size
|
|
@@ -82,6 +84,16 @@
|
|
|
82
84
|
[...fields].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),
|
|
83
85
|
)
|
|
84
86
|
|
|
87
|
+
const isCompact = $derived(density === 'compact')
|
|
88
|
+
const controlSize = $derived(isCompact ? 'xs' : 'sm')
|
|
89
|
+
const formClass = $derived(isCompact ? 'space-y-3' : 'space-y-4')
|
|
90
|
+
const extraClass = $derived(isCompact ? 'border-t border-base-content/5 pt-3' : 'border-t border-base-content/5 pt-4')
|
|
91
|
+
const multiSelectWrapClass = $derived(isCompact ? 'flex flex-wrap gap-2 p-2.5 bg-base-200 rounded-lg max-h-48 overflow-y-auto' : 'flex flex-wrap gap-3 p-3 bg-base-200 rounded-lg max-h-48 overflow-y-auto')
|
|
92
|
+
const multiSelectLabelClass = $derived(isCompact ? 'inline-flex items-center gap-1.5 cursor-pointer' : 'inline-flex items-center gap-2 cursor-pointer')
|
|
93
|
+
const multiSelectTextClass = $derived(isCompact ? 'text-xs text-base-content' : 'text-sm text-base-content')
|
|
94
|
+
const drawerFormClass = $derived(isCompact ? 'space-y-3 pb-16' : 'space-y-4 pb-20')
|
|
95
|
+
const drawerFooterClass = $derived(isCompact ? 'absolute bottom-0 left-0 right-0 p-3 bg-base-200 border-t border-base-content/10 flex justify-end gap-2' : 'absolute bottom-0 left-0 right-0 p-4 bg-base-200 border-t border-base-content/10 flex justify-end gap-2')
|
|
96
|
+
|
|
85
97
|
function handleClose() {
|
|
86
98
|
open = false
|
|
87
99
|
onclose?.()
|
|
@@ -139,6 +151,7 @@
|
|
|
139
151
|
<FormField label={resolveText(field.label)} required={isRequired}>
|
|
140
152
|
<Textarea
|
|
141
153
|
id={field.id}
|
|
154
|
+
size={controlSize}
|
|
142
155
|
value={String(fieldValue ?? '')}
|
|
143
156
|
{placeholder}
|
|
144
157
|
disabled={submitting || isReadonly}
|
|
@@ -152,6 +165,7 @@
|
|
|
152
165
|
<FormField label={resolveText(field.label)} required={isRequired}>
|
|
153
166
|
<Select
|
|
154
167
|
id={field.id}
|
|
168
|
+
size={controlSize}
|
|
155
169
|
value={String(fieldValue ?? '')}
|
|
156
170
|
disabled={submitting || isReadonly}
|
|
157
171
|
onchange={value => updateField(field.id, value)}
|
|
@@ -167,20 +181,20 @@
|
|
|
167
181
|
{@const opts = resolveOptions(field.options)}
|
|
168
182
|
{@const selectedValues = ((fieldValue ?? []) as Array<string | number | boolean>).map(String)}
|
|
169
183
|
<FormField label={resolveText(field.label)} required={isRequired}>
|
|
170
|
-
<div class=
|
|
184
|
+
<div class={multiSelectWrapClass}>
|
|
171
185
|
{#each opts as opt (String(opt.value))}
|
|
172
|
-
<label class=
|
|
186
|
+
<label class={multiSelectLabelClass}>
|
|
173
187
|
<Checkbox
|
|
174
|
-
size=
|
|
188
|
+
size={controlSize}
|
|
175
189
|
checked={selectedValues.includes(String(opt.value))}
|
|
176
190
|
onchange={() => toggleMultiSelect(field.id, opt.value)}
|
|
177
191
|
disabled={submitting || isReadonly}
|
|
178
192
|
/>
|
|
179
|
-
<span class=
|
|
193
|
+
<span class={multiSelectTextClass}>{opt.label}</span>
|
|
180
194
|
</label>
|
|
181
195
|
{/each}
|
|
182
196
|
{#if opts.length === 0}
|
|
183
|
-
<span class=
|
|
197
|
+
<span class={multiSelectTextClass}>{uiM('crud_no_data')}</span>
|
|
184
198
|
{/if}
|
|
185
199
|
</div>
|
|
186
200
|
</FormField>
|
|
@@ -188,6 +202,7 @@
|
|
|
188
202
|
{:else if field.type === 'boolean' || field.type === 'checkbox'}
|
|
189
203
|
<FormField label={resolveText(field.label)}>
|
|
190
204
|
<Checkbox
|
|
205
|
+
size={controlSize}
|
|
191
206
|
checked={Boolean(fieldValue)}
|
|
192
207
|
onchange={(checked: boolean) => updateField(field.id, checked)}
|
|
193
208
|
disabled={submitting || isReadonly}
|
|
@@ -199,6 +214,7 @@
|
|
|
199
214
|
<Input
|
|
200
215
|
id={field.id}
|
|
201
216
|
type={getInputType(field.type)}
|
|
217
|
+
size={controlSize}
|
|
202
218
|
value={String(fieldValue ?? '')}
|
|
203
219
|
{placeholder}
|
|
204
220
|
disabled={submitting || isReadonly}
|
|
@@ -215,17 +231,17 @@
|
|
|
215
231
|
{/each}
|
|
216
232
|
|
|
217
233
|
{#if editFormExtra}
|
|
218
|
-
<div class=
|
|
234
|
+
<div class={extraClass}>
|
|
219
235
|
{@render editFormExtra(editingItem, mode)}
|
|
220
236
|
</div>
|
|
221
237
|
{/if}
|
|
222
238
|
{/snippet}
|
|
223
239
|
|
|
224
240
|
{#snippet footerActions()}
|
|
225
|
-
<Button variant='ghost' onclick={handleClose} disabled={submitting}>
|
|
241
|
+
<Button variant='ghost' size='sm' onclick={handleClose} disabled={submitting}>
|
|
226
242
|
{uiM('crud_cancel')}
|
|
227
243
|
</Button>
|
|
228
|
-
<Button variant='primary' onclick={handleSubmit} disabled={submitting}>
|
|
244
|
+
<Button variant='primary' size='sm' onclick={handleSubmit} disabled={submitting}>
|
|
229
245
|
{#if submitting}
|
|
230
246
|
<span class='loading loading-spinner loading-xs mr-2'></span>
|
|
231
247
|
{/if}
|
|
@@ -243,7 +259,7 @@
|
|
|
243
259
|
closeOnBackdrop={false}
|
|
244
260
|
onclose={handleClose}
|
|
245
261
|
>
|
|
246
|
-
<form onsubmit={handleSubmit} class=
|
|
262
|
+
<form onsubmit={handleSubmit} class={formClass}>
|
|
247
263
|
{@render formFields()}
|
|
248
264
|
</form>
|
|
249
265
|
|
|
@@ -253,12 +269,12 @@
|
|
|
253
269
|
</Modal>
|
|
254
270
|
{:else}
|
|
255
271
|
<Drawer bind:open {title} position='right' {size} width={drawerWidth} onclose={handleClose} closeOnBackdrop={false}>
|
|
256
|
-
<form onsubmit={handleSubmit} class=
|
|
272
|
+
<form onsubmit={handleSubmit} class={drawerFormClass}>
|
|
257
273
|
{@render formFields()}
|
|
258
274
|
</form>
|
|
259
275
|
|
|
260
276
|
<!-- 底部操作栏 -->
|
|
261
|
-
<div class=
|
|
277
|
+
<div class={drawerFooterClass}>
|
|
262
278
|
{@render footerActions()}
|
|
263
279
|
</div>
|
|
264
280
|
</Drawer>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { Size } from '../../../types.js';
|
|
3
|
-
import type { CrudFormVariant } from './crud-types.js';
|
|
3
|
+
import type { CrudDensity, CrudFormVariant } from './crud-types.js';
|
|
4
4
|
type FieldDef = {
|
|
5
5
|
id: string;
|
|
6
6
|
label: string | (() => string);
|
|
@@ -28,6 +28,7 @@ type $$ComponentProps = {
|
|
|
28
28
|
mode?: 'create' | 'edit';
|
|
29
29
|
fields?: FieldDef[];
|
|
30
30
|
formData?: Record<string, unknown>;
|
|
31
|
+
density?: CrudDensity;
|
|
31
32
|
title?: string;
|
|
32
33
|
variant?: CrudFormVariant;
|
|
33
34
|
size?: Size;
|
|
@@ -57,15 +57,16 @@
|
|
|
57
57
|
}
|
|
58
58
|
</script>
|
|
59
59
|
|
|
60
|
-
<div class='
|
|
61
|
-
<div class='flex
|
|
60
|
+
<div class='grid gap-3 md:grid-cols-[minmax(0,1fr)_auto] md:items-center'>
|
|
61
|
+
<div class='flex min-w-0 flex-wrap items-center gap-3 [&_.fieldset]:m-0 [&_.fieldset]:min-w-0'>
|
|
62
62
|
{#if searchable}
|
|
63
|
-
<div class='flex-
|
|
64
|
-
<span class='icon-[tabler--search]
|
|
63
|
+
<div class='relative min-w-[15rem] flex-[1_1_20rem]'>
|
|
64
|
+
<span class='icon-[tabler--search] pointer-events-none absolute left-3.5 top-1/2 z-10 size-4 -translate-y-1/2 text-base-content/35'></span>
|
|
65
65
|
<Input
|
|
66
66
|
type='text'
|
|
67
|
+
size='sm'
|
|
67
68
|
placeholder={searchPlaceholder || uiM('crud_search_placeholder')}
|
|
68
|
-
class='pl-10'
|
|
69
|
+
class='pl-10 shadow-none'
|
|
69
70
|
bind:value={searchValue}
|
|
70
71
|
oninput={handleSearchInput}
|
|
71
72
|
autocomplete='off'
|
|
@@ -76,21 +77,24 @@
|
|
|
76
77
|
{#each filterFields as field (field.id)}
|
|
77
78
|
{@const opts = resolveOptions(field.options)}
|
|
78
79
|
{#if opts.length > 0}
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
<option value={
|
|
87
|
-
|
|
88
|
-
|
|
80
|
+
<div class='w-full sm:w-[11rem] sm:flex-none'>
|
|
81
|
+
<Select
|
|
82
|
+
size='sm'
|
|
83
|
+
class='border-base-content/10 bg-base-100 shadow-none'
|
|
84
|
+
value={String(filterValues[field.id] ?? '')}
|
|
85
|
+
onchange={value => handleFilterChange(field.id, value)}
|
|
86
|
+
>
|
|
87
|
+
<option value="">{uiM('crud_filter_all')}</option>
|
|
88
|
+
{#each opts as opt (String(opt.value))}
|
|
89
|
+
<option value={String(opt.value)}>{opt.label}</option>
|
|
90
|
+
{/each}
|
|
91
|
+
</Select>
|
|
92
|
+
</div>
|
|
89
93
|
{/if}
|
|
90
94
|
{/each}
|
|
95
|
+
</div>
|
|
91
96
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
</div>
|
|
97
|
+
<div class='inline-flex items-center justify-self-start rounded-full border border-base-content/8 bg-base-200/60 px-3 py-1 text-xs font-medium text-base-content/60 md:justify-self-end'>
|
|
98
|
+
{uiM('crud_total', { count: total })}
|
|
95
99
|
</div>
|
|
96
100
|
</div>
|