@h-ai/ui 0.1.0-alpha.21 → 0.1.0-alpha.23
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 +41 -5
- package/auto-import.d.ts +2 -2
- package/auto-import.js +2 -2
- package/dist/components/compounds/DataTable.svelte +4 -1
- package/dist/components/compounds/DataTable.svelte.d.ts +2 -0
- package/dist/components/compounds/Drawer.svelte +7 -2
- package/dist/components/compounds/Modal.svelte +2 -0
- package/dist/components/compounds/Pagination.svelte +35 -8
- package/dist/components/scenes/ai/AiDocumentEditor.svelte +102 -0
- package/dist/components/scenes/ai/code-preview.js +9 -0
- package/dist/components/scenes/ai/document-parse.js +11 -0
- package/dist/components/scenes/ai/document-types.d.ts +1 -1
- package/dist/components/scenes/ai/mermaid-render.d.ts +14 -0
- package/dist/components/scenes/ai/mermaid-render.js +42 -0
- package/dist/components/scenes/crud/{CrudDetailDrawer.svelte → CrudDetailPanel.svelte} +60 -23
- package/dist/components/scenes/crud/{CrudDetailDrawer.svelte.d.ts → CrudDetailPanel.svelte.d.ts} +12 -6
- package/dist/components/scenes/crud/CrudEditPanel.svelte +265 -0
- package/dist/components/scenes/crud/{CrudEditDrawer.svelte.d.ts → CrudEditPanel.svelte.d.ts} +12 -6
- package/dist/components/scenes/crud/CrudPage.svelte +84 -49
- package/dist/components/scenes/crud/CrudPage.svelte.d.ts +6 -4
- package/dist/components/scenes/crud/crud-types.d.ts +36 -2
- package/dist/components/scenes/crud/index.d.ts +3 -3
- package/dist/components/scenes/crud/index.js +3 -3
- package/dist/messages/en-US.json +4 -2
- package/dist/messages/zh-CN.json +4 -2
- package/dist/types.d.ts +10 -0
- package/package.json +4 -3
- package/dist/components/scenes/crud/CrudEditDrawer.svelte +0 -225
package/dist/components/scenes/crud/{CrudDetailDrawer.svelte.d.ts → CrudDetailPanel.svelte.d.ts}
RENAMED
|
@@ -1,5 +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
4
|
type FieldDef = {
|
|
4
5
|
id: string;
|
|
5
6
|
label: string | (() => string);
|
|
@@ -20,19 +21,24 @@ type $$ComponentProps = {
|
|
|
20
21
|
item?: Record<string, unknown> | null;
|
|
21
22
|
fields?: FieldDef[];
|
|
22
23
|
title?: string;
|
|
24
|
+
variant?: CrudFormVariant;
|
|
23
25
|
size?: Size;
|
|
26
|
+
drawerWidth?: string;
|
|
27
|
+
modalSize?: Size | 'full';
|
|
28
|
+
modalWidth?: string;
|
|
29
|
+
modalHeight?: string;
|
|
24
30
|
canEdit?: boolean;
|
|
25
31
|
onedit?: () => void;
|
|
26
32
|
onclose?: () => void;
|
|
27
33
|
detailExtra?: Snippet<[Record<string, unknown>]>;
|
|
28
34
|
};
|
|
29
35
|
/**
|
|
30
|
-
*
|
|
31
|
-
* CRUD
|
|
36
|
+
* CrudDetailPanel
|
|
37
|
+
* CRUD 详情组件(只读展示),支持抽屉(drawer)与弹窗(modal)两种展示形式
|
|
32
38
|
*
|
|
33
39
|
* 使用 Svelte 5 Runes ($props, $derived)
|
|
34
|
-
* 使用 compounds 组件:Drawer, Badge
|
|
40
|
+
* 使用 compounds 组件:Drawer, Modal, Badge
|
|
35
41
|
*/
|
|
36
|
-
declare const
|
|
37
|
-
type
|
|
38
|
-
export default
|
|
42
|
+
declare const CrudDetailPanel: import("svelte").Component<$$ComponentProps, {}, "open">;
|
|
43
|
+
type CrudDetailPanel = ReturnType<typeof CrudDetailPanel>;
|
|
44
|
+
export default CrudDetailPanel;
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component CrudEditPanel
|
|
3
|
+
CRUD 编辑/新建表单组件,支持抽屉(drawer)与弹窗(modal)两种展示形式
|
|
4
|
+
|
|
5
|
+
使用 Svelte 5 Runes ($props, $state, $derived)
|
|
6
|
+
使用 compounds/primitives 组件:Drawer, Modal, FormField, Input, Select, Textarea, Checkbox, Button
|
|
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 FormField from '../../compounds/FormField.svelte'
|
|
15
|
+
import Modal from '../../compounds/Modal.svelte'
|
|
16
|
+
import Button from '../../primitives/Button.svelte'
|
|
17
|
+
import Checkbox from '../../primitives/Checkbox.svelte'
|
|
18
|
+
import Input from '../../primitives/Input.svelte'
|
|
19
|
+
import Select from '../../primitives/Select.svelte'
|
|
20
|
+
import Textarea from '../../primitives/Textarea.svelte'
|
|
21
|
+
|
|
22
|
+
type FieldDef = {
|
|
23
|
+
id: string
|
|
24
|
+
label: string | (() => string)
|
|
25
|
+
type: string
|
|
26
|
+
readonly?: boolean
|
|
27
|
+
options?: Array<{ label: string, value: string | number | boolean }> | (() => Array<{ label: string, value: string | number | boolean }>)
|
|
28
|
+
validation?: { required?: boolean, min?: number, max?: number, pattern?: string, message?: string }
|
|
29
|
+
placeholder?: string | (() => string)
|
|
30
|
+
order?: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let {
|
|
34
|
+
open = $bindable(false),
|
|
35
|
+
mode = 'create' as 'create' | 'edit',
|
|
36
|
+
fields = [],
|
|
37
|
+
formData = $bindable<Record<string, unknown>>({}),
|
|
38
|
+
title = '',
|
|
39
|
+
variant = 'drawer' as CrudFormVariant,
|
|
40
|
+
size = '2xl' as Size,
|
|
41
|
+
drawerWidth,
|
|
42
|
+
modalSize = '2xl' as Size | 'full',
|
|
43
|
+
modalWidth,
|
|
44
|
+
modalHeight,
|
|
45
|
+
submitting = false,
|
|
46
|
+
error = '',
|
|
47
|
+
onsubmit,
|
|
48
|
+
onclose,
|
|
49
|
+
editFormExtra,
|
|
50
|
+
editingItem = null,
|
|
51
|
+
}: {
|
|
52
|
+
open?: boolean
|
|
53
|
+
mode?: 'create' | 'edit'
|
|
54
|
+
fields?: FieldDef[]
|
|
55
|
+
formData?: Record<string, unknown>
|
|
56
|
+
title?: string
|
|
57
|
+
variant?: CrudFormVariant
|
|
58
|
+
size?: Size
|
|
59
|
+
drawerWidth?: string
|
|
60
|
+
modalSize?: Size | 'full'
|
|
61
|
+
modalWidth?: string
|
|
62
|
+
modalHeight?: string
|
|
63
|
+
submitting?: boolean
|
|
64
|
+
error?: string
|
|
65
|
+
onsubmit?: (data: Record<string, unknown>) => Promise<void>
|
|
66
|
+
onclose?: () => void
|
|
67
|
+
editFormExtra?: Snippet<[Record<string, unknown> | null, 'create' | 'edit']>
|
|
68
|
+
editingItem?: Record<string, unknown> | null
|
|
69
|
+
} = $props()
|
|
70
|
+
|
|
71
|
+
function resolveText(text: string | (() => string)): string {
|
|
72
|
+
return typeof text === 'function' ? text() : text
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function resolveOptions(options?: FieldDef['options']): Array<{ label: string, value: string | number | boolean }> {
|
|
76
|
+
if (!options)
|
|
77
|
+
return []
|
|
78
|
+
return typeof options === 'function' ? options() : options
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const sortedFields = $derived(
|
|
82
|
+
[...fields].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
function handleClose() {
|
|
86
|
+
open = false
|
|
87
|
+
onclose?.()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function handleSubmit(e: Event) {
|
|
91
|
+
e.preventDefault()
|
|
92
|
+
await onsubmit?.(formData)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getInputType(fieldType: string): 'email' | 'url' | 'tel' | 'password' | 'number' | 'text' {
|
|
96
|
+
switch (fieldType) {
|
|
97
|
+
case 'email': return 'email'
|
|
98
|
+
case 'url': return 'url'
|
|
99
|
+
case 'tel': return 'tel'
|
|
100
|
+
case 'password': return 'password'
|
|
101
|
+
case 'number': return 'number'
|
|
102
|
+
case 'date': return 'text'
|
|
103
|
+
case 'datetime': return 'text'
|
|
104
|
+
default: return 'text'
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function updateField(fieldId: string, value: unknown) {
|
|
109
|
+
formData = { ...formData, [fieldId]: value }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function toggleMultiSelect(fieldId: string, optValue: string | number | boolean) {
|
|
113
|
+
const current = (formData[fieldId] as Array<string | number | boolean>) ?? []
|
|
114
|
+
const strVal = String(optValue)
|
|
115
|
+
if (current.map(String).includes(strVal)) {
|
|
116
|
+
formData = { ...formData, [fieldId]: current.filter(v => String(v) !== strVal) }
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
formData = { ...formData, [fieldId]: [...current, optValue] }
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
</script>
|
|
123
|
+
|
|
124
|
+
{#snippet formFields()}
|
|
125
|
+
{#if error}
|
|
126
|
+
<div class='p-3 bg-error/10 border border-error/20 rounded-lg text-sm text-error flex items-center gap-2'>
|
|
127
|
+
<span class='icon-[tabler--alert-circle] size-4 shrink-0'></span>
|
|
128
|
+
<span>{error}</span>
|
|
129
|
+
</div>
|
|
130
|
+
{/if}
|
|
131
|
+
|
|
132
|
+
{#each sortedFields as field (field.id)}
|
|
133
|
+
{@const isReadonly = field.readonly && mode === 'edit'}
|
|
134
|
+
{@const isRequired = field.validation?.required}
|
|
135
|
+
{@const fieldValue = formData[field.id]}
|
|
136
|
+
{@const placeholder = field.placeholder ? resolveText(field.placeholder) : ''}
|
|
137
|
+
|
|
138
|
+
{#if field.type === 'textarea'}
|
|
139
|
+
<FormField label={resolveText(field.label)} required={isRequired}>
|
|
140
|
+
<Textarea
|
|
141
|
+
id={field.id}
|
|
142
|
+
value={String(fieldValue ?? '')}
|
|
143
|
+
{placeholder}
|
|
144
|
+
disabled={submitting || isReadonly}
|
|
145
|
+
required={isRequired}
|
|
146
|
+
oninput={e => updateField(field.id, (e.target as HTMLTextAreaElement).value)}
|
|
147
|
+
/>
|
|
148
|
+
</FormField>
|
|
149
|
+
|
|
150
|
+
{:else if field.type === 'select' || field.type === 'radio'}
|
|
151
|
+
{@const opts = resolveOptions(field.options)}
|
|
152
|
+
<FormField label={resolveText(field.label)} required={isRequired}>
|
|
153
|
+
<Select
|
|
154
|
+
id={field.id}
|
|
155
|
+
value={String(fieldValue ?? '')}
|
|
156
|
+
disabled={submitting || isReadonly}
|
|
157
|
+
onchange={value => updateField(field.id, value)}
|
|
158
|
+
>
|
|
159
|
+
<option value="">{placeholder || uiM('crud_filter_all')}</option>
|
|
160
|
+
{#each opts as opt (String(opt.value))}
|
|
161
|
+
<option value={String(opt.value)}>{opt.label}</option>
|
|
162
|
+
{/each}
|
|
163
|
+
</Select>
|
|
164
|
+
</FormField>
|
|
165
|
+
|
|
166
|
+
{:else if field.type === 'multi-select'}
|
|
167
|
+
{@const opts = resolveOptions(field.options)}
|
|
168
|
+
{@const selectedValues = ((fieldValue ?? []) as Array<string | number | boolean>).map(String)}
|
|
169
|
+
<FormField label={resolveText(field.label)} required={isRequired}>
|
|
170
|
+
<div class='flex flex-wrap gap-3 p-3 bg-base-200 rounded-lg max-h-48 overflow-y-auto'>
|
|
171
|
+
{#each opts as opt (String(opt.value))}
|
|
172
|
+
<label class='inline-flex items-center gap-2 cursor-pointer'>
|
|
173
|
+
<Checkbox
|
|
174
|
+
size='sm'
|
|
175
|
+
checked={selectedValues.includes(String(opt.value))}
|
|
176
|
+
onchange={() => toggleMultiSelect(field.id, opt.value)}
|
|
177
|
+
disabled={submitting || isReadonly}
|
|
178
|
+
/>
|
|
179
|
+
<span class='text-sm text-base-content'>{opt.label}</span>
|
|
180
|
+
</label>
|
|
181
|
+
{/each}
|
|
182
|
+
{#if opts.length === 0}
|
|
183
|
+
<span class='text-sm text-base-content/60'>{uiM('crud_no_data')}</span>
|
|
184
|
+
{/if}
|
|
185
|
+
</div>
|
|
186
|
+
</FormField>
|
|
187
|
+
|
|
188
|
+
{:else if field.type === 'boolean' || field.type === 'checkbox'}
|
|
189
|
+
<FormField label={resolveText(field.label)}>
|
|
190
|
+
<Checkbox
|
|
191
|
+
checked={Boolean(fieldValue)}
|
|
192
|
+
onchange={(checked: boolean) => updateField(field.id, checked)}
|
|
193
|
+
disabled={submitting || isReadonly}
|
|
194
|
+
/>
|
|
195
|
+
</FormField>
|
|
196
|
+
|
|
197
|
+
{:else}
|
|
198
|
+
<FormField label={resolveText(field.label)} required={isRequired}>
|
|
199
|
+
<Input
|
|
200
|
+
id={field.id}
|
|
201
|
+
type={getInputType(field.type)}
|
|
202
|
+
value={String(fieldValue ?? '')}
|
|
203
|
+
{placeholder}
|
|
204
|
+
disabled={submitting || isReadonly}
|
|
205
|
+
required={isRequired}
|
|
206
|
+
pattern={field.validation?.pattern}
|
|
207
|
+
minlength={field.type !== 'number' ? field.validation?.min : undefined}
|
|
208
|
+
maxlength={field.type !== 'number' ? field.validation?.max : undefined}
|
|
209
|
+
min={field.type === 'number' ? field.validation?.min : undefined}
|
|
210
|
+
max={field.type === 'number' ? field.validation?.max : undefined}
|
|
211
|
+
oninput={e => updateField(field.id, field.type === 'number' ? Number((e.currentTarget as HTMLInputElement).value) : (e.currentTarget as HTMLInputElement).value)}
|
|
212
|
+
/>
|
|
213
|
+
</FormField>
|
|
214
|
+
{/if}
|
|
215
|
+
{/each}
|
|
216
|
+
|
|
217
|
+
{#if editFormExtra}
|
|
218
|
+
<div class='border-t border-base-content/5 pt-4'>
|
|
219
|
+
{@render editFormExtra(editingItem, mode)}
|
|
220
|
+
</div>
|
|
221
|
+
{/if}
|
|
222
|
+
{/snippet}
|
|
223
|
+
|
|
224
|
+
{#snippet footerActions()}
|
|
225
|
+
<Button variant='ghost' onclick={handleClose} disabled={submitting}>
|
|
226
|
+
{uiM('crud_cancel')}
|
|
227
|
+
</Button>
|
|
228
|
+
<Button variant='primary' onclick={handleSubmit} disabled={submitting}>
|
|
229
|
+
{#if submitting}
|
|
230
|
+
<span class='loading loading-spinner loading-xs mr-2'></span>
|
|
231
|
+
{/if}
|
|
232
|
+
{mode === 'create' ? uiM('crud_create') : uiM('crud_save')}
|
|
233
|
+
</Button>
|
|
234
|
+
{/snippet}
|
|
235
|
+
|
|
236
|
+
{#if variant === 'modal'}
|
|
237
|
+
<Modal
|
|
238
|
+
bind:open
|
|
239
|
+
{title}
|
|
240
|
+
size={modalSize}
|
|
241
|
+
width={modalWidth}
|
|
242
|
+
height={modalHeight}
|
|
243
|
+
closeOnBackdrop={false}
|
|
244
|
+
onclose={handleClose}
|
|
245
|
+
>
|
|
246
|
+
<form onsubmit={handleSubmit} class='space-y-4'>
|
|
247
|
+
{@render formFields()}
|
|
248
|
+
</form>
|
|
249
|
+
|
|
250
|
+
{#snippet footer()}
|
|
251
|
+
{@render footerActions()}
|
|
252
|
+
{/snippet}
|
|
253
|
+
</Modal>
|
|
254
|
+
{:else}
|
|
255
|
+
<Drawer bind:open {title} position='right' {size} width={drawerWidth} onclose={handleClose} closeOnBackdrop={false}>
|
|
256
|
+
<form onsubmit={handleSubmit} class='space-y-4 pb-20'>
|
|
257
|
+
{@render formFields()}
|
|
258
|
+
</form>
|
|
259
|
+
|
|
260
|
+
<!-- 底部操作栏 -->
|
|
261
|
+
<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'>
|
|
262
|
+
{@render footerActions()}
|
|
263
|
+
</div>
|
|
264
|
+
</Drawer>
|
|
265
|
+
{/if}
|
package/dist/components/scenes/crud/{CrudEditDrawer.svelte.d.ts → CrudEditPanel.svelte.d.ts}
RENAMED
|
@@ -1,5 +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
4
|
type FieldDef = {
|
|
4
5
|
id: string;
|
|
5
6
|
label: string | (() => string);
|
|
@@ -28,7 +29,12 @@ type $$ComponentProps = {
|
|
|
28
29
|
fields?: FieldDef[];
|
|
29
30
|
formData?: Record<string, unknown>;
|
|
30
31
|
title?: string;
|
|
32
|
+
variant?: CrudFormVariant;
|
|
31
33
|
size?: Size;
|
|
34
|
+
drawerWidth?: string;
|
|
35
|
+
modalSize?: Size | 'full';
|
|
36
|
+
modalWidth?: string;
|
|
37
|
+
modalHeight?: string;
|
|
32
38
|
submitting?: boolean;
|
|
33
39
|
error?: string;
|
|
34
40
|
onsubmit?: (data: Record<string, unknown>) => Promise<void>;
|
|
@@ -37,12 +43,12 @@ type $$ComponentProps = {
|
|
|
37
43
|
editingItem?: Record<string, unknown> | null;
|
|
38
44
|
};
|
|
39
45
|
/**
|
|
40
|
-
*
|
|
41
|
-
* CRUD
|
|
46
|
+
* CrudEditPanel
|
|
47
|
+
* CRUD 编辑/新建表单组件,支持抽屉(drawer)与弹窗(modal)两种展示形式
|
|
42
48
|
*
|
|
43
49
|
* 使用 Svelte 5 Runes ($props, $state, $derived)
|
|
44
|
-
* 使用 compounds/primitives 组件:Drawer, FormField, Input, Select, Textarea, Checkbox, Button
|
|
50
|
+
* 使用 compounds/primitives 组件:Drawer, Modal, FormField, Input, Select, Textarea, Checkbox, Button
|
|
45
51
|
*/
|
|
46
|
-
declare const
|
|
47
|
-
type
|
|
48
|
-
export default
|
|
52
|
+
declare const CrudEditPanel: import("svelte").Component<$$ComponentProps, {}, "open" | "formData">;
|
|
53
|
+
type CrudEditPanel = ReturnType<typeof CrudEditPanel>;
|
|
54
|
+
export default CrudEditPanel;
|
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
@component CrudPage
|
|
3
3
|
通用 CRUD 页面组件
|
|
4
4
|
|
|
5
|
-
基于声明式资源定义,自动生成列表 + 搜索过滤 + 分页 +
|
|
5
|
+
基于声明式资源定义,自动生成列表 + 搜索过滤 + 分页 + 详情查看 + 编辑/新建面板 + 删除确认。
|
|
6
6
|
|
|
7
7
|
使用 Svelte 5 Runes ($props, $state, $derived, $effect)
|
|
8
|
-
使用 compounds 组件:Card, DataTable, PageHeader, Pagination, Drawer
|
|
8
|
+
使用 compounds 组件:Card, DataTable, PageHeader, Pagination, Drawer, Modal
|
|
9
9
|
|
|
10
10
|
@example
|
|
11
11
|
<CrudPage crud={userCrud} data={data} permissions={{ create: true, update: true, delete: true }} />
|
|
12
12
|
-->
|
|
13
13
|
<script lang='ts'>
|
|
14
14
|
import type { Snippet } from 'svelte'
|
|
15
|
-
import type {
|
|
15
|
+
import type { CrudDensity, CrudFormConfig, CrudPaginationConfig } from './crud-types.js'
|
|
16
16
|
import type { NavAdapter } from './nav-adapter.js'
|
|
17
17
|
import { SvelteURLSearchParams } from 'svelte/reactivity'
|
|
18
18
|
import { uiM } from '../../../messages.js'
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
import Button from '../../primitives/Button.svelte'
|
|
24
24
|
import IconButton from '../../primitives/IconButton.svelte'
|
|
25
25
|
import CrudDeleteConfirm from './CrudDeleteConfirm.svelte'
|
|
26
|
-
import
|
|
27
|
-
import
|
|
26
|
+
import CrudDetailPanel from './CrudDetailPanel.svelte'
|
|
27
|
+
import CrudEditPanel from './CrudEditPanel.svelte'
|
|
28
28
|
import CrudFilterBar from './CrudFilterBar.svelte'
|
|
29
29
|
import { createBrowserNavAdapter } from './nav-adapter.js'
|
|
30
30
|
|
|
@@ -87,7 +87,9 @@
|
|
|
87
87
|
crud,
|
|
88
88
|
data,
|
|
89
89
|
permissions = {},
|
|
90
|
-
|
|
90
|
+
form = {},
|
|
91
|
+
pagination = {},
|
|
92
|
+
density = 'normal',
|
|
91
93
|
rowClickDetail = true,
|
|
92
94
|
listItemActions,
|
|
93
95
|
editFormExtra,
|
|
@@ -104,7 +106,9 @@
|
|
|
104
106
|
crud: CrudDef
|
|
105
107
|
data: { items: Record<string, unknown>[], total: number, page: number, pageSize: number, filters?: Record<string, unknown> }
|
|
106
108
|
permissions?: { create?: boolean, update?: boolean, delete?: boolean }
|
|
107
|
-
|
|
109
|
+
form?: CrudFormConfig
|
|
110
|
+
pagination?: CrudPaginationConfig
|
|
111
|
+
density?: CrudDensity
|
|
108
112
|
rowClickDetail?: boolean
|
|
109
113
|
listItemActions?: Snippet<[Record<string, unknown>]>
|
|
110
114
|
editFormExtra?: Snippet<[Record<string, unknown> | null, 'create' | 'edit']>
|
|
@@ -134,8 +138,22 @@
|
|
|
134
138
|
const canDelete = $derived(permissions.delete !== false && Boolean(crud.api.remove))
|
|
135
139
|
const searchable = $derived(crud.resource.searchable !== false)
|
|
136
140
|
|
|
141
|
+
// 表单展示配置(抽屉 / 弹窗)
|
|
142
|
+
const formVariant = $derived(form.variant ?? 'drawer')
|
|
143
|
+
const formDrawerSize = $derived(form.drawerSize ?? '2xl')
|
|
144
|
+
const formDrawerWidth = $derived(form.drawerWidth)
|
|
145
|
+
const formModalSize = $derived(form.modalSize ?? '2xl')
|
|
146
|
+
const formModalWidth = $derived(form.modalWidth)
|
|
147
|
+
const formModalHeight = $derived(form.modalHeight)
|
|
148
|
+
|
|
149
|
+
// 分页栏配置
|
|
150
|
+
const paginationShowSizeChanger = $derived(pagination.showSizeChanger !== false)
|
|
151
|
+
const paginationShowJumper = $derived(pagination.showJumper !== false)
|
|
152
|
+
const paginationShowTotal = $derived(pagination.showTotal !== false)
|
|
153
|
+
const paginationPageSizeOptions = $derived(pagination.pageSizeOptions ?? [10, 20, 50, 100])
|
|
154
|
+
|
|
137
155
|
// 视图状态
|
|
138
|
-
let
|
|
156
|
+
let panelMode = $state<'detail' | 'edit' | 'create' | null>(null)
|
|
139
157
|
let selectedItem = $state<Record<string, unknown> | null>(null)
|
|
140
158
|
let formData = $state<Record<string, unknown>>({})
|
|
141
159
|
let submitting = $state(false)
|
|
@@ -219,29 +237,33 @@
|
|
|
219
237
|
navigateWithParams({ page: newPage })
|
|
220
238
|
}
|
|
221
239
|
|
|
240
|
+
function handlePageSizeChange(newPageSize: number) {
|
|
241
|
+
navigateWithParams({ pageSize: newPageSize, page: 1 })
|
|
242
|
+
}
|
|
243
|
+
|
|
222
244
|
// ─── 打开/关闭抽屉 ───
|
|
223
245
|
|
|
224
246
|
function openDetail(item: Record<string, unknown>) {
|
|
225
247
|
selectedItem = item
|
|
226
|
-
|
|
248
|
+
panelMode = 'detail'
|
|
227
249
|
}
|
|
228
250
|
|
|
229
251
|
function openCreate() {
|
|
230
252
|
selectedItem = null
|
|
231
253
|
formData = { ...crud.getDefaultValues() }
|
|
232
254
|
formError = ''
|
|
233
|
-
|
|
255
|
+
panelMode = 'create'
|
|
234
256
|
}
|
|
235
257
|
|
|
236
258
|
function openEdit(item: Record<string, unknown>) {
|
|
237
259
|
selectedItem = item
|
|
238
260
|
formData = { ...item }
|
|
239
261
|
formError = ''
|
|
240
|
-
|
|
262
|
+
panelMode = 'edit'
|
|
241
263
|
}
|
|
242
264
|
|
|
243
|
-
function
|
|
244
|
-
|
|
265
|
+
function closePanel() {
|
|
266
|
+
panelMode = null
|
|
245
267
|
selectedItem = null
|
|
246
268
|
formError = ''
|
|
247
269
|
}
|
|
@@ -259,18 +281,18 @@
|
|
|
259
281
|
submitting = true
|
|
260
282
|
|
|
261
283
|
try {
|
|
262
|
-
if (
|
|
284
|
+
if (panelMode === 'create' && crud.api.create) {
|
|
263
285
|
const result = await crud.api.create(submitData)
|
|
264
286
|
const submitted = (result ?? {}) as Record<string, unknown>
|
|
265
|
-
|
|
287
|
+
closePanel()
|
|
266
288
|
onaftersubmit?.(submitted, 'create')
|
|
267
289
|
await nav.refresh?.()
|
|
268
290
|
}
|
|
269
|
-
else if (
|
|
291
|
+
else if (panelMode === 'edit' && crud.api.update && selectedItem) {
|
|
270
292
|
const id = String(selectedItem[keyField])
|
|
271
293
|
const result = await crud.api.update(id, submitData)
|
|
272
294
|
const submitted = (result ?? {}) as Record<string, unknown>
|
|
273
|
-
|
|
295
|
+
closePanel()
|
|
274
296
|
onaftersubmit?.(submitted, 'edit')
|
|
275
297
|
await nav.refresh?.()
|
|
276
298
|
}
|
|
@@ -314,7 +336,7 @@
|
|
|
314
336
|
deletingItem = null
|
|
315
337
|
// 如果删除的是当前详情/编辑中的项,关闭抽屉
|
|
316
338
|
if (selectedItem && String(selectedItem[keyField]) === id) {
|
|
317
|
-
|
|
339
|
+
closePanel()
|
|
318
340
|
}
|
|
319
341
|
onafterdelete?.(deleted)
|
|
320
342
|
await nav.refresh?.()
|
|
@@ -347,17 +369,17 @@
|
|
|
347
369
|
const filterFields = $derived(crud.getFilterFields() as FieldDef[])
|
|
348
370
|
|
|
349
371
|
// 抽屉标题
|
|
350
|
-
const
|
|
351
|
-
|
|
372
|
+
const panelTitle = $derived(
|
|
373
|
+
panelMode === 'create'
|
|
352
374
|
? `${uiM('crud_create')}${resourceLabel}`
|
|
353
|
-
:
|
|
375
|
+
: panelMode === 'edit'
|
|
354
376
|
? `${uiM('crud_edit')}${resourceLabel}`
|
|
355
377
|
: `${resourceLabel}${uiM('crud_detail')}`,
|
|
356
378
|
)
|
|
357
379
|
|
|
358
|
-
//
|
|
359
|
-
const detailOpen = $derived(
|
|
360
|
-
const editOpen = $derived(
|
|
380
|
+
// 面板 open 状态
|
|
381
|
+
const detailOpen = $derived(panelMode === 'detail')
|
|
382
|
+
const editOpen = $derived(panelMode === 'create' || panelMode === 'edit')
|
|
361
383
|
|
|
362
384
|
// 用于 DataTable 的 columns(含自定义渲染)
|
|
363
385
|
const dtColumns = $derived(
|
|
@@ -412,6 +434,7 @@
|
|
|
412
434
|
columns={dtColumns}
|
|
413
435
|
keyField={keyField}
|
|
414
436
|
loading={false}
|
|
437
|
+
{density}
|
|
415
438
|
>
|
|
416
439
|
{#snippet actions(item)}
|
|
417
440
|
{#if rowClickDetail}
|
|
@@ -451,47 +474,59 @@
|
|
|
451
474
|
{/snippet}
|
|
452
475
|
</DataTable>
|
|
453
476
|
|
|
454
|
-
<!--
|
|
455
|
-
|
|
456
|
-
<
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
477
|
+
<!-- 分页栏:始终显示,支持每页条数选择与跳页 -->
|
|
478
|
+
<div class='flex justify-center p-4 border-t border-base-content/5'>
|
|
479
|
+
<Pagination
|
|
480
|
+
page={data.page}
|
|
481
|
+
total={data.total}
|
|
482
|
+
pageSize={data.pageSize}
|
|
483
|
+
size='sm'
|
|
484
|
+
showTotal={paginationShowTotal}
|
|
485
|
+
showJumper={paginationShowJumper}
|
|
486
|
+
showSizeChanger={paginationShowSizeChanger}
|
|
487
|
+
pageSizeOptions={paginationPageSizeOptions}
|
|
488
|
+
onchange={handlePageChange}
|
|
489
|
+
onpagesizechange={handlePageSizeChange}
|
|
490
|
+
/>
|
|
491
|
+
</div>
|
|
467
492
|
</Card>
|
|
468
493
|
</div>
|
|
469
494
|
|
|
470
|
-
<!--
|
|
471
|
-
<
|
|
495
|
+
<!-- 详情:抽屉或弹窗 -->
|
|
496
|
+
<CrudDetailPanel
|
|
472
497
|
open={detailOpen}
|
|
473
498
|
item={selectedItem}
|
|
474
499
|
fields={detailFields}
|
|
475
|
-
title={
|
|
476
|
-
|
|
500
|
+
title={panelTitle}
|
|
501
|
+
variant={formVariant}
|
|
502
|
+
size={formDrawerSize}
|
|
503
|
+
drawerWidth={formDrawerWidth}
|
|
504
|
+
modalSize={formModalSize}
|
|
505
|
+
modalWidth={formModalWidth}
|
|
506
|
+
modalHeight={formModalHeight}
|
|
477
507
|
canEdit={canUpdate}
|
|
478
508
|
onedit={switchToEdit}
|
|
479
|
-
onclose={
|
|
509
|
+
onclose={closePanel}
|
|
480
510
|
{detailExtra}
|
|
481
511
|
/>
|
|
482
512
|
|
|
483
|
-
<!--
|
|
484
|
-
<
|
|
513
|
+
<!-- 编辑/新建:抽屉或弹窗 -->
|
|
514
|
+
<CrudEditPanel
|
|
485
515
|
open={editOpen}
|
|
486
|
-
mode={
|
|
487
|
-
fields={
|
|
516
|
+
mode={panelMode === 'create' ? 'create' : 'edit'}
|
|
517
|
+
fields={panelMode === 'create' ? createFields : editFields}
|
|
488
518
|
bind:formData
|
|
489
|
-
title={
|
|
490
|
-
|
|
519
|
+
title={panelTitle}
|
|
520
|
+
variant={formVariant}
|
|
521
|
+
size={formDrawerSize}
|
|
522
|
+
drawerWidth={formDrawerWidth}
|
|
523
|
+
modalSize={formModalSize}
|
|
524
|
+
modalWidth={formModalWidth}
|
|
525
|
+
modalHeight={formModalHeight}
|
|
491
526
|
{submitting}
|
|
492
527
|
error={formError}
|
|
493
528
|
onsubmit={handleSubmit}
|
|
494
|
-
onclose={
|
|
529
|
+
onclose={closePanel}
|
|
495
530
|
{editFormExtra}
|
|
496
531
|
editingItem={selectedItem}
|
|
497
532
|
/>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
-
import type {
|
|
2
|
+
import type { CrudDensity, CrudFormConfig, CrudPaginationConfig } from './crud-types.js';
|
|
3
3
|
import type { NavAdapter } from './nav-adapter.js';
|
|
4
4
|
type FieldDef = {
|
|
5
5
|
id: string;
|
|
@@ -84,7 +84,9 @@ type $$ComponentProps = {
|
|
|
84
84
|
update?: boolean;
|
|
85
85
|
delete?: boolean;
|
|
86
86
|
};
|
|
87
|
-
|
|
87
|
+
form?: CrudFormConfig;
|
|
88
|
+
pagination?: CrudPaginationConfig;
|
|
89
|
+
density?: CrudDensity;
|
|
88
90
|
rowClickDetail?: boolean;
|
|
89
91
|
listItemActions?: Snippet<[Record<string, unknown>]>;
|
|
90
92
|
editFormExtra?: Snippet<[Record<string, unknown> | null, 'create' | 'edit']>;
|
|
@@ -108,10 +110,10 @@ type $$ComponentProps = {
|
|
|
108
110
|
* CrudPage
|
|
109
111
|
* 通用 CRUD 页面组件
|
|
110
112
|
*
|
|
111
|
-
* 基于声明式资源定义,自动生成列表 + 搜索过滤 + 分页 +
|
|
113
|
+
* 基于声明式资源定义,自动生成列表 + 搜索过滤 + 分页 + 详情查看 + 编辑/新建面板 + 删除确认。
|
|
112
114
|
*
|
|
113
115
|
* 使用 Svelte 5 Runes ($props, $state, $derived, $effect)
|
|
114
|
-
* 使用 compounds 组件:Card, DataTable, PageHeader, Pagination, Drawer
|
|
116
|
+
* 使用 compounds 组件:Card, DataTable, PageHeader, Pagination, Drawer, Modal
|
|
115
117
|
*
|
|
116
118
|
* @example
|
|
117
119
|
* <CrudPage crud={userCrud} data={data} permissions={{ create: true, update: true, delete: true }} />
|