@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
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
@component CrudEditDrawer
|
|
3
|
-
CRUD 编辑/新建抽屉组件
|
|
4
|
-
|
|
5
|
-
使用 Svelte 5 Runes ($props, $state, $derived)
|
|
6
|
-
使用 compounds/primitives 组件:Drawer, 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 { uiM } from '../../../messages.js'
|
|
12
|
-
import Drawer from '../../compounds/Drawer.svelte'
|
|
13
|
-
import FormField from '../../compounds/FormField.svelte'
|
|
14
|
-
import Button from '../../primitives/Button.svelte'
|
|
15
|
-
import Checkbox from '../../primitives/Checkbox.svelte'
|
|
16
|
-
import Input from '../../primitives/Input.svelte'
|
|
17
|
-
import Select from '../../primitives/Select.svelte'
|
|
18
|
-
import Textarea from '../../primitives/Textarea.svelte'
|
|
19
|
-
|
|
20
|
-
type FieldDef = {
|
|
21
|
-
id: string
|
|
22
|
-
label: string | (() => string)
|
|
23
|
-
type: string
|
|
24
|
-
readonly?: boolean
|
|
25
|
-
options?: Array<{ label: string, value: string | number | boolean }> | (() => Array<{ label: string, value: string | number | boolean }>)
|
|
26
|
-
validation?: { required?: boolean, min?: number, max?: number, pattern?: string, message?: string }
|
|
27
|
-
placeholder?: string | (() => string)
|
|
28
|
-
order?: number
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
let {
|
|
32
|
-
open = $bindable(false),
|
|
33
|
-
mode = 'create' as 'create' | 'edit',
|
|
34
|
-
fields = [],
|
|
35
|
-
formData = $bindable<Record<string, unknown>>({}),
|
|
36
|
-
title = '',
|
|
37
|
-
size = '2xl' as Size,
|
|
38
|
-
submitting = false,
|
|
39
|
-
error = '',
|
|
40
|
-
onsubmit,
|
|
41
|
-
onclose,
|
|
42
|
-
editFormExtra,
|
|
43
|
-
editingItem = null,
|
|
44
|
-
}: {
|
|
45
|
-
open?: boolean
|
|
46
|
-
mode?: 'create' | 'edit'
|
|
47
|
-
fields?: FieldDef[]
|
|
48
|
-
formData?: Record<string, unknown>
|
|
49
|
-
title?: string
|
|
50
|
-
size?: Size
|
|
51
|
-
submitting?: boolean
|
|
52
|
-
error?: string
|
|
53
|
-
onsubmit?: (data: Record<string, unknown>) => Promise<void>
|
|
54
|
-
onclose?: () => void
|
|
55
|
-
editFormExtra?: Snippet<[Record<string, unknown> | null, 'create' | 'edit']>
|
|
56
|
-
editingItem?: Record<string, unknown> | null
|
|
57
|
-
} = $props()
|
|
58
|
-
|
|
59
|
-
function resolveText(text: string | (() => string)): string {
|
|
60
|
-
return typeof text === 'function' ? text() : text
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function resolveOptions(options?: FieldDef['options']): Array<{ label: string, value: string | number | boolean }> {
|
|
64
|
-
if (!options)
|
|
65
|
-
return []
|
|
66
|
-
return typeof options === 'function' ? options() : options
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const sortedFields = $derived(
|
|
70
|
-
[...fields].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
function handleClose() {
|
|
74
|
-
open = false
|
|
75
|
-
onclose?.()
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function handleSubmit(e: Event) {
|
|
79
|
-
e.preventDefault()
|
|
80
|
-
await onsubmit?.(formData)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function getInputType(fieldType: string): 'email' | 'url' | 'tel' | 'password' | 'number' | 'text' {
|
|
84
|
-
switch (fieldType) {
|
|
85
|
-
case 'email': return 'email'
|
|
86
|
-
case 'url': return 'url'
|
|
87
|
-
case 'tel': return 'tel'
|
|
88
|
-
case 'password': return 'password'
|
|
89
|
-
case 'number': return 'number'
|
|
90
|
-
case 'date': return 'text'
|
|
91
|
-
case 'datetime': return 'text'
|
|
92
|
-
default: return 'text'
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function updateField(fieldId: string, value: unknown) {
|
|
97
|
-
formData = { ...formData, [fieldId]: value }
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function toggleMultiSelect(fieldId: string, optValue: string | number | boolean) {
|
|
101
|
-
const current = (formData[fieldId] as Array<string | number | boolean>) ?? []
|
|
102
|
-
const strVal = String(optValue)
|
|
103
|
-
if (current.map(String).includes(strVal)) {
|
|
104
|
-
formData = { ...formData, [fieldId]: current.filter(v => String(v) !== strVal) }
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
formData = { ...formData, [fieldId]: [...current, optValue] }
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
</script>
|
|
111
|
-
|
|
112
|
-
<Drawer bind:open {title} position='right' {size} onclose={handleClose} closeOnBackdrop={false}>
|
|
113
|
-
<form onsubmit={handleSubmit} class='space-y-4 pb-20'>
|
|
114
|
-
{#if error}
|
|
115
|
-
<div class='p-3 bg-error/10 border border-error/20 rounded-lg text-sm text-error flex items-center gap-2'>
|
|
116
|
-
<span class='icon-[tabler--alert-circle] size-4 shrink-0'></span>
|
|
117
|
-
<span>{error}</span>
|
|
118
|
-
</div>
|
|
119
|
-
{/if}
|
|
120
|
-
|
|
121
|
-
{#each sortedFields as field (field.id)}
|
|
122
|
-
{@const isReadonly = field.readonly && mode === 'edit'}
|
|
123
|
-
{@const isRequired = field.validation?.required}
|
|
124
|
-
{@const fieldValue = formData[field.id]}
|
|
125
|
-
{@const placeholder = field.placeholder ? resolveText(field.placeholder) : ''}
|
|
126
|
-
|
|
127
|
-
{#if field.type === 'textarea'}
|
|
128
|
-
<FormField label={resolveText(field.label)} required={isRequired}>
|
|
129
|
-
<Textarea
|
|
130
|
-
id={field.id}
|
|
131
|
-
value={String(fieldValue ?? '')}
|
|
132
|
-
{placeholder}
|
|
133
|
-
disabled={submitting || isReadonly}
|
|
134
|
-
required={isRequired}
|
|
135
|
-
oninput={e => updateField(field.id, (e.target as HTMLTextAreaElement).value)}
|
|
136
|
-
/>
|
|
137
|
-
</FormField>
|
|
138
|
-
|
|
139
|
-
{:else if field.type === 'select' || field.type === 'radio'}
|
|
140
|
-
{@const opts = resolveOptions(field.options)}
|
|
141
|
-
<FormField label={resolveText(field.label)} required={isRequired}>
|
|
142
|
-
<Select
|
|
143
|
-
id={field.id}
|
|
144
|
-
value={String(fieldValue ?? '')}
|
|
145
|
-
disabled={submitting || isReadonly}
|
|
146
|
-
onchange={value => updateField(field.id, value)}
|
|
147
|
-
>
|
|
148
|
-
<option value="">{placeholder || uiM('crud_filter_all')}</option>
|
|
149
|
-
{#each opts as opt (String(opt.value))}
|
|
150
|
-
<option value={String(opt.value)}>{opt.label}</option>
|
|
151
|
-
{/each}
|
|
152
|
-
</Select>
|
|
153
|
-
</FormField>
|
|
154
|
-
|
|
155
|
-
{:else if field.type === 'multi-select'}
|
|
156
|
-
{@const opts = resolveOptions(field.options)}
|
|
157
|
-
{@const selectedValues = ((fieldValue ?? []) as Array<string | number | boolean>).map(String)}
|
|
158
|
-
<FormField label={resolveText(field.label)} required={isRequired}>
|
|
159
|
-
<div class='flex flex-wrap gap-3 p-3 bg-base-200 rounded-lg max-h-48 overflow-y-auto'>
|
|
160
|
-
{#each opts as opt (String(opt.value))}
|
|
161
|
-
<label class='inline-flex items-center gap-2 cursor-pointer'>
|
|
162
|
-
<Checkbox
|
|
163
|
-
size='sm'
|
|
164
|
-
checked={selectedValues.includes(String(opt.value))}
|
|
165
|
-
onchange={() => toggleMultiSelect(field.id, opt.value)}
|
|
166
|
-
disabled={submitting || isReadonly}
|
|
167
|
-
/>
|
|
168
|
-
<span class='text-sm text-base-content'>{opt.label}</span>
|
|
169
|
-
</label>
|
|
170
|
-
{/each}
|
|
171
|
-
{#if opts.length === 0}
|
|
172
|
-
<span class='text-sm text-base-content/60'>{uiM('crud_no_data')}</span>
|
|
173
|
-
{/if}
|
|
174
|
-
</div>
|
|
175
|
-
</FormField>
|
|
176
|
-
|
|
177
|
-
{:else if field.type === 'boolean' || field.type === 'checkbox'}
|
|
178
|
-
<FormField label={resolveText(field.label)}>
|
|
179
|
-
<Checkbox
|
|
180
|
-
checked={Boolean(fieldValue)}
|
|
181
|
-
onchange={(checked: boolean) => updateField(field.id, checked)}
|
|
182
|
-
disabled={submitting || isReadonly}
|
|
183
|
-
/>
|
|
184
|
-
</FormField>
|
|
185
|
-
|
|
186
|
-
{:else}
|
|
187
|
-
<FormField label={resolveText(field.label)} required={isRequired}>
|
|
188
|
-
<Input
|
|
189
|
-
id={field.id}
|
|
190
|
-
type={getInputType(field.type)}
|
|
191
|
-
value={String(fieldValue ?? '')}
|
|
192
|
-
{placeholder}
|
|
193
|
-
disabled={submitting || isReadonly}
|
|
194
|
-
required={isRequired}
|
|
195
|
-
pattern={field.validation?.pattern}
|
|
196
|
-
minlength={field.type !== 'number' ? field.validation?.min : undefined}
|
|
197
|
-
maxlength={field.type !== 'number' ? field.validation?.max : undefined}
|
|
198
|
-
min={field.type === 'number' ? field.validation?.min : undefined}
|
|
199
|
-
max={field.type === 'number' ? field.validation?.max : undefined}
|
|
200
|
-
oninput={e => updateField(field.id, field.type === 'number' ? Number((e.currentTarget as HTMLInputElement).value) : (e.currentTarget as HTMLInputElement).value)}
|
|
201
|
-
/>
|
|
202
|
-
</FormField>
|
|
203
|
-
{/if}
|
|
204
|
-
{/each}
|
|
205
|
-
|
|
206
|
-
{#if editFormExtra}
|
|
207
|
-
<div class='border-t border-base-content/5 pt-4'>
|
|
208
|
-
{@render editFormExtra(editingItem, mode)}
|
|
209
|
-
</div>
|
|
210
|
-
{/if}
|
|
211
|
-
</form>
|
|
212
|
-
|
|
213
|
-
<!-- 底部操作栏 -->
|
|
214
|
-
<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'>
|
|
215
|
-
<Button variant='ghost' onclick={handleClose} disabled={submitting}>
|
|
216
|
-
{uiM('crud_cancel')}
|
|
217
|
-
</Button>
|
|
218
|
-
<Button variant='primary' onclick={handleSubmit} disabled={submitting}>
|
|
219
|
-
{#if submitting}
|
|
220
|
-
<span class='loading loading-spinner loading-xs mr-2'></span>
|
|
221
|
-
{/if}
|
|
222
|
-
{mode === 'create' ? uiM('crud_create') : uiM('crud_save')}
|
|
223
|
-
</Button>
|
|
224
|
-
</div>
|
|
225
|
-
</Drawer>
|