@h-ai/ui 0.1.0-alpha.22 → 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 +39 -5
- package/auto-import.d.ts +2 -2
- package/auto-import.js +2 -2
- package/dist/components/compounds/Card.svelte +1 -1
- package/dist/components/compounds/DataTable.svelte +32 -7
- 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 +57 -16
- package/dist/components/primitives/Input.svelte +23 -1
- package/dist/components/primitives/Select.svelte +2 -2
- package/dist/components/scenes/crud/CrudDetailDrawer.svelte +59 -18
- package/dist/components/scenes/crud/CrudDetailDrawer.svelte.d.ts +8 -2
- package/dist/components/scenes/crud/CrudDetailPanel.svelte +204 -0
- package/dist/components/scenes/crud/CrudDetailPanel.svelte.d.ts +45 -0
- package/dist/components/scenes/crud/CrudEditPanel.svelte +281 -0
- package/dist/components/scenes/crud/{CrudEditDrawer.svelte.d.ts → CrudEditPanel.svelte.d.ts} +13 -6
- package/dist/components/scenes/crud/CrudFilterBar.svelte +22 -18
- package/dist/components/scenes/crud/CrudPage.svelte +105 -60
- 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 +3 -2
- package/dist/messages/zh-CN.json +3 -2
- package/dist/types.d.ts +10 -0
- package/package.json +2 -2
- package/dist/components/scenes/crud/CrudEditDrawer.svelte +0 -225
package/README.md
CHANGED
|
@@ -274,11 +274,21 @@ components/
|
|
|
274
274
|
|
|
275
275
|
| 组件 | 描述 | 主要属性 / 能力 |
|
|
276
276
|
| -------------------- | ------------ | ---------------- |
|
|
277
|
-
| `CrudPage` | CRUD 主页面 | `
|
|
278
|
-
| `CrudFilterBar` | 过滤工具栏 | `
|
|
279
|
-
| `
|
|
280
|
-
| `
|
|
281
|
-
| `CrudDeleteConfirm` | 删除确认框 | `open`, `
|
|
277
|
+
| `CrudPage` | CRUD 主页面 | `crud`, `data`, `permissions`, `form`, `pagination`, `density`, `nav` |
|
|
278
|
+
| `CrudFilterBar` | 过滤工具栏 | `filterFields`, `searchValue`, `onsearch`, `onfilterchange` |
|
|
279
|
+
| `CrudDetailPanel` | 详情面板(抽屉/弹窗) | `open`, `item`, `fields`, `variant`, `drawerWidth`, `modalSize` |
|
|
280
|
+
| `CrudEditPanel` | 编辑面板(抽屉/弹窗) | `open`, `fields`, `variant`, `drawerWidth`, `modalSize`, `onsubmit` |
|
|
281
|
+
| `CrudDeleteConfirm` | 删除确认框 | `open`, `loading`, `onconfirm` |
|
|
282
|
+
|
|
283
|
+
> `CrudPage` 通过 `form` 配置新建/编辑的展示形式:
|
|
284
|
+
>
|
|
285
|
+
> - `form.variant`:`'drawer'`(抽屉,默认)或 `'modal'`(弹出窗口)。
|
|
286
|
+
> - 抽屉:`form.drawerSize` 选预设尺寸,`form.drawerWidth` 传任意 CSS 宽度(优先级更高)。
|
|
287
|
+
> - 弹窗:`form.modalSize` 选预设尺寸,`form.modalWidth` / `form.modalHeight` 传任意 CSS 尺寸。
|
|
288
|
+
>
|
|
289
|
+
> `pagination` 配置分页栏(始终显示):`showSizeChanger`、`pageSizeOptions`、`showJumper`、`showTotal` 默认开启。
|
|
290
|
+
>
|
|
291
|
+
> `density` 配置列表密度:`'normal'`(默认)或 `'compact'`(紧凑)。它只影响列表行、行内操作、分页与创建/编辑/详情表单元素,不影响页头与筛选栏。
|
|
282
292
|
|
|
283
293
|
## 使用示例
|
|
284
294
|
|
|
@@ -368,6 +378,30 @@ components/
|
|
|
368
378
|
<ToastContainer />
|
|
369
379
|
```
|
|
370
380
|
|
|
381
|
+
### 声明式 CRUD 页面(CrudPage)
|
|
382
|
+
|
|
383
|
+
```svelte
|
|
384
|
+
<script lang='ts'>
|
|
385
|
+
import { createSvelteKitNavAdapter } from '@h-ai/kit/client'
|
|
386
|
+
import { CrudPage } from '@h-ai/ui'
|
|
387
|
+
|
|
388
|
+
let { data } = $props()
|
|
389
|
+
const nav = createSvelteKitNavAdapter()
|
|
390
|
+
</script>
|
|
391
|
+
|
|
392
|
+
<CrudPage
|
|
393
|
+
crud={roleCrud}
|
|
394
|
+
{data}
|
|
395
|
+
permissions={{ create: true, update: true, delete: true }}
|
|
396
|
+
form={{ variant: 'modal', modalSize: 'lg' }}
|
|
397
|
+
pagination={{ showSizeChanger: true, showJumper: true, pageSizeOptions: [10, 20, 50] }}
|
|
398
|
+
density='compact'
|
|
399
|
+
{nav}
|
|
400
|
+
/>
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
> 抽屉形式可改用 `form={{ variant: 'drawer', drawerWidth: '40rem' }}`。
|
|
404
|
+
|
|
371
405
|
### 登录页面
|
|
372
406
|
|
|
373
407
|
```svelte
|
package/auto-import.d.ts
CHANGED
|
@@ -110,8 +110,8 @@ declare global {
|
|
|
110
110
|
|
|
111
111
|
// scenes - crud
|
|
112
112
|
const CrudDeleteConfirm: typeof import('@h-ai/ui')['CrudDeleteConfirm']
|
|
113
|
-
const
|
|
114
|
-
const
|
|
113
|
+
const CrudDetailPanel: typeof import('@h-ai/ui')['CrudDetailPanel']
|
|
114
|
+
const CrudEditPanel: typeof import('@h-ai/ui')['CrudEditPanel']
|
|
115
115
|
const CrudFilterBar: typeof import('@h-ai/ui')['CrudFilterBar']
|
|
116
116
|
const CrudPage: typeof import('@h-ai/ui')['CrudPage']
|
|
117
117
|
}
|
package/auto-import.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
@prop {boolean} loading - 是否加载中
|
|
11
11
|
@prop {boolean} striped - 是否斑马纹
|
|
12
12
|
@prop {boolean} hoverable - 是否悬停高亮
|
|
13
|
+
@prop {'normal' | 'compact'} density - 显示密度,compact 为紧凑布局
|
|
13
14
|
|
|
14
15
|
@example
|
|
15
16
|
<DataTable
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
loading = false,
|
|
40
41
|
striped = true,
|
|
41
42
|
hoverable = true,
|
|
43
|
+
density = 'normal',
|
|
42
44
|
class: className = '',
|
|
43
45
|
}: {
|
|
44
46
|
data: T[]
|
|
@@ -49,11 +51,34 @@
|
|
|
49
51
|
loading?: boolean
|
|
50
52
|
striped?: boolean
|
|
51
53
|
hoverable?: boolean
|
|
54
|
+
density?: 'normal' | 'compact'
|
|
52
55
|
class?: string
|
|
53
56
|
} = $props()
|
|
54
57
|
|
|
55
58
|
type Column = { key: keyof T | string, label: string, width?: string, align?: 'left' | 'center' | 'right', render?: (item: T) => string }
|
|
56
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
|
+
|
|
57
82
|
function getValue(item: T, col: Column): string {
|
|
58
83
|
if (col.render) {
|
|
59
84
|
return col.render(item)
|
|
@@ -79,9 +104,9 @@
|
|
|
79
104
|
</script>
|
|
80
105
|
|
|
81
106
|
<div class='overflow-x-auto {className}'>
|
|
82
|
-
<table class=
|
|
107
|
+
<table class={tableClass} class:table-zebra={striped}>
|
|
83
108
|
<thead>
|
|
84
|
-
<tr class=
|
|
109
|
+
<tr class={headerRowClass}>
|
|
85
110
|
{#each columns as col (String(col.key))}
|
|
86
111
|
<th
|
|
87
112
|
style={col.width ? `width: ${col.width}` : ''}
|
|
@@ -91,20 +116,20 @@
|
|
|
91
116
|
</th>
|
|
92
117
|
{/each}
|
|
93
118
|
{#if actions}
|
|
94
|
-
<th class=
|
|
119
|
+
<th class={actionColumnClass}>{uiM('data_table_actions')}</th>
|
|
95
120
|
{/if}
|
|
96
121
|
</tr>
|
|
97
122
|
</thead>
|
|
98
123
|
<tbody>
|
|
99
124
|
{#if loading}
|
|
100
125
|
<tr>
|
|
101
|
-
<td colspan={columns.length + (actions ? 1 : 0)} class=
|
|
126
|
+
<td colspan={columns.length + (actions ? 1 : 0)} class={placeholderCellClass}>
|
|
102
127
|
<span class='loading loading-spinner loading-sm'></span>
|
|
103
128
|
</td>
|
|
104
129
|
</tr>
|
|
105
130
|
{:else if data.length === 0}
|
|
106
131
|
<tr>
|
|
107
|
-
<td colspan={columns.length + (actions ? 1 : 0)} class=
|
|
132
|
+
<td colspan={columns.length + (actions ? 1 : 0)} class={placeholderCellClass}>
|
|
108
133
|
{#if empty}
|
|
109
134
|
{@render empty()}
|
|
110
135
|
{:else}
|
|
@@ -114,13 +139,13 @@
|
|
|
114
139
|
</tr>
|
|
115
140
|
{:else}
|
|
116
141
|
{#each data as item (item[keyField])}
|
|
117
|
-
<tr class={
|
|
142
|
+
<tr class={rowClass}>
|
|
118
143
|
{#each columns as col (String(col.key))}
|
|
119
144
|
<td class={getAlignClass(col.align)}>{getValue(item, col)}</td>
|
|
120
145
|
{/each}
|
|
121
146
|
{#if actions}
|
|
122
147
|
<td>
|
|
123
|
-
<div class=
|
|
148
|
+
<div class={actionWrapClass}>
|
|
124
149
|
{@render actions(item)}
|
|
125
150
|
</div>
|
|
126
151
|
</td>
|
|
@@ -15,6 +15,7 @@ declare function $$render<T>(): {
|
|
|
15
15
|
loading?: boolean;
|
|
16
16
|
striped?: boolean;
|
|
17
17
|
hoverable?: boolean;
|
|
18
|
+
density?: "normal" | "compact";
|
|
18
19
|
class?: string;
|
|
19
20
|
};
|
|
20
21
|
exports: {};
|
|
@@ -48,6 +49,7 @@ interface $$IsomorphicComponent {
|
|
|
48
49
|
* @prop {boolean} loading - 是否加载中
|
|
49
50
|
* @prop {boolean} striped - 是否斑马纹
|
|
50
51
|
* @prop {boolean} hoverable - 是否悬停高亮
|
|
52
|
+
* @prop {'normal' | 'compact'} density - 显示密度,compact 为紧凑布局
|
|
51
53
|
*
|
|
52
54
|
* @example
|
|
53
55
|
* <DataTable
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
title = '',
|
|
21
21
|
position = 'right',
|
|
22
22
|
size = 'md',
|
|
23
|
+
width,
|
|
23
24
|
closeOnBackdrop = true,
|
|
24
25
|
showClose = true,
|
|
25
26
|
class: className = '',
|
|
@@ -40,6 +41,10 @@
|
|
|
40
41
|
'4xl': 'w-[42rem]',
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
// width 优先:传入自定义 CSS 宽度时通过 inline style 覆盖预设宽度类
|
|
45
|
+
const hasCustomWidth = $derived(typeof width === 'string' && width.trim().length > 0)
|
|
46
|
+
const widthStyle = $derived(hasCustomWidth ? `width: ${width!.trim()}; max-width: 100vw` : undefined)
|
|
47
|
+
|
|
43
48
|
const drawerClass = $derived(
|
|
44
49
|
cn(
|
|
45
50
|
'drawer',
|
|
@@ -56,7 +61,7 @@
|
|
|
56
61
|
const contentClass = $derived(
|
|
57
62
|
cn(
|
|
58
63
|
'menu bg-base-200 text-base-content min-h-full p-4',
|
|
59
|
-
sizeMap[size],
|
|
64
|
+
!hasCustomWidth && sizeMap[size],
|
|
60
65
|
className,
|
|
61
66
|
),
|
|
62
67
|
)
|
|
@@ -86,7 +91,7 @@
|
|
|
86
91
|
onkeydown={e => e.key === 'Enter' && handleBackdropClick()}
|
|
87
92
|
></div>
|
|
88
93
|
|
|
89
|
-
<div class={contentClass}>
|
|
94
|
+
<div class={contentClass} style={widthStyle}>
|
|
90
95
|
<div class='flex items-center justify-between mb-4'>
|
|
91
96
|
{#if title}
|
|
92
97
|
<h3 class='text-lg font-bold'>{title}</h3>
|
|
@@ -210,6 +210,7 @@
|
|
|
210
210
|
max-height: calc(100vh - clamp(2rem, 5.2vw, 4rem));
|
|
211
211
|
max-height: calc(100dvh - clamp(2rem, 5.2vw, 4rem));
|
|
212
212
|
border-radius: 1.4rem;
|
|
213
|
+
overflow: hidden;
|
|
213
214
|
color: var(--color-base-content);
|
|
214
215
|
outline: none;
|
|
215
216
|
box-shadow:
|
|
@@ -224,6 +225,7 @@
|
|
|
224
225
|
|
|
225
226
|
.hai-modal__footer {
|
|
226
227
|
box-shadow: inset 0 1px 0 color-mix(in srgb, var(--color-base-content) 6%, transparent);
|
|
228
|
+
border-radius: 0 0 1.4rem 1.4rem;
|
|
227
229
|
}
|
|
228
230
|
|
|
229
231
|
.hai-modal__close {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import BareButton from '../primitives/BareButton.svelte'
|
|
15
15
|
|
|
16
16
|
import Input from '../primitives/Input.svelte'
|
|
17
|
+
import Select from '../primitives/Select.svelte'
|
|
17
18
|
|
|
18
19
|
let {
|
|
19
20
|
page = $bindable(1),
|
|
@@ -22,15 +23,18 @@
|
|
|
22
23
|
size = 'md',
|
|
23
24
|
showTotal = true,
|
|
24
25
|
showJumper = false,
|
|
26
|
+
showSizeChanger = false,
|
|
27
|
+
pageSizeOptions = [10, 20, 50, 100],
|
|
25
28
|
labels = {},
|
|
26
29
|
class: className = '',
|
|
27
30
|
onchange,
|
|
31
|
+
onpagesizechange,
|
|
28
32
|
}: PaginationProps = $props()
|
|
29
33
|
|
|
30
34
|
// labels 优先,缺省回退到内置消息 uiM(...)
|
|
31
35
|
|
|
32
|
-
//
|
|
33
|
-
const totalPages = $derived(Math.ceil(total / pageSize))
|
|
36
|
+
// 计算总页数(至少 1 页,保证分页栏在空数据时仍可渲染)
|
|
37
|
+
const totalPages = $derived(Math.max(1, Math.ceil(total / pageSize)))
|
|
34
38
|
|
|
35
39
|
// 生成页码列表
|
|
36
40
|
const pages = $derived(() => {
|
|
@@ -83,6 +87,18 @@
|
|
|
83
87
|
),
|
|
84
88
|
)
|
|
85
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
|
+
|
|
86
102
|
function goToPage(p: number) {
|
|
87
103
|
if (p >= 1 && p <= totalPages && p !== page) {
|
|
88
104
|
page = p
|
|
@@ -99,15 +115,38 @@
|
|
|
99
115
|
jumperValue = ''
|
|
100
116
|
}
|
|
101
117
|
}
|
|
118
|
+
|
|
119
|
+
function handlePageSizeChange(value: string) {
|
|
120
|
+
const next = Number.parseInt(value, 10)
|
|
121
|
+
if (!Number.isNaN(next) && next !== pageSize) {
|
|
122
|
+
onpagesizechange?.(next)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
102
125
|
</script>
|
|
103
126
|
|
|
104
|
-
<div class=
|
|
127
|
+
<div class={wrapperClass}>
|
|
105
128
|
{#if showTotal}
|
|
106
|
-
<span class=
|
|
129
|
+
<span class={totalClass}>
|
|
107
130
|
{(labels.total ?? uiM('pagination_total')).replace('{count}', String(total))}
|
|
108
131
|
</span>
|
|
109
132
|
{/if}
|
|
110
133
|
|
|
134
|
+
{#if showSizeChanger}
|
|
135
|
+
<div class={sizeChangerClass}>
|
|
136
|
+
<Select
|
|
137
|
+
size={controlSize}
|
|
138
|
+
value={String(pageSize)}
|
|
139
|
+
onchange={handlePageSizeChange}
|
|
140
|
+
>
|
|
141
|
+
{#each pageSizeOptions as opt (opt)}
|
|
142
|
+
<option value={String(opt)}>
|
|
143
|
+
{(labels.pageSize ?? uiM('pagination_page_size')).replace('{size}', String(opt))}
|
|
144
|
+
</option>
|
|
145
|
+
{/each}
|
|
146
|
+
</Select>
|
|
147
|
+
</div>
|
|
148
|
+
{/if}
|
|
149
|
+
|
|
111
150
|
<div class={joinClass}>
|
|
112
151
|
<BareButton
|
|
113
152
|
class={btnClass}
|
|
@@ -140,18 +179,20 @@
|
|
|
140
179
|
</div>
|
|
141
180
|
|
|
142
181
|
{#if showJumper}
|
|
143
|
-
<div class=
|
|
144
|
-
<span class=
|
|
145
|
-
<
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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>
|
|
155
196
|
</div>
|
|
156
197
|
{/if}
|
|
157
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
|
),
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
@component CrudDetailDrawer
|
|
3
|
-
CRUD
|
|
3
|
+
CRUD 详情组件(只读展示),支持抽屉(drawer)与弹窗(modal)两种展示形式
|
|
4
4
|
|
|
5
5
|
使用 Svelte 5 Runes ($props, $derived)
|
|
6
|
-
使用 compounds 组件:Drawer, Badge
|
|
6
|
+
使用 compounds 组件:Drawer, Modal, Badge
|
|
7
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
12
|
import { uiM } from '../../../messages.js'
|
|
12
13
|
import Drawer from '../../compounds/Drawer.svelte'
|
|
14
|
+
import Modal from '../../compounds/Modal.svelte'
|
|
13
15
|
import Badge from '../../primitives/Badge.svelte'
|
|
14
16
|
import Button from '../../primitives/Button.svelte'
|
|
15
17
|
|
|
@@ -28,7 +30,12 @@
|
|
|
28
30
|
item = null,
|
|
29
31
|
fields = [],
|
|
30
32
|
title = '',
|
|
33
|
+
variant = 'drawer' as CrudFormVariant,
|
|
31
34
|
size = '2xl' as Size,
|
|
35
|
+
drawerWidth,
|
|
36
|
+
modalSize = '2xl' as Size | 'full',
|
|
37
|
+
modalWidth,
|
|
38
|
+
modalHeight,
|
|
32
39
|
canEdit = false,
|
|
33
40
|
onedit,
|
|
34
41
|
onclose,
|
|
@@ -38,7 +45,12 @@
|
|
|
38
45
|
item?: Record<string, unknown> | null
|
|
39
46
|
fields?: FieldDef[]
|
|
40
47
|
title?: string
|
|
48
|
+
variant?: CrudFormVariant
|
|
41
49
|
size?: Size
|
|
50
|
+
drawerWidth?: string
|
|
51
|
+
modalSize?: Size | 'full'
|
|
52
|
+
modalWidth?: string
|
|
53
|
+
modalHeight?: string
|
|
42
54
|
canEdit?: boolean
|
|
43
55
|
onedit?: () => void
|
|
44
56
|
onclose?: () => void
|
|
@@ -107,9 +119,9 @@
|
|
|
107
119
|
}
|
|
108
120
|
</script>
|
|
109
121
|
|
|
110
|
-
|
|
122
|
+
{#snippet detailBody()}
|
|
111
123
|
{#if item}
|
|
112
|
-
<div class='space-y-4
|
|
124
|
+
<div class='space-y-4'>
|
|
113
125
|
{#each detailFields as field (field.id)}
|
|
114
126
|
{@const value = item[field.id]}
|
|
115
127
|
<div>
|
|
@@ -138,18 +150,47 @@
|
|
|
138
150
|
</div>
|
|
139
151
|
{/if}
|
|
140
152
|
</div>
|
|
141
|
-
|
|
142
|
-
<!-- 底部操作栏 -->
|
|
143
|
-
<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'>
|
|
144
|
-
{#if canEdit}
|
|
145
|
-
<Button variant='primary' onclick={onedit}>
|
|
146
|
-
<span class='icon-[tabler--edit] size-4 mr-1'></span>
|
|
147
|
-
{uiM('crud_edit')}
|
|
148
|
-
</Button>
|
|
149
|
-
{/if}
|
|
150
|
-
<Button variant='ghost' onclick={handleClose}>
|
|
151
|
-
{uiM('crud_close')}
|
|
152
|
-
</Button>
|
|
153
|
-
</div>
|
|
154
153
|
{/if}
|
|
155
|
-
|
|
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}
|
|
@@ -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,7 +21,12 @@ 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;
|
|
@@ -28,10 +34,10 @@ type $$ComponentProps = {
|
|
|
28
34
|
};
|
|
29
35
|
/**
|
|
30
36
|
* CrudDetailDrawer
|
|
31
|
-
* CRUD
|
|
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
42
|
declare const CrudDetailDrawer: import("svelte").Component<$$ComponentProps, {}, "open">;
|
|
37
43
|
type CrudDetailDrawer = ReturnType<typeof CrudDetailDrawer>;
|