@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.
Files changed (28) hide show
  1. package/README.md +41 -5
  2. package/auto-import.d.ts +2 -2
  3. package/auto-import.js +2 -2
  4. package/dist/components/compounds/DataTable.svelte +4 -1
  5. package/dist/components/compounds/DataTable.svelte.d.ts +2 -0
  6. package/dist/components/compounds/Drawer.svelte +7 -2
  7. package/dist/components/compounds/Modal.svelte +2 -0
  8. package/dist/components/compounds/Pagination.svelte +35 -8
  9. package/dist/components/scenes/ai/AiDocumentEditor.svelte +102 -0
  10. package/dist/components/scenes/ai/code-preview.js +9 -0
  11. package/dist/components/scenes/ai/document-parse.js +11 -0
  12. package/dist/components/scenes/ai/document-types.d.ts +1 -1
  13. package/dist/components/scenes/ai/mermaid-render.d.ts +14 -0
  14. package/dist/components/scenes/ai/mermaid-render.js +42 -0
  15. package/dist/components/scenes/crud/{CrudDetailDrawer.svelte → CrudDetailPanel.svelte} +60 -23
  16. package/dist/components/scenes/crud/{CrudDetailDrawer.svelte.d.ts → CrudDetailPanel.svelte.d.ts} +12 -6
  17. package/dist/components/scenes/crud/CrudEditPanel.svelte +265 -0
  18. package/dist/components/scenes/crud/{CrudEditDrawer.svelte.d.ts → CrudEditPanel.svelte.d.ts} +12 -6
  19. package/dist/components/scenes/crud/CrudPage.svelte +84 -49
  20. package/dist/components/scenes/crud/CrudPage.svelte.d.ts +6 -4
  21. package/dist/components/scenes/crud/crud-types.d.ts +36 -2
  22. package/dist/components/scenes/crud/index.d.ts +3 -3
  23. package/dist/components/scenes/crud/index.js +3 -3
  24. package/dist/messages/en-US.json +4 -2
  25. package/dist/messages/zh-CN.json +4 -2
  26. package/dist/types.d.ts +10 -0
  27. package/package.json +4 -3
  28. package/dist/components/scenes/crud/CrudEditDrawer.svelte +0 -225
@@ -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
- * CrudDetailDrawer
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 CrudDetailDrawer: import("svelte").Component<$$ComponentProps, {}, "open">;
37
- type CrudDetailDrawer = ReturnType<typeof CrudDetailDrawer>;
38
- export default CrudDetailDrawer;
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}
@@ -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
- * CrudEditDrawer
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 CrudEditDrawer: import("svelte").Component<$$ComponentProps, {}, "open" | "formData">;
47
- type CrudEditDrawer = ReturnType<typeof CrudEditDrawer>;
48
- export default CrudEditDrawer;
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 { Size } from '../../../types.js'
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 CrudDetailDrawer from './CrudDetailDrawer.svelte'
27
- import CrudEditDrawer from './CrudEditDrawer.svelte'
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
- drawerSize = '2xl' as Size,
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
- drawerSize?: Size
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 drawerMode = $state<'detail' | 'edit' | 'create' | null>(null)
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
- drawerMode = 'detail'
248
+ panelMode = 'detail'
227
249
  }
228
250
 
229
251
  function openCreate() {
230
252
  selectedItem = null
231
253
  formData = { ...crud.getDefaultValues() }
232
254
  formError = ''
233
- drawerMode = 'create'
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
- drawerMode = 'edit'
262
+ panelMode = 'edit'
241
263
  }
242
264
 
243
- function closeDrawer() {
244
- drawerMode = null
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 (drawerMode === 'create' && crud.api.create) {
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
- closeDrawer()
287
+ closePanel()
266
288
  onaftersubmit?.(submitted, 'create')
267
289
  await nav.refresh?.()
268
290
  }
269
- else if (drawerMode === 'edit' && crud.api.update && selectedItem) {
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
- closeDrawer()
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
- closeDrawer()
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 drawerTitle = $derived(
351
- drawerMode === 'create'
372
+ const panelTitle = $derived(
373
+ panelMode === 'create'
352
374
  ? `${uiM('crud_create')}${resourceLabel}`
353
- : drawerMode === 'edit'
375
+ : panelMode === 'edit'
354
376
  ? `${uiM('crud_edit')}${resourceLabel}`
355
377
  : `${resourceLabel}${uiM('crud_detail')}`,
356
378
  )
357
379
 
358
- // 抽屉 open 状态
359
- const detailOpen = $derived(drawerMode === 'detail')
360
- const editOpen = $derived(drawerMode === 'create' || drawerMode === 'edit')
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
- {#if data.total > data.pageSize}
456
- <div class='flex justify-center p-4 border-t border-base-content/5'>
457
- <Pagination
458
- page={data.page}
459
- total={data.total}
460
- pageSize={data.pageSize}
461
- size='sm'
462
- showTotal
463
- onchange={handlePageChange}
464
- />
465
- </div>
466
- {/if}
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
- <CrudDetailDrawer
495
+ <!-- 详情:抽屉或弹窗 -->
496
+ <CrudDetailPanel
472
497
  open={detailOpen}
473
498
  item={selectedItem}
474
499
  fields={detailFields}
475
- title={drawerTitle}
476
- size={drawerSize}
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={closeDrawer}
509
+ onclose={closePanel}
480
510
  {detailExtra}
481
511
  />
482
512
 
483
- <!-- 编辑/新建抽屉 -->
484
- <CrudEditDrawer
513
+ <!-- 编辑/新建:抽屉或弹窗 -->
514
+ <CrudEditPanel
485
515
  open={editOpen}
486
- mode={drawerMode === 'create' ? 'create' : 'edit'}
487
- fields={drawerMode === 'create' ? createFields : editFields}
516
+ mode={panelMode === 'create' ? 'create' : 'edit'}
517
+ fields={panelMode === 'create' ? createFields : editFields}
488
518
  bind:formData
489
- title={drawerTitle}
490
- size={drawerSize}
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={closeDrawer}
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 { Size } from '../../../types.js';
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
- drawerSize?: Size;
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 }} />