@h-ai/ui 0.1.0-alpha.25 → 0.1.0-alpha.27
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 +28 -8
- package/auto-import.d.ts +5 -0
- package/auto-import.js +4 -0
- package/dist/components/compounds/Calendar.svelte +1 -1
- package/dist/components/compounds/DataTable.svelte +81 -4
- package/dist/components/compounds/DataTable.svelte.d.ts +7 -0
- package/dist/components/compounds/DatePicker.svelte +1 -1
- package/dist/components/compounds/Modal.svelte +44 -6
- package/dist/components/compounds/Pagination.svelte +72 -119
- package/dist/components/compounds/ToastContainer.svelte +159 -20
- package/dist/components/scenes/app/SettingsLayout.svelte +111 -0
- package/dist/components/scenes/app/SettingsLayout.svelte.d.ts +41 -0
- package/dist/components/scenes/app/ThemeColorPicker.svelte +41 -0
- package/dist/components/scenes/app/index.d.ts +1 -0
- package/dist/components/scenes/app/index.js +1 -0
- package/dist/components/scenes/crud/CrudFilterBar.svelte +173 -35
- package/dist/components/scenes/crud/CrudFilterBar.svelte.d.ts +7 -1
- package/dist/components/scenes/crud/CrudPage.svelte +19 -15
- package/dist/components/scenes/error/ErrorPage.svelte +125 -0
- package/dist/components/scenes/error/ErrorPage.svelte.d.ts +42 -0
- package/dist/components/scenes/error/error-presets.d.ts +24 -0
- package/dist/components/scenes/error/error-presets.js +31 -0
- package/dist/components/scenes/error/index.d.ts +8 -0
- package/dist/components/scenes/error/index.js +8 -0
- package/dist/components/scenes/iam/AuthShell.svelte +138 -0
- package/dist/components/scenes/iam/AuthShell.svelte.d.ts +45 -0
- package/dist/components/scenes/iam/index.d.ts +1 -0
- package/dist/components/scenes/iam/index.js +2 -1
- package/dist/components/scenes/index.d.ts +1 -0
- package/dist/components/scenes/index.js +2 -0
- package/dist/messages/en-US.json +20 -1
- package/dist/messages/zh-CN.json +20 -1
- package/dist/styles/global.css +4 -1
- package/dist/types.d.ts +6 -0
- package/package.json +9 -3
- package/vite.d.ts +13 -0
- package/vite.js +95 -0
package/README.md
CHANGED
|
@@ -32,13 +32,21 @@ export default config
|
|
|
32
32
|
### 2. 配置 vite.config.ts
|
|
33
33
|
|
|
34
34
|
```ts
|
|
35
|
+
import { haiOptimizeExclude, haiPrebundledDeps } from '@h-ai/ui/vite'
|
|
35
36
|
import { sveltekit } from '@sveltejs/kit/vite'
|
|
36
37
|
import tailwindcss from '@tailwindcss/vite'
|
|
37
38
|
import { defineConfig } from 'vite'
|
|
38
39
|
|
|
39
40
|
export default defineConfig({
|
|
40
41
|
plugins: [sveltekit(), tailwindcss()],
|
|
41
|
-
optimizeDeps: {
|
|
42
|
+
optimizeDeps: {
|
|
43
|
+
// bits-ui 含 .svelte 源码须 exclude;@internationalized/date 须与 bits-ui 同为原始副本,
|
|
44
|
+
// 否则日期组件会因双实例 instanceof 失败抛 "Unknown date type"
|
|
45
|
+
exclude: [...haiOptimizeExclude],
|
|
46
|
+
// 预打包 @h-ai/ui 的重型纯 JS 依赖(语法高亮 / Mermaid / PDF 等),
|
|
47
|
+
// 避免首次进入业务路由时触发依赖再优化与整页刷新
|
|
48
|
+
include: [...haiPrebundledDeps],
|
|
49
|
+
},
|
|
42
50
|
ssr: { noExternal: [/@h-ai\//] },
|
|
43
51
|
})
|
|
44
52
|
```
|
|
@@ -151,7 +159,7 @@ components/
|
|
|
151
159
|
|
|
152
160
|
| 组件 | 描述 | 主要属性 |
|
|
153
161
|
| --------- | ------ | ------------------------------------------------------- |
|
|
154
|
-
| `Modal` | 模态框 | `open`, `title`, `size`, `
|
|
162
|
+
| `Modal` | 模态框 | `open`, `title`, `size`, `radius`, `bodyOverflow`, `showClose` |
|
|
155
163
|
| `Drawer` | 抽屉 | `open`, `position`, `title`, `size` |
|
|
156
164
|
| `Confirm` | 确认框 | `open`, `title`, `message`, `variant`, `onconfirm` |
|
|
157
165
|
| `Popover` | 弹出层 | `open`, `position`, `trigger`, `offset` |
|
|
@@ -161,7 +169,7 @@ components/
|
|
|
161
169
|
| 组件 | 描述 | 主要属性 |
|
|
162
170
|
| ----------- | -------- | ---------------------------------------- |
|
|
163
171
|
| `Card` | 卡片容器 | `title`, `bordered`, `shadow`, `padding` |
|
|
164
|
-
| `DataTable` | 数据表格 | `data`, `columns`, `keyField`, `loading` |
|
|
172
|
+
| `DataTable` | 数据表格 | `data`, `columns`, `keyField`, `loading`, `sortKey`, `sortDir`, `onsort`(列 `sortable` 可排序) |
|
|
165
173
|
| `Accordion` | 手风琴 | `items: AccordionItem[]` |
|
|
166
174
|
| `Timeline` | 时间线 | `items: TimelineItem[]` |
|
|
167
175
|
|
|
@@ -192,7 +200,7 @@ components/
|
|
|
192
200
|
| 组件 | 描述 | 主要属性 |
|
|
193
201
|
| ------------ | -------- | -------------------------------------------- |
|
|
194
202
|
| `Tabs` | 标签页 | `items`, `active`, `type`, `size` |
|
|
195
|
-
| `Pagination` |
|
|
203
|
+
| `Pagination` | 分页(统一 table 风格) | `page`, `total`, `pageSize`, `showTotal`, `showJumper`, `showSizeChanger` |
|
|
196
204
|
| `Breadcrumb` | 面包屑 | `items`, `separator` |
|
|
197
205
|
| `Steps` | 步骤条 | `items`, `current`, `direction`, `clickable` |
|
|
198
206
|
| `Dropdown` | 下拉菜单 | `items`, `trigger`, `position` |
|
|
@@ -212,23 +220,25 @@ components/
|
|
|
212
220
|
| ------------ | -------- | ------------------------------------------- |
|
|
213
221
|
| `PageHeader` | 页面头部 | `title`, `description`,支持 `actions` 插槽 |
|
|
214
222
|
|
|
215
|
-
### 场景组件 Scenes(
|
|
223
|
+
### 场景组件 Scenes(33 个)
|
|
216
224
|
|
|
217
|
-
#### App 应用级(
|
|
225
|
+
#### App 应用级(7 个)
|
|
218
226
|
|
|
219
227
|
| 组件 | 描述 | 主要属性 |
|
|
220
228
|
| ---------------- | ---------------- | ------------------ |
|
|
221
229
|
| `FeedbackModal` | 反馈模态框 | `open`, `onsubmit` |
|
|
222
230
|
| `SettingsModal` | 设置模态框 | `open`, `onclose` |
|
|
231
|
+
| `SettingsLayout` | 设置页布局(分区导航 + 内容区) | `title`, `description`, `sections`, `active`, `onselect` |
|
|
223
232
|
| `LanguageSwitch` | 语言切换 | 无需 Props |
|
|
224
233
|
| `ThemeColorPicker` | 主题色选择器 | `value`, `presets`, `onchange` |
|
|
225
234
|
| `ThemeSelector` | 完整主题选择面板 | 无需 Props |
|
|
226
235
|
| `ThemeToggle` | 明/暗主题切换 | 无需 Props |
|
|
227
236
|
|
|
228
|
-
#### IAM 身份认证(
|
|
237
|
+
#### IAM 身份认证(9 个)
|
|
229
238
|
|
|
230
239
|
| 组件 | 描述 | 主要属性 |
|
|
231
240
|
| -------------------- | ---------- | ----------------------------------------------------- |
|
|
241
|
+
| `AuthShell` | 认证页布局(居中卡片 / 左右分栏) | `variant`, `title`, `subtitle`, `brandTitle`, `brandText`, `illustration`, `description`, `highlights` |
|
|
232
242
|
| `LoginForm` | 登录表单 | `loading`, `errors`, `showRememberMe`, `onsubmit` |
|
|
233
243
|
| `RegisterForm` | 注册表单 | `loading`, `errors`, `fields`, `onsubmit` |
|
|
234
244
|
| `ForgotPasswordForm` | 忘记密码 | `mode`, `loading`, `errors`, `onsubmit` |
|
|
@@ -275,11 +285,19 @@ components/
|
|
|
275
285
|
| 组件 | 描述 | 主要属性 / 能力 |
|
|
276
286
|
| -------------------- | ------------ | ---------------- |
|
|
277
287
|
| `CrudPage` | CRUD 主页面 | `crud`, `data`, `permissions`, `form`, `pagination`, `density`, `nav` |
|
|
278
|
-
| `CrudFilterBar` |
|
|
288
|
+
| `CrudFilterBar` | 过滤工具栏(搜索 + 多类型过滤 + 重置) | `filterFields`, `searchValue`, `onsearch`, `onfilterchange`, `onreset` |
|
|
279
289
|
| `CrudDetailPanel` | 详情面板(抽屉/弹窗) | `open`, `item`, `fields`, `variant`, `drawerWidth`, `modalSize` |
|
|
280
290
|
| `CrudEditPanel` | 编辑面板(抽屉/弹窗) | `open`, `fields`, `variant`, `drawerWidth`, `modalSize`, `onsubmit` |
|
|
281
291
|
| `CrudDeleteConfirm` | 删除确认框 | `open`, `loading`, `onconfirm` |
|
|
282
292
|
|
|
293
|
+
> 列表列头默认支持点击排序(`DataTable` 内置客户端排序,受控模式可通过 `sortKey`/`sortDir`/`onsort` 接管为服务端排序);存在搜索/过滤条件时筛选栏显示「重置」按钮。过滤工具栏支持常用控件:`select`、`boolean`、`number`、`date`、`date-range`、`text`。分页栏统一为 shadcn 风格(总数 / 每页 / 第 X / Y 页 / 首末翻页 / 跳转到)。
|
|
294
|
+
|
|
295
|
+
#### 错误页场景(1 个)
|
|
296
|
+
|
|
297
|
+
| 组件 | 描述 | 主要属性 / 能力 |
|
|
298
|
+
| ----------- | ------------ | ---------------- |
|
|
299
|
+
| `ErrorPage` | 通用错误页(内置 401/403/404/500/503 预设) | `status`, `code`, `title`, `description`, `homeUrl`, `onhome`, `onback` |
|
|
300
|
+
|
|
283
301
|
> `CrudPage` 通过 `form` 配置新建/编辑的展示形式:
|
|
284
302
|
>
|
|
285
303
|
> - `form.variant`:`'drawer'`(抽屉,默认)或 `'modal'`(弹出窗口)。
|
|
@@ -378,6 +396,8 @@ components/
|
|
|
378
396
|
<ToastContainer />
|
|
379
397
|
```
|
|
380
398
|
|
|
399
|
+
`Modal` 不传 `title` / `header` 时会保留右上角关闭按钮,但不额外渲染整条头部。需要调整圆角时可传 `radius="1.4rem"`,内部下拉等浮层需要越界显示时可传 `bodyOverflow="visible"`。
|
|
400
|
+
|
|
381
401
|
### 声明式 CRUD 页面(CrudPage)
|
|
382
402
|
|
|
383
403
|
```svelte
|
package/auto-import.d.ts
CHANGED
|
@@ -76,12 +76,14 @@ declare global {
|
|
|
76
76
|
// scenes - app
|
|
77
77
|
const FeedbackModal: typeof import('@h-ai/ui')['FeedbackModal']
|
|
78
78
|
const LanguageSwitch: typeof import('@h-ai/ui')['LanguageSwitch']
|
|
79
|
+
const SettingsLayout: typeof import('@h-ai/ui')['SettingsLayout']
|
|
79
80
|
const SettingsModal: typeof import('@h-ai/ui')['SettingsModal']
|
|
80
81
|
const ThemeColorPicker: typeof import('@h-ai/ui')['ThemeColorPicker']
|
|
81
82
|
const ThemeSelector: typeof import('@h-ai/ui')['ThemeSelector']
|
|
82
83
|
const ThemeToggle: typeof import('@h-ai/ui')['ThemeToggle']
|
|
83
84
|
|
|
84
85
|
// scenes - iam
|
|
86
|
+
const AuthShell: typeof import('@h-ai/ui')['AuthShell']
|
|
85
87
|
const ChangePasswordForm: typeof import('@h-ai/ui')['ChangePasswordForm']
|
|
86
88
|
const ForgotPasswordForm: typeof import('@h-ai/ui')['ForgotPasswordForm']
|
|
87
89
|
const LoginForm: typeof import('@h-ai/ui')['LoginForm']
|
|
@@ -114,6 +116,9 @@ declare global {
|
|
|
114
116
|
const CrudEditPanel: typeof import('@h-ai/ui')['CrudEditPanel']
|
|
115
117
|
const CrudFilterBar: typeof import('@h-ai/ui')['CrudFilterBar']
|
|
116
118
|
const CrudPage: typeof import('@h-ai/ui')['CrudPage']
|
|
119
|
+
|
|
120
|
+
// scenes - error
|
|
121
|
+
const ErrorPage: typeof import('@h-ai/ui')['ErrorPage']
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
export {}
|
package/auto-import.js
CHANGED
|
@@ -66,11 +66,13 @@ const UI_COMPONENTS = new Set([
|
|
|
66
66
|
// scenes - app
|
|
67
67
|
'FeedbackModal',
|
|
68
68
|
'LanguageSwitch',
|
|
69
|
+
'SettingsLayout',
|
|
69
70
|
'SettingsModal',
|
|
70
71
|
'ThemeColorPicker',
|
|
71
72
|
'ThemeSelector',
|
|
72
73
|
'ThemeToggle',
|
|
73
74
|
// scenes - iam
|
|
75
|
+
'AuthShell',
|
|
74
76
|
'ChangePasswordForm',
|
|
75
77
|
'ForgotPasswordForm',
|
|
76
78
|
'LoginForm',
|
|
@@ -99,6 +101,8 @@ const UI_COMPONENTS = new Set([
|
|
|
99
101
|
'CrudEditPanel',
|
|
100
102
|
'CrudFilterBar',
|
|
101
103
|
'CrudPage',
|
|
104
|
+
// scenes - error
|
|
105
|
+
'ErrorPage',
|
|
102
106
|
])
|
|
103
107
|
|
|
104
108
|
const TAG_REGEX = /<([A-Z][A-Za-z0-9]*)\b/g
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
<BitsCalendar.Grid class='w-full border-collapse'>
|
|
89
89
|
<BitsCalendar.GridHead>
|
|
90
90
|
<BitsCalendar.GridRow class='flex w-full'>
|
|
91
|
-
{#each weekdays as day (
|
|
91
|
+
{#each weekdays as day, weekdayIndex (weekdayIndex)}
|
|
92
92
|
<BitsCalendar.HeadCell class='w-9 text-center text-xs font-medium text-base-content/50'>
|
|
93
93
|
{day}
|
|
94
94
|
</BitsCalendar.HeadCell>
|
|
@@ -41,10 +41,13 @@
|
|
|
41
41
|
striped = true,
|
|
42
42
|
hoverable = true,
|
|
43
43
|
density = 'normal',
|
|
44
|
+
sortKey,
|
|
45
|
+
sortDir,
|
|
46
|
+
onsort,
|
|
44
47
|
class: className = '',
|
|
45
48
|
}: {
|
|
46
49
|
data: T[]
|
|
47
|
-
columns: { key: keyof T | string, label: string, width?: string, align?: 'left' | 'center' | 'right', render?: (item: T) => string }[]
|
|
50
|
+
columns: { key: keyof T | string, label: string, width?: string, align?: 'left' | 'center' | 'right', sortable?: boolean, render?: (item: T) => string }[]
|
|
48
51
|
keyField: keyof T
|
|
49
52
|
actions?: Snippet<[T]>
|
|
50
53
|
empty?: Snippet
|
|
@@ -52,10 +55,66 @@
|
|
|
52
55
|
striped?: boolean
|
|
53
56
|
hoverable?: boolean
|
|
54
57
|
density?: 'normal' | 'compact'
|
|
58
|
+
/** 受控排序字段(提供 onsort 时由外部驱动,通常用于服务端排序) */
|
|
59
|
+
sortKey?: string
|
|
60
|
+
/** 受控排序方向 */
|
|
61
|
+
sortDir?: 'asc' | 'desc' | null
|
|
62
|
+
/** 排序回调;提供时为受控模式(不再对 data 做本地排序),否则组件内部对当前数据做客户端排序 */
|
|
63
|
+
onsort?: (key: string, dir: 'asc' | 'desc') => void
|
|
55
64
|
class?: string
|
|
56
65
|
} = $props()
|
|
57
66
|
|
|
58
|
-
type Column = { key: keyof T | string, label: string, width?: string, align?: 'left' | 'center' | 'right', render?: (item: T) => string }
|
|
67
|
+
type Column = { key: keyof T | string, label: string, width?: string, align?: 'left' | 'center' | 'right', sortable?: boolean, render?: (item: T) => string }
|
|
68
|
+
|
|
69
|
+
// ─── 排序状态 ───
|
|
70
|
+
const controlled = $derived(typeof onsort === 'function')
|
|
71
|
+
// 非受控模式下的内部排序状态
|
|
72
|
+
let localSortKey = $state<string | null>(null)
|
|
73
|
+
let localSortDir = $state<'asc' | 'desc'>('asc')
|
|
74
|
+
|
|
75
|
+
const activeSortKey = $derived(controlled ? (sortKey ?? null) : localSortKey)
|
|
76
|
+
const activeSortDir = $derived<'asc' | 'desc' | null>(controlled ? (sortDir ?? null) : (localSortKey ? localSortDir : null))
|
|
77
|
+
|
|
78
|
+
function toggleSort(col: Column) {
|
|
79
|
+
if (!col.sortable)
|
|
80
|
+
return
|
|
81
|
+
const key = String(col.key)
|
|
82
|
+
const nextDir: 'asc' | 'desc' = activeSortKey === key && activeSortDir === 'asc' ? 'desc' : 'asc'
|
|
83
|
+
if (controlled) {
|
|
84
|
+
onsort?.(key, nextDir)
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
localSortKey = key
|
|
88
|
+
localSortDir = nextDir
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 非受控模式:对当前数据做客户端排序(受控模式直接使用外部已排序数据)
|
|
92
|
+
const sortedData = $derived.by(() => {
|
|
93
|
+
if (controlled || !localSortKey)
|
|
94
|
+
return data
|
|
95
|
+
const key = localSortKey
|
|
96
|
+
const dir = localSortDir
|
|
97
|
+
const copy = [...data]
|
|
98
|
+
copy.sort((a, b) => {
|
|
99
|
+
const av = (a as Record<string, unknown>)[key]
|
|
100
|
+
const bv = (b as Record<string, unknown>)[key]
|
|
101
|
+
if (av === bv)
|
|
102
|
+
return 0
|
|
103
|
+
if (av === null || av === undefined)
|
|
104
|
+
return 1
|
|
105
|
+
if (bv === null || bv === undefined)
|
|
106
|
+
return -1
|
|
107
|
+
let cmp: number
|
|
108
|
+
if (typeof av === 'number' && typeof bv === 'number') {
|
|
109
|
+
cmp = av - bv
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
cmp = String(av).localeCompare(String(bv), undefined, { numeric: true })
|
|
113
|
+
}
|
|
114
|
+
return dir === 'asc' ? cmp : -cmp
|
|
115
|
+
})
|
|
116
|
+
return copy
|
|
117
|
+
})
|
|
59
118
|
|
|
60
119
|
const tableClass = $derived(
|
|
61
120
|
density === 'compact'
|
|
@@ -108,11 +167,29 @@
|
|
|
108
167
|
<thead>
|
|
109
168
|
<tr class={headerRowClass}>
|
|
110
169
|
{#each columns as col (String(col.key))}
|
|
170
|
+
{@const isSorted = activeSortKey === String(col.key)}
|
|
111
171
|
<th
|
|
112
172
|
style={col.width ? `width: ${col.width}` : ''}
|
|
113
173
|
class='{getAlignClass(col.align)} font-medium'
|
|
114
174
|
>
|
|
115
|
-
{col.
|
|
175
|
+
{#if col.sortable}
|
|
176
|
+
<button
|
|
177
|
+
type='button'
|
|
178
|
+
class='group inline-flex select-none items-center gap-1 rounded-md px-1.5 py-0.5 -mx-1.5 font-medium transition-colors hover:bg-base-content/5 {isSorted ? 'text-base-content' : ''}'
|
|
179
|
+
onclick={() => toggleSort(col)}
|
|
180
|
+
>
|
|
181
|
+
<span>{col.label}</span>
|
|
182
|
+
{#if isSorted && activeSortDir === 'asc'}
|
|
183
|
+
<span class='icon-[tabler--arrow-narrow-up] size-3.5 text-base-content/70'></span>
|
|
184
|
+
{:else if isSorted && activeSortDir === 'desc'}
|
|
185
|
+
<span class='icon-[tabler--arrow-narrow-down] size-3.5 text-base-content/70'></span>
|
|
186
|
+
{:else}
|
|
187
|
+
<span class='icon-[tabler--arrows-sort] size-3.5 text-base-content/30 group-hover:text-base-content/50'></span>
|
|
188
|
+
{/if}
|
|
189
|
+
</button>
|
|
190
|
+
{:else}
|
|
191
|
+
{col.label}
|
|
192
|
+
{/if}
|
|
116
193
|
</th>
|
|
117
194
|
{/each}
|
|
118
195
|
{#if actions}
|
|
@@ -138,7 +215,7 @@
|
|
|
138
215
|
</td>
|
|
139
216
|
</tr>
|
|
140
217
|
{:else}
|
|
141
|
-
{#each
|
|
218
|
+
{#each sortedData as item (item[keyField])}
|
|
142
219
|
<tr class={rowClass}>
|
|
143
220
|
{#each columns as col (String(col.key))}
|
|
144
221
|
<td class={getAlignClass(col.align)}>{getValue(item, col)}</td>
|
|
@@ -7,6 +7,7 @@ declare function $$render<T>(): {
|
|
|
7
7
|
label: string;
|
|
8
8
|
width?: string;
|
|
9
9
|
align?: "left" | "center" | "right";
|
|
10
|
+
sortable?: boolean;
|
|
10
11
|
render?: (item: T) => string;
|
|
11
12
|
}[];
|
|
12
13
|
keyField: keyof T;
|
|
@@ -16,6 +17,12 @@ declare function $$render<T>(): {
|
|
|
16
17
|
striped?: boolean;
|
|
17
18
|
hoverable?: boolean;
|
|
18
19
|
density?: "normal" | "compact";
|
|
20
|
+
/** 受控排序字段(提供 onsort 时由外部驱动,通常用于服务端排序) */
|
|
21
|
+
sortKey?: string;
|
|
22
|
+
/** 受控排序方向 */
|
|
23
|
+
sortDir?: "asc" | "desc" | null;
|
|
24
|
+
/** 排序回调;提供时为受控模式(不再对 data 做本地排序),否则组件内部对当前数据做客户端排序 */
|
|
25
|
+
onsort?: (key: string, dir: "asc" | "desc") => void;
|
|
19
26
|
class?: string;
|
|
20
27
|
};
|
|
21
28
|
exports: {};
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
<BitsDatePicker.Grid class='w-full border-collapse'>
|
|
102
102
|
<BitsDatePicker.GridHead>
|
|
103
103
|
<BitsDatePicker.GridRow class='flex w-full'>
|
|
104
|
-
{#each weekdays as day (
|
|
104
|
+
{#each weekdays as day, weekdayIndex (weekdayIndex)}
|
|
105
105
|
<BitsDatePicker.HeadCell class='w-9 text-center text-xs font-medium text-base-content/50'>
|
|
106
106
|
{day}
|
|
107
107
|
</BitsDatePicker.HeadCell>
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
open = $bindable(false),
|
|
12
12
|
title = '',
|
|
13
13
|
size = 'md',
|
|
14
|
+
radius = '1rem',
|
|
14
15
|
closeOnBackdrop = true,
|
|
15
16
|
closeOnEscape = true,
|
|
16
17
|
showClose = true,
|
|
@@ -29,8 +30,10 @@
|
|
|
29
30
|
// 只有调用 `showModal()` 进入 top-layer 后,弹框才不会被父级布局和 overflow 裁剪。
|
|
30
31
|
let modalElement: HTMLDialogElement | undefined = $state()
|
|
31
32
|
|
|
32
|
-
//
|
|
33
|
-
|
|
33
|
+
// 标题区仅在显式提供标题或自定义 header 时出现;
|
|
34
|
+
// 仅需关闭按钮的场景改为右上角悬浮按钮,避免业务侧额外占一整行头部。
|
|
35
|
+
const hasHeader = $derived(Boolean(header || title))
|
|
36
|
+
const hasFloatingClose = $derived(showClose && !hasHeader)
|
|
34
37
|
const sizeMap: Record<string, string> = {
|
|
35
38
|
'xs': 'hai-modal__panel--xs',
|
|
36
39
|
'sm': 'hai-modal__panel--sm',
|
|
@@ -55,12 +58,21 @@
|
|
|
55
58
|
styles.push(`--hai-modal-height: ${height.trim()}`)
|
|
56
59
|
}
|
|
57
60
|
|
|
61
|
+
if (typeof radius === 'string' && radius.trim().length > 0) {
|
|
62
|
+
styles.push(`--hai-modal-radius: ${radius.trim()}`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (bodyOverflow === 'visible') {
|
|
66
|
+
styles.push('--hai-modal-panel-overflow: visible')
|
|
67
|
+
}
|
|
68
|
+
|
|
58
69
|
return styles.length > 0 ? styles.join('; ') : undefined
|
|
59
70
|
})
|
|
60
71
|
|
|
61
72
|
const modalBoxClass = $derived(
|
|
62
73
|
cn(
|
|
63
74
|
'hai-modal__panel flex min-h-0 flex-col',
|
|
75
|
+
'relative',
|
|
64
76
|
'bg-base-100 p-0',
|
|
65
77
|
sizeMap[size],
|
|
66
78
|
className,
|
|
@@ -138,6 +150,25 @@
|
|
|
138
150
|
onkeydown={handleViewportKeydown}
|
|
139
151
|
>
|
|
140
152
|
<div class={modalBoxClass} style={panelStyle}>
|
|
153
|
+
{#if hasFloatingClose}
|
|
154
|
+
<button
|
|
155
|
+
type='button'
|
|
156
|
+
class='hai-modal__close hai-modal__close--floating'
|
|
157
|
+
aria-label={uiM('common_close')}
|
|
158
|
+
onclick={handleClose}
|
|
159
|
+
>
|
|
160
|
+
<svg viewBox='0 0 24 24' class='hai-modal__close-icon' aria-hidden='true'>
|
|
161
|
+
<path
|
|
162
|
+
d='M6 6l12 12M18 6L6 18'
|
|
163
|
+
fill='none'
|
|
164
|
+
stroke='currentColor'
|
|
165
|
+
stroke-width='1.9'
|
|
166
|
+
stroke-linecap='round'
|
|
167
|
+
></path>
|
|
168
|
+
</svg>
|
|
169
|
+
</button>
|
|
170
|
+
{/if}
|
|
171
|
+
|
|
141
172
|
{#if hasHeader}
|
|
142
173
|
<!-- 顶部栏单独固定,保证滚动时标题和关闭操作始终可见。 -->
|
|
143
174
|
<div class='hai-modal__header flex flex-none items-start justify-between gap-4 px-6 py-5 sm:px-7'>
|
|
@@ -225,8 +256,8 @@
|
|
|
225
256
|
height: var(--hai-modal-height, auto);
|
|
226
257
|
max-height: calc(100vh - clamp(2rem, 5.2vw, 4rem));
|
|
227
258
|
max-height: calc(100dvh - clamp(2rem, 5.2vw, 4rem));
|
|
228
|
-
border-radius:
|
|
229
|
-
overflow: hidden;
|
|
259
|
+
border-radius: var(--hai-modal-radius, 1rem);
|
|
260
|
+
overflow: var(--hai-modal-panel-overflow, hidden);
|
|
230
261
|
color: var(--color-base-content);
|
|
231
262
|
outline: none;
|
|
232
263
|
box-shadow:
|
|
@@ -247,7 +278,7 @@
|
|
|
247
278
|
border-bottom-right-radius: inherit;
|
|
248
279
|
border-bottom-left-radius: inherit;
|
|
249
280
|
box-shadow: inset 0 1px 0 color-mix(in srgb, var(--color-base-content) 6%, transparent);
|
|
250
|
-
border-radius: 0 0
|
|
281
|
+
border-radius: 0 0 var(--hai-modal-radius, 1rem) var(--hai-modal-radius, 1rem);
|
|
251
282
|
}
|
|
252
283
|
|
|
253
284
|
.hai-modal__close {
|
|
@@ -284,6 +315,13 @@
|
|
|
284
315
|
height: 1rem;
|
|
285
316
|
}
|
|
286
317
|
|
|
318
|
+
.hai-modal__close--floating {
|
|
319
|
+
position: absolute;
|
|
320
|
+
top: 1rem;
|
|
321
|
+
right: 1rem;
|
|
322
|
+
z-index: 1;
|
|
323
|
+
}
|
|
324
|
+
|
|
287
325
|
.hai-modal__panel--xs {
|
|
288
326
|
--hai-modal-width: min(100%, 24rem);
|
|
289
327
|
}
|
|
@@ -342,7 +380,7 @@
|
|
|
342
380
|
max-width: 100%;
|
|
343
381
|
max-height: calc(100vh - 1.7rem);
|
|
344
382
|
max-height: calc(100dvh - 1.7rem);
|
|
345
|
-
border-radius: 1rem;
|
|
383
|
+
border-radius: min(var(--hai-modal-radius, 1rem), 1rem);
|
|
346
384
|
}
|
|
347
385
|
}
|
|
348
386
|
</style>
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<script lang='ts'>
|
|
11
11
|
import type { PaginationProps } from '../../types.js'
|
|
12
12
|
import { uiM } from '../../messages.js'
|
|
13
|
-
import { cn
|
|
13
|
+
import { cn } from '../../utils.js'
|
|
14
14
|
import BareButton from '../primitives/BareButton.svelte'
|
|
15
15
|
|
|
16
16
|
import Input from '../primitives/Input.svelte'
|
|
@@ -36,63 +36,10 @@
|
|
|
36
36
|
// 计算总页数(至少 1 页,保证分页栏在空数据时仍可渲染)
|
|
37
37
|
const totalPages = $derived(Math.max(1, Math.ceil(total / pageSize)))
|
|
38
38
|
|
|
39
|
-
// 生成页码列表
|
|
40
|
-
const pages = $derived(() => {
|
|
41
|
-
const result: (number | string)[] = []
|
|
42
|
-
const maxVisible = 7
|
|
43
|
-
|
|
44
|
-
if (totalPages <= maxVisible) {
|
|
45
|
-
for (let i = 1; i <= totalPages; i++) {
|
|
46
|
-
result.push(i)
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
// 始终显示第一页
|
|
51
|
-
result.push(1)
|
|
52
|
-
|
|
53
|
-
if (page > 3) {
|
|
54
|
-
result.push('...')
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// 中间页码
|
|
58
|
-
const start = Math.max(2, page - 1)
|
|
59
|
-
const end = Math.min(totalPages - 1, page + 1)
|
|
60
|
-
|
|
61
|
-
for (let i = start; i <= end; i++) {
|
|
62
|
-
result.push(i)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (page < totalPages - 2) {
|
|
66
|
-
result.push('...')
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// 始终显示最后一页
|
|
70
|
-
result.push(totalPages)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return result
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
const joinClass = $derived(
|
|
77
|
-
cn(
|
|
78
|
-
'join',
|
|
79
|
-
className,
|
|
80
|
-
),
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
const btnClass = $derived(
|
|
84
|
-
cn(
|
|
85
|
-
'join-item btn',
|
|
86
|
-
getSizeClass(size),
|
|
87
|
-
),
|
|
88
|
-
)
|
|
89
|
-
|
|
90
39
|
const controlSize = $derived(size === 'xs' ? 'xs' : 'sm')
|
|
91
40
|
const jumperControlSize = 'xs'
|
|
92
|
-
const
|
|
93
|
-
const
|
|
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')
|
|
41
|
+
const sizeChangerClass = $derived(size === 'xs' ? 'w-17 shrink-0' : 'w-18 shrink-0')
|
|
42
|
+
const jumperWrapClass = $derived(size === 'xs' ? 'flex min-w-fit items-center gap-1.5 whitespace-nowrap' : 'flex min-w-fit items-center gap-2 whitespace-nowrap')
|
|
96
43
|
const jumperInputClass = $derived(
|
|
97
44
|
size === 'xs'
|
|
98
45
|
? 'w-[2.25rem] shrink-0 [&>.fieldset]:m-0 [&>.fieldset]:min-w-0'
|
|
@@ -122,77 +69,83 @@
|
|
|
122
69
|
onpagesizechange?.(next)
|
|
123
70
|
}
|
|
124
71
|
}
|
|
72
|
+
|
|
73
|
+
// 统一 table 风格:首/上/下/末 导航按钮样式(等比例缩小)
|
|
74
|
+
const navBtnClass = $derived(
|
|
75
|
+
size === 'xs'
|
|
76
|
+
? 'inline-flex h-6 w-6 items-center justify-center rounded-md border border-base-content/12 text-base-content/55 transition-colors hover:bg-base-content/5 hover:text-base-content disabled:pointer-events-none disabled:opacity-40'
|
|
77
|
+
: 'inline-flex h-7 w-7 items-center justify-center rounded-md border border-base-content/12 text-base-content/55 transition-colors hover:bg-base-content/5 hover:text-base-content disabled:pointer-events-none disabled:opacity-40',
|
|
78
|
+
)
|
|
79
|
+
const navIconClass = $derived(size === 'xs' ? 'size-3.5' : 'size-3.5')
|
|
80
|
+
const tableTextClass = $derived(size === 'xs' ? 'text-xs text-base-content/60' : 'text-sm text-base-content/60')
|
|
81
|
+
const rowGapClass = $derived(size === 'xs' ? 'flex flex-wrap items-center gap-3 sm:gap-4' : 'flex flex-wrap items-center gap-4 sm:gap-6')
|
|
82
|
+
const pageInfoText = $derived(
|
|
83
|
+
(labels.pageInfo ?? uiM('pagination_page_info'))
|
|
84
|
+
.replace('{page}', String(page))
|
|
85
|
+
.replace('{total}', String(totalPages)),
|
|
86
|
+
)
|
|
125
87
|
</script>
|
|
126
88
|
|
|
127
|
-
<div class={
|
|
89
|
+
<div class={cn('flex flex-wrap items-center justify-between gap-4', className)}>
|
|
128
90
|
{#if showTotal}
|
|
129
|
-
<span class={
|
|
91
|
+
<span class={tableTextClass}>
|
|
130
92
|
{(labels.total ?? uiM('pagination_total')).replace('{count}', String(total))}
|
|
131
93
|
</span>
|
|
94
|
+
{:else}
|
|
95
|
+
<span></span>
|
|
132
96
|
{/if}
|
|
133
97
|
|
|
134
|
-
{
|
|
135
|
-
|
|
136
|
-
<
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
98
|
+
<div class={rowGapClass}>
|
|
99
|
+
{#if showSizeChanger}
|
|
100
|
+
<div class='flex items-center gap-2'>
|
|
101
|
+
<span class={tableTextClass}>{labels.rowsPerPage ?? uiM('pagination_rows_per_page')}</span>
|
|
102
|
+
<div class={sizeChangerClass}>
|
|
103
|
+
<Select
|
|
104
|
+
size={controlSize}
|
|
105
|
+
value={String(pageSize)}
|
|
106
|
+
onchange={handlePageSizeChange}
|
|
107
|
+
>
|
|
108
|
+
{#each pageSizeOptions as opt (opt)}
|
|
109
|
+
<option value={String(opt)}>{opt}</option>
|
|
110
|
+
{/each}
|
|
111
|
+
</Select>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
{/if}
|
|
115
|
+
|
|
116
|
+
<span class={tableTextClass}>{pageInfoText}</span>
|
|
117
|
+
|
|
118
|
+
<div class='flex items-center gap-1'>
|
|
119
|
+
<BareButton class={navBtnClass} disabled={page === 1} onclick={() => goToPage(1)} ariaLabel='First page'>
|
|
120
|
+
<span class={cn('icon-[tabler--chevrons-left]', navIconClass)}></span>
|
|
121
|
+
</BareButton>
|
|
122
|
+
<BareButton class={navBtnClass} disabled={page === 1} onclick={() => goToPage(page - 1)} ariaLabel='Previous page'>
|
|
123
|
+
<span class={cn('icon-[tabler--chevron-left]', navIconClass)}></span>
|
|
124
|
+
</BareButton>
|
|
125
|
+
<BareButton class={navBtnClass} disabled={page === totalPages} onclick={() => goToPage(page + 1)} ariaLabel='Next page'>
|
|
126
|
+
<span class={cn('icon-[tabler--chevron-right]', navIconClass)}></span>
|
|
127
|
+
</BareButton>
|
|
128
|
+
<BareButton class={navBtnClass} disabled={page === totalPages} onclick={() => goToPage(totalPages)} ariaLabel='Last page'>
|
|
129
|
+
<span class={cn('icon-[tabler--chevrons-right]', navIconClass)}></span>
|
|
130
|
+
</BareButton>
|
|
147
131
|
</div>
|
|
148
|
-
{/if}
|
|
149
|
-
|
|
150
|
-
<div class={joinClass}>
|
|
151
|
-
<BareButton
|
|
152
|
-
class={btnClass}
|
|
153
|
-
disabled={page === 1}
|
|
154
|
-
onclick={() => goToPage(page - 1)}
|
|
155
|
-
>
|
|
156
|
-
«
|
|
157
|
-
</BareButton>
|
|
158
|
-
|
|
159
|
-
{#each pages() as p, index (typeof p === 'number' ? p : `ellipsis-${index}`)}
|
|
160
|
-
{#if typeof p === 'number'}
|
|
161
|
-
<BareButton
|
|
162
|
-
class={cn(btnClass, page === p && 'btn-active')}
|
|
163
|
-
onclick={() => goToPage(p)}
|
|
164
|
-
>
|
|
165
|
-
{p}
|
|
166
|
-
</BareButton>
|
|
167
|
-
{:else}
|
|
168
|
-
<BareButton class={cn(btnClass, 'btn-disabled')} disabled>...</BareButton>
|
|
169
|
-
{/if}
|
|
170
|
-
{/each}
|
|
171
|
-
|
|
172
|
-
<BareButton
|
|
173
|
-
class={btnClass}
|
|
174
|
-
disabled={page === totalPages}
|
|
175
|
-
onclick={() => goToPage(page + 1)}
|
|
176
|
-
>
|
|
177
|
-
»
|
|
178
|
-
</BareButton>
|
|
179
|
-
</div>
|
|
180
132
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
133
|
+
{#if showJumper}
|
|
134
|
+
<div class={jumperWrapClass}>
|
|
135
|
+
<span class={tableTextClass}>{labels.jumpTo ?? uiM('pagination_jump_to')}</span>
|
|
136
|
+
<div class={jumperInputClass}>
|
|
137
|
+
<Input
|
|
138
|
+
type='number'
|
|
139
|
+
size={jumperControlSize}
|
|
140
|
+
class='h-7 w-full rounded-md [&_input]:px-1.5 [&_input]:text-center [&_input]:text-[12px]'
|
|
141
|
+
min={1}
|
|
142
|
+
max={totalPages}
|
|
143
|
+
bind:value={jumperValue}
|
|
144
|
+
onkeydown={(e: KeyboardEvent & { currentTarget: HTMLInputElement }) => e.key === 'Enter' && handleJump()}
|
|
145
|
+
/>
|
|
146
|
+
</div>
|
|
147
|
+
<span class={tableTextClass}>{labels.page ?? uiM('pagination_page')}</span>
|
|
194
148
|
</div>
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
{/if}
|
|
149
|
+
{/if}
|
|
150
|
+
</div>
|
|
198
151
|
</div>
|