@h-ai/ui 0.1.0-alpha.23 → 0.1.0-alpha.24
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 +1 -1
- package/dist/components/compounds/Card.svelte +1 -1
- package/dist/components/compounds/DataTable.svelte +29 -7
- 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/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/package.json +2 -2
package/README.md
CHANGED
|
@@ -288,7 +288,7 @@ components/
|
|
|
288
288
|
>
|
|
289
289
|
> `pagination` 配置分页栏(始终显示):`showSizeChanger`、`pageSizeOptions`、`showJumper`、`showTotal` 默认开启。
|
|
290
290
|
>
|
|
291
|
-
> `density` 配置列表密度:`'normal'`(默认)或 `'compact'
|
|
291
|
+
> `density` 配置列表密度:`'normal'`(默认)或 `'compact'`(紧凑)。它只影响列表行、行内操作、分页与创建/编辑/详情表单元素,不影响页头与筛选栏。
|
|
292
292
|
|
|
293
293
|
## 使用示例
|
|
294
294
|
|
|
@@ -57,6 +57,28 @@
|
|
|
57
57
|
|
|
58
58
|
type Column = { key: keyof T | string, label: string, width?: string, align?: 'left' | 'center' | 'right', render?: (item: T) => string }
|
|
59
59
|
|
|
60
|
+
const tableClass = $derived(
|
|
61
|
+
density === 'compact'
|
|
62
|
+
? 'table table-sm w-full text-[12px] [&_thead_th]:px-3 [&_thead_th]:py-2 [&_tbody_td]:px-3 [&_tbody_td]:py-2'
|
|
63
|
+
: 'table w-full text-[13px] [&_thead_th]:px-4 [&_thead_th]:py-2.5 [&_tbody_td]:px-4 [&_tbody_td]:py-3',
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
const headerRowClass = $derived(
|
|
67
|
+
density === 'compact'
|
|
68
|
+
? 'text-[11px] text-base-content/55 border-b border-base-content/6'
|
|
69
|
+
: 'text-[12px] text-base-content/55 border-b border-base-content/6',
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
const rowClass = $derived(
|
|
73
|
+
hoverable
|
|
74
|
+
? 'border-b border-base-content/5 transition-colors hover:bg-base-content/3 last:border-b-0'
|
|
75
|
+
: 'border-b border-base-content/5 last:border-b-0',
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
const actionColumnClass = $derived(density === 'compact' ? 'w-20 text-center font-medium' : 'w-24 text-center font-medium')
|
|
79
|
+
const actionWrapClass = $derived(density === 'compact' ? 'flex items-center justify-center gap-0.5' : 'flex items-center justify-center gap-1')
|
|
80
|
+
const placeholderCellClass = $derived(density === 'compact' ? 'text-center py-6 text-base-content/40' : 'text-center py-8 text-base-content/40')
|
|
81
|
+
|
|
60
82
|
function getValue(item: T, col: Column): string {
|
|
61
83
|
if (col.render) {
|
|
62
84
|
return col.render(item)
|
|
@@ -82,9 +104,9 @@
|
|
|
82
104
|
</script>
|
|
83
105
|
|
|
84
106
|
<div class='overflow-x-auto {className}'>
|
|
85
|
-
<table class=
|
|
107
|
+
<table class={tableClass} class:table-zebra={striped}>
|
|
86
108
|
<thead>
|
|
87
|
-
<tr class=
|
|
109
|
+
<tr class={headerRowClass}>
|
|
88
110
|
{#each columns as col (String(col.key))}
|
|
89
111
|
<th
|
|
90
112
|
style={col.width ? `width: ${col.width}` : ''}
|
|
@@ -94,20 +116,20 @@
|
|
|
94
116
|
</th>
|
|
95
117
|
{/each}
|
|
96
118
|
{#if actions}
|
|
97
|
-
<th class=
|
|
119
|
+
<th class={actionColumnClass}>{uiM('data_table_actions')}</th>
|
|
98
120
|
{/if}
|
|
99
121
|
</tr>
|
|
100
122
|
</thead>
|
|
101
123
|
<tbody>
|
|
102
124
|
{#if loading}
|
|
103
125
|
<tr>
|
|
104
|
-
<td colspan={columns.length + (actions ? 1 : 0)} class=
|
|
126
|
+
<td colspan={columns.length + (actions ? 1 : 0)} class={placeholderCellClass}>
|
|
105
127
|
<span class='loading loading-spinner loading-sm'></span>
|
|
106
128
|
</td>
|
|
107
129
|
</tr>
|
|
108
130
|
{:else if data.length === 0}
|
|
109
131
|
<tr>
|
|
110
|
-
<td colspan={columns.length + (actions ? 1 : 0)} class=
|
|
132
|
+
<td colspan={columns.length + (actions ? 1 : 0)} class={placeholderCellClass}>
|
|
111
133
|
{#if empty}
|
|
112
134
|
{@render empty()}
|
|
113
135
|
{:else}
|
|
@@ -117,13 +139,13 @@
|
|
|
117
139
|
</tr>
|
|
118
140
|
{:else}
|
|
119
141
|
{#each data as item (item[keyField])}
|
|
120
|
-
<tr class={
|
|
142
|
+
<tr class={rowClass}>
|
|
121
143
|
{#each columns as col (String(col.key))}
|
|
122
144
|
<td class={getAlignClass(col.align)}>{getValue(item, col)}</td>
|
|
123
145
|
{/each}
|
|
124
146
|
{#if actions}
|
|
125
147
|
<td>
|
|
126
|
-
<div class=
|
|
148
|
+
<div class={actionWrapClass}>
|
|
127
149
|
{@render actions(item)}
|
|
128
150
|
</div>
|
|
129
151
|
</td>
|
|
@@ -87,6 +87,18 @@
|
|
|
87
87
|
),
|
|
88
88
|
)
|
|
89
89
|
|
|
90
|
+
const controlSize = $derived(size === 'xs' ? 'xs' : 'sm')
|
|
91
|
+
const jumperControlSize = 'xs'
|
|
92
|
+
const wrapperClass = $derived(size === 'xs' ? 'flex flex-wrap items-center justify-center gap-1.5 text-xs' : 'flex flex-wrap items-center justify-center gap-2.5 text-sm')
|
|
93
|
+
const totalClass = $derived(size === 'xs' ? 'text-xs text-base-content/70' : 'text-sm text-base-content/70')
|
|
94
|
+
const sizeChangerClass = $derived(size === 'xs' ? 'w-22 shrink-0' : 'w-24 shrink-0')
|
|
95
|
+
const jumperWrapClass = $derived(size === 'xs' ? 'flex min-w-fit items-center gap-1.5 whitespace-nowrap' : 'flex min-w-fit items-center gap-1.5 whitespace-nowrap')
|
|
96
|
+
const jumperInputClass = $derived(
|
|
97
|
+
size === 'xs'
|
|
98
|
+
? 'w-[2.25rem] shrink-0 [&>.fieldset]:m-0 [&>.fieldset]:min-w-0'
|
|
99
|
+
: 'w-[2.5rem] shrink-0 [&>.fieldset]:m-0 [&>.fieldset]:min-w-0',
|
|
100
|
+
)
|
|
101
|
+
|
|
90
102
|
function goToPage(p: number) {
|
|
91
103
|
if (p >= 1 && p <= totalPages && p !== page) {
|
|
92
104
|
page = p
|
|
@@ -112,17 +124,17 @@
|
|
|
112
124
|
}
|
|
113
125
|
</script>
|
|
114
126
|
|
|
115
|
-
<div class=
|
|
127
|
+
<div class={wrapperClass}>
|
|
116
128
|
{#if showTotal}
|
|
117
|
-
<span class=
|
|
129
|
+
<span class={totalClass}>
|
|
118
130
|
{(labels.total ?? uiM('pagination_total')).replace('{count}', String(total))}
|
|
119
131
|
</span>
|
|
120
132
|
{/if}
|
|
121
133
|
|
|
122
134
|
{#if showSizeChanger}
|
|
123
|
-
<div class=
|
|
135
|
+
<div class={sizeChangerClass}>
|
|
124
136
|
<Select
|
|
125
|
-
size=
|
|
137
|
+
size={controlSize}
|
|
126
138
|
value={String(pageSize)}
|
|
127
139
|
onchange={handlePageSizeChange}
|
|
128
140
|
>
|
|
@@ -167,18 +179,20 @@
|
|
|
167
179
|
</div>
|
|
168
180
|
|
|
169
181
|
{#if showJumper}
|
|
170
|
-
<div class=
|
|
171
|
-
<span class=
|
|
172
|
-
<
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
+
<div class={jumperWrapClass}>
|
|
183
|
+
<span class={totalClass}>{labels.jumpTo ?? uiM('pagination_jump_to')}</span>
|
|
184
|
+
<div class={jumperInputClass}>
|
|
185
|
+
<Input
|
|
186
|
+
type='number'
|
|
187
|
+
size={jumperControlSize}
|
|
188
|
+
class='h-7 w-full rounded-md [&_input]:px-1.5 [&_input]:text-center [&_input]:text-[12px]'
|
|
189
|
+
min={1}
|
|
190
|
+
max={totalPages}
|
|
191
|
+
bind:value={jumperValue}
|
|
192
|
+
onkeydown={(e: KeyboardEvent & { currentTarget: HTMLInputElement }) => e.key === 'Enter' && handleJump()}
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
<span class={totalClass}>{labels.page ?? uiM('pagination_page')}</span>
|
|
182
196
|
</div>
|
|
183
197
|
{/if}
|
|
184
198
|
</div>
|
|
@@ -49,6 +49,24 @@
|
|
|
49
49
|
: 'h-10',
|
|
50
50
|
)
|
|
51
51
|
|
|
52
|
+
const inputSpacingClass = $derived(
|
|
53
|
+
size === 'xs'
|
|
54
|
+
? 'px-2.5 text-xs'
|
|
55
|
+
: size === 'sm'
|
|
56
|
+
? 'px-3 text-sm'
|
|
57
|
+
: size === 'lg'
|
|
58
|
+
? 'px-4 text-base'
|
|
59
|
+
: size === 'xl'
|
|
60
|
+
? 'px-4 text-lg'
|
|
61
|
+
: 'px-3 text-sm',
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
const inputTypeClass = $derived(
|
|
65
|
+
type === 'number'
|
|
66
|
+
? '[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none'
|
|
67
|
+
: '',
|
|
68
|
+
)
|
|
69
|
+
|
|
52
70
|
const containerClass = $derived(
|
|
53
71
|
cn(
|
|
54
72
|
'flex items-center w-full rounded-lg border bg-base-100',
|
|
@@ -114,7 +132,11 @@
|
|
|
114
132
|
{step}
|
|
115
133
|
inputmode={inputmode}
|
|
116
134
|
autocomplete={autocomplete as HTMLInputElement['autocomplete']}
|
|
117
|
-
class=
|
|
135
|
+
class={cn(
|
|
136
|
+
'flex-1 h-full bg-transparent border-none outline-none placeholder:text-base-content/35',
|
|
137
|
+
inputSpacingClass,
|
|
138
|
+
inputTypeClass,
|
|
139
|
+
)}
|
|
118
140
|
bind:value
|
|
119
141
|
oninput={handleInput}
|
|
120
142
|
onchange={handleChange}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
-->
|
|
11
11
|
<script lang='ts' generics="T = string">
|
|
12
12
|
import type { SelectProps } from '../../types.js'
|
|
13
|
-
import { cn,
|
|
13
|
+
import { cn, getSizeClass } from '../../utils.js'
|
|
14
14
|
|
|
15
15
|
let {
|
|
16
16
|
value = $bindable<T>(),
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
const selectClass = $derived(
|
|
33
33
|
cn(
|
|
34
34
|
'select w-full',
|
|
35
|
-
|
|
35
|
+
getSizeClass(size, 'input'),
|
|
36
36
|
error && 'select-error',
|
|
37
37
|
className,
|
|
38
38
|
),
|
|
@@ -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>
|
|
@@ -137,6 +137,14 @@
|
|
|
137
137
|
const canUpdate = $derived(permissions.update !== false && Boolean(crud.api.update))
|
|
138
138
|
const canDelete = $derived(permissions.delete !== false && Boolean(crud.api.remove))
|
|
139
139
|
const searchable = $derived(crud.resource.searchable !== false)
|
|
140
|
+
const isCompact = $derived(density === 'compact')
|
|
141
|
+
const pageGapClass = 'space-y-5'
|
|
142
|
+
const actionButtonSize = $derived(isCompact ? 'xs' : 'sm')
|
|
143
|
+
const actionIconClass = $derived(isCompact ? 'size-3.5' : 'size-4')
|
|
144
|
+
const paginationSize = $derived(isCompact ? 'xs' : 'sm')
|
|
145
|
+
const filterCardClass = 'overflow-hidden rounded-[1.25rem] border-base-content/7 shadow-sm'
|
|
146
|
+
const listCardClass = 'overflow-hidden rounded-[1.25rem] border-base-content/7 shadow-sm'
|
|
147
|
+
const paginationBarClass = 'flex justify-center border-t border-base-content/5 px-4 py-2'
|
|
140
148
|
|
|
141
149
|
// 表单展示配置(抽屉 / 弹窗)
|
|
142
150
|
const formVariant = $derived(form.variant ?? 'drawer')
|
|
@@ -395,7 +403,7 @@
|
|
|
395
403
|
)
|
|
396
404
|
</script>
|
|
397
405
|
|
|
398
|
-
<div class='
|
|
406
|
+
<div class='{pageGapClass} {className}'>
|
|
399
407
|
<!-- 页面标题 -->
|
|
400
408
|
<PageHeader title={resourceLabel}>
|
|
401
409
|
{#snippet actions()}
|
|
@@ -403,8 +411,8 @@
|
|
|
403
411
|
{@render headerActions()}
|
|
404
412
|
{/if}
|
|
405
413
|
{#if canCreate}
|
|
406
|
-
<Button variant='primary' onclick={openCreate}>
|
|
407
|
-
<span class='icon-[tabler--plus] size-4.5 mr-1'></span>
|
|
414
|
+
<Button variant='primary' size='sm' onclick={openCreate} class='shadow-sm'>
|
|
415
|
+
<span class='icon-[tabler--plus] size-4.5 mr-1.5'></span>
|
|
408
416
|
{uiM('crud_create')}
|
|
409
417
|
</Button>
|
|
410
418
|
{/if}
|
|
@@ -413,7 +421,7 @@
|
|
|
413
421
|
|
|
414
422
|
<!-- 搜索 + 过滤栏 -->
|
|
415
423
|
{#if searchable || filterFields.length > 0}
|
|
416
|
-
<Card>
|
|
424
|
+
<Card padding='sm' class={filterCardClass}>
|
|
417
425
|
<CrudFilterBar
|
|
418
426
|
{searchable}
|
|
419
427
|
searchPlaceholder={searchPlaceholderText}
|
|
@@ -428,7 +436,7 @@
|
|
|
428
436
|
{/if}
|
|
429
437
|
|
|
430
438
|
<!-- 数据列表 -->
|
|
431
|
-
<Card>
|
|
439
|
+
<Card padding='none' class={listCardClass}>
|
|
432
440
|
<DataTable
|
|
433
441
|
data={data.items}
|
|
434
442
|
columns={dtColumns}
|
|
@@ -440,32 +448,32 @@
|
|
|
440
448
|
{#if rowClickDetail}
|
|
441
449
|
<IconButton
|
|
442
450
|
variant='ghost'
|
|
443
|
-
size=
|
|
451
|
+
size={actionButtonSize}
|
|
444
452
|
ariaLabel={uiM('crud_detail')}
|
|
445
453
|
onclick={() => openDetail(item)}
|
|
446
454
|
>
|
|
447
|
-
<span class='icon-[tabler--eye]
|
|
455
|
+
<span class='icon-[tabler--eye] {actionIconClass}'></span>
|
|
448
456
|
</IconButton>
|
|
449
457
|
{/if}
|
|
450
458
|
{#if canUpdate}
|
|
451
459
|
<IconButton
|
|
452
460
|
variant='ghost'
|
|
453
|
-
size=
|
|
461
|
+
size={actionButtonSize}
|
|
454
462
|
ariaLabel={uiM('crud_edit')}
|
|
455
463
|
onclick={() => openEdit(item)}
|
|
456
464
|
>
|
|
457
|
-
<span class='icon-[tabler--edit]
|
|
465
|
+
<span class='icon-[tabler--edit] {actionIconClass}'></span>
|
|
458
466
|
</IconButton>
|
|
459
467
|
{/if}
|
|
460
468
|
{#if canDelete}
|
|
461
469
|
<IconButton
|
|
462
470
|
variant='ghost'
|
|
463
|
-
size=
|
|
471
|
+
size={actionButtonSize}
|
|
464
472
|
ariaLabel={uiM('crud_delete')}
|
|
465
473
|
onclick={() => requestDelete(item)}
|
|
466
474
|
class='hover:text-error'
|
|
467
475
|
>
|
|
468
|
-
<span class='icon-[tabler--trash]
|
|
476
|
+
<span class='icon-[tabler--trash] {actionIconClass}'></span>
|
|
469
477
|
</IconButton>
|
|
470
478
|
{/if}
|
|
471
479
|
{#if listItemActions}
|
|
@@ -475,12 +483,12 @@
|
|
|
475
483
|
</DataTable>
|
|
476
484
|
|
|
477
485
|
<!-- 分页栏:始终显示,支持每页条数选择与跳页 -->
|
|
478
|
-
<div class=
|
|
486
|
+
<div class={paginationBarClass}>
|
|
479
487
|
<Pagination
|
|
480
488
|
page={data.page}
|
|
481
489
|
total={data.total}
|
|
482
490
|
pageSize={data.pageSize}
|
|
483
|
-
size=
|
|
491
|
+
size={paginationSize}
|
|
484
492
|
showTotal={paginationShowTotal}
|
|
485
493
|
showJumper={paginationShowJumper}
|
|
486
494
|
showSizeChanger={paginationShowSizeChanger}
|
|
@@ -497,6 +505,7 @@
|
|
|
497
505
|
open={detailOpen}
|
|
498
506
|
item={selectedItem}
|
|
499
507
|
fields={detailFields}
|
|
508
|
+
{density}
|
|
500
509
|
title={panelTitle}
|
|
501
510
|
variant={formVariant}
|
|
502
511
|
size={formDrawerSize}
|
|
@@ -515,6 +524,7 @@
|
|
|
515
524
|
open={editOpen}
|
|
516
525
|
mode={panelMode === 'create' ? 'create' : 'edit'}
|
|
517
526
|
fields={panelMode === 'create' ? createFields : editFields}
|
|
527
|
+
{density}
|
|
518
528
|
bind:formData
|
|
519
529
|
title={panelTitle}
|
|
520
530
|
variant={formVariant}
|
|
@@ -142,7 +142,7 @@ export interface CrudPageProps<T = Record<string, unknown>> {
|
|
|
142
142
|
form?: CrudFormConfig;
|
|
143
143
|
/** 分页栏展示配置(每页条数选择、跳页、总数) */
|
|
144
144
|
pagination?: CrudPaginationConfig;
|
|
145
|
-
/**
|
|
145
|
+
/** 列表显示密度:影响列表行、行内操作、分页与详情/编辑表单;不影响页头与筛选栏 */
|
|
146
146
|
density?: CrudDensity;
|
|
147
147
|
/** 列表行点击是否打开详情(默认 true) */
|
|
148
148
|
rowClickDetail?: boolean;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@h-ai/ui",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.0-alpha.
|
|
4
|
+
"version": "0.1.0-alpha.24",
|
|
5
5
|
"description": "Hai Framework UI component library for Svelte 5 Runes.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"svelte": "./dist/index.js",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"mermaid": "^11.12.0",
|
|
46
46
|
"shiki": "^4.0.2",
|
|
47
47
|
"tailwind-merge": "^3.5.0",
|
|
48
|
-
"@h-ai/core": "0.1.0-alpha.
|
|
48
|
+
"@h-ai/core": "0.1.0-alpha.24"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@sveltejs/package": "^2.5.7",
|