@h-ai/ui 0.1.0-alpha.40 → 0.1.0-alpha.41
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 +8 -3
- package/auto-import.d.ts +1 -0
- package/auto-import.js +1 -0
- package/dist/components/compounds/DataTable.svelte +156 -20
- package/dist/components/compounds/DataTable.svelte.d.ts +6 -0
- package/dist/components/compounds/TableToolbar.svelte +691 -0
- package/dist/components/compounds/TableToolbar.svelte.d.ts +129 -0
- package/dist/components/compounds/index.d.ts +1 -0
- package/dist/components/compounds/index.js +2 -0
- package/dist/components/primitives/Select.svelte +28 -0
- package/dist/components/scenes/crud/CrudPage.svelte +354 -81
- package/dist/components/scenes/crud/CrudPage.svelte.d.ts +83 -1
- package/dist/components/scenes/crud/crud-types.d.ts +25 -0
- package/dist/messages/en-US.json +17 -0
- package/dist/messages/zh-CN.json +17 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -181,7 +181,7 @@ components/
|
|
|
181
181
|
| 组件 | 描述 | 主要属性 |
|
|
182
182
|
| ----------- | -------- | ---------------------------------------- |
|
|
183
183
|
| `Card` | 卡片容器 | `title`, `bordered`, `shadow`, `padding` |
|
|
184
|
-
| `DataTable` | 数据表格 | `data`, `columns`, `keyField`, `loading`, `sortKey`, `sortDir`, `onsort`(列 `sortable` 可排序) |
|
|
184
|
+
| `DataTable` | 数据表格 | `data`, `columns`, `keyField`, `loading`, `sortKey`, `sortDir`, `onsort`,`cell` 单元格插槽、`onrowclick`(列 `sortable` 可排序) |
|
|
185
185
|
| `Accordion` | 手风琴 | `items: AccordionItem[]` |
|
|
186
186
|
| `Timeline` | 时间线 | `items: TimelineItem[]` |
|
|
187
187
|
|
|
@@ -334,13 +334,17 @@ components/
|
|
|
334
334
|
|
|
335
335
|
| 组件 | 描述 | 主要属性 / 能力 |
|
|
336
336
|
| -------------------- | ------------ | ---------------- |
|
|
337
|
-
| `CrudPage` | CRUD 主页面 | `crud`, `data`, `permissions`, `form`, `pagination`, `density`, `nav` |
|
|
337
|
+
| `CrudPage` | CRUD 主页面 | `crud`, `data`, `permissions`, `form`, `pagination`, `density`, `showHeader`, `toolbarStyle`, `toolbarLeading`, `toolbarActions`, `card`, `tableCell`, `onrowclick`, `sortableColumns`, `nav` |
|
|
338
338
|
| `CrudFilterBar` | 过滤工具栏(搜索 + 多类型过滤 + 重置) | `filterFields`, `searchValue`, `onsearch`, `onfilterchange`, `onreset` |
|
|
339
339
|
| `CrudDetailPanel` | 详情面板(抽屉/弹窗) | `open`, `item`, `fields`, `variant`, `drawerWidth`, `modalSize` |
|
|
340
340
|
| `CrudEditPanel` | 编辑面板(抽屉/弹窗) | `open`, `fields`, `variant`, `drawerWidth`, `modalSize`, `onsubmit` |
|
|
341
341
|
| `CrudDeleteConfirm` | 删除确认框 | `open`, `loading`, `onconfirm` |
|
|
342
342
|
|
|
343
|
-
>
|
|
343
|
+
> `CrudPage` 默认使用 `TableToolbar` 图标弹层工具栏,也可通过 `toolbarStyle='filter-bar'` 切回平铺筛选栏;`toolbarLeading` 用于注入左侧标题或说明,配合 `showHeader={false}` 可避免重复标题;`toolbarActions` 注入右侧业务操作,`card` 插槽可将列表项替换为卡片视图,`tableCell` 可按行和列 key 渲染交互式单元格内容(例如 Tooltip),`sortableColumns` 控制服务端排序字段。过滤工具栏支持 `select`、`boolean`、`number`、`date`、`date-range`、`text`。
|
|
344
|
+
>
|
|
345
|
+
> 页面根容器会占满父级可用高度。表格无论是否有数据都会撑满剩余空间,仅数据区域滚动;表头固定在数据区顶部,分页栏固定在列表卡片底部。
|
|
346
|
+
>
|
|
347
|
+
> 表格列会优先压缩到 7rem 并截断超长文本;列总宽度仍超过容器时才显示横向滚动条。操作列固定在右侧,横向滚动时保持可见。
|
|
344
348
|
|
|
345
349
|
#### 错误页场景(1 个)
|
|
346
350
|
|
|
@@ -484,6 +488,7 @@ components/
|
|
|
484
488
|
form={{ variant: 'modal', modalSize: 'lg' }}
|
|
485
489
|
pagination={{ showSizeChanger: true, showJumper: true, pageSizeOptions: [10, 20, 50] }}
|
|
486
490
|
density='compact'
|
|
491
|
+
sortableColumns={[{ key: 'createdAt', label: '创建时间' }]}
|
|
487
492
|
{nav}
|
|
488
493
|
/>
|
|
489
494
|
```
|
package/auto-import.d.ts
CHANGED
|
@@ -70,6 +70,7 @@ declare global {
|
|
|
70
70
|
const Skeleton: typeof import('@h-ai/ui')['Skeleton']
|
|
71
71
|
const Steps: typeof import('@h-ai/ui')['Steps']
|
|
72
72
|
const SwipeCell: typeof import('@h-ai/ui')['SwipeCell']
|
|
73
|
+
const TableToolbar: typeof import('@h-ai/ui')['TableToolbar']
|
|
73
74
|
const Tabs: typeof import('@h-ai/ui')['Tabs']
|
|
74
75
|
const TagInput: typeof import('@h-ai/ui')['TagInput']
|
|
75
76
|
const Timeline: typeof import('@h-ai/ui')['Timeline']
|
package/auto-import.js
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
@prop {T[]} data - 表格数据
|
|
6
6
|
@prop {Column<T>[]} columns - 列配置
|
|
7
7
|
@prop {keyof T} keyField - 用于唯一标识行的字段
|
|
8
|
+
@prop {Snippet<[T, string]>} cell - 单元格内容插槽
|
|
8
9
|
@prop {Snippet<[T]>} actions - 操作按钮插槽
|
|
10
|
+
@prop {(item: T) => void} onrowclick - 行点击回调
|
|
9
11
|
@prop {Snippet} empty - 空状态插槽
|
|
10
12
|
@prop {boolean} loading - 是否加载中
|
|
11
13
|
@prop {boolean} striped - 是否斑马纹
|
|
@@ -37,7 +39,9 @@
|
|
|
37
39
|
data,
|
|
38
40
|
columns,
|
|
39
41
|
keyField,
|
|
42
|
+
cell,
|
|
40
43
|
actions,
|
|
44
|
+
onrowclick,
|
|
41
45
|
empty,
|
|
42
46
|
loading = false,
|
|
43
47
|
striped = true,
|
|
@@ -52,7 +56,11 @@
|
|
|
52
56
|
data: T[]
|
|
53
57
|
columns: { key: keyof T | string, label: string, width?: string, align?: 'left' | 'center' | 'right', sortable?: boolean, render?: (item: T) => string }[]
|
|
54
58
|
keyField: keyof T
|
|
59
|
+
/** 自定义单元格内容;参数依次为行记录和列 key。 */
|
|
60
|
+
cell?: Snippet<[T, string]>
|
|
55
61
|
actions?: Snippet<[T]>
|
|
62
|
+
/** 点击数据行时的回调。 */
|
|
63
|
+
onrowclick?: (item: T) => void
|
|
56
64
|
empty?: Snippet
|
|
57
65
|
loading?: boolean
|
|
58
66
|
striped?: boolean
|
|
@@ -67,22 +75,45 @@
|
|
|
67
75
|
class?: string
|
|
68
76
|
} & DataAttributes = $props()
|
|
69
77
|
|
|
78
|
+
/** 透传到根节点的 data-* 属性。 */
|
|
70
79
|
const dataAttributes = $derived(getDataAttributes(restProps))
|
|
71
|
-
|
|
80
|
+
/** DataTable 内部使用的列定义。 */
|
|
81
|
+
type Column = {
|
|
82
|
+
/** 列对应的数据字段。 */
|
|
83
|
+
key: keyof T | string
|
|
84
|
+
/** 表头文案。 */
|
|
85
|
+
label: string
|
|
86
|
+
/** 列宽 CSS 值。 */
|
|
87
|
+
width?: string
|
|
88
|
+
/** 单元格对齐方式。 */
|
|
89
|
+
align?: 'left' | 'center' | 'right'
|
|
90
|
+
/** 是否允许点击表头排序。 */
|
|
91
|
+
sortable?: boolean
|
|
92
|
+
/** 自定义单元格格式化函数。 */
|
|
93
|
+
render?: (item: T) => string
|
|
94
|
+
}
|
|
72
95
|
|
|
73
96
|
// ─── 排序状态 ───
|
|
97
|
+
/** 是否由父组件接管排序;受控模式不复制和排序当前数据。 */
|
|
74
98
|
const controlled = $derived(typeof onsort === 'function')
|
|
75
99
|
// 非受控模式下的内部排序状态
|
|
100
|
+
/** 非受控模式下的本地排序字段。 */
|
|
76
101
|
let localSortKey = $state<string | null>(null)
|
|
102
|
+
/** 非受控模式下的本地排序方向。 */
|
|
77
103
|
let localSortDir = $state<'asc' | 'desc'>('asc')
|
|
78
104
|
|
|
105
|
+
/** 当前生效的排序字段。 */
|
|
79
106
|
const activeSortKey = $derived(controlled ? (sortKey ?? null) : localSortKey)
|
|
107
|
+
/** 当前生效的排序方向。 */
|
|
80
108
|
const activeSortDir = $derived<'asc' | 'desc' | null>(controlled ? (sortDir ?? null) : (localSortKey ? localSortDir : null))
|
|
81
109
|
|
|
110
|
+
/** 切换列排序,并在受控模式下通知父组件。 */
|
|
82
111
|
function toggleSort(col: Column) {
|
|
83
112
|
if (!col.sortable)
|
|
84
113
|
return
|
|
114
|
+
/** 统一为字符串的列 key,用于和受控排序字段比较。 */
|
|
85
115
|
const key = String(col.key)
|
|
116
|
+
/** 同一列再次点击时切换方向,否则从升序开始。 */
|
|
86
117
|
const nextDir: 'asc' | 'desc' = activeSortKey === key && activeSortDir === 'asc' ? 'desc' : 'asc'
|
|
87
118
|
if (controlled) {
|
|
88
119
|
onsort?.(key, nextDir)
|
|
@@ -93,13 +124,17 @@
|
|
|
93
124
|
}
|
|
94
125
|
|
|
95
126
|
// 非受控模式:对当前数据做客户端排序(受控模式直接使用外部已排序数据)
|
|
127
|
+
/** 非受控模式的当前页客户端排序结果。 */
|
|
96
128
|
const sortedData = $derived.by(() => {
|
|
97
129
|
if (controlled || !localSortKey)
|
|
98
130
|
return data
|
|
131
|
+
/** 已确定的排序字段和方向。 */
|
|
99
132
|
const key = localSortKey
|
|
100
133
|
const dir = localSortDir
|
|
134
|
+
/** 复制输入数组,避免客户端排序改变父组件数据。 */
|
|
101
135
|
const copy = [...data]
|
|
102
136
|
copy.sort((a, b) => {
|
|
137
|
+
/** 两条记录的待比较值。 */
|
|
103
138
|
const av = (a as Record<string, unknown>)[key]
|
|
104
139
|
const bv = (b as Record<string, unknown>)[key]
|
|
105
140
|
if (av === bv)
|
|
@@ -108,6 +143,7 @@
|
|
|
108
143
|
return 1
|
|
109
144
|
if (bv === null || bv === undefined)
|
|
110
145
|
return -1
|
|
146
|
+
/** 兼容数值和字符串字段的比较结果。 */
|
|
111
147
|
let cmp: number
|
|
112
148
|
if (typeof av === 'number' && typeof bv === 'number') {
|
|
113
149
|
cmp = av - bv
|
|
@@ -120,32 +156,89 @@
|
|
|
120
156
|
return copy
|
|
121
157
|
})
|
|
122
158
|
|
|
159
|
+
/** 按密度生成表格单元格间距。 */
|
|
123
160
|
const tableClass = $derived(
|
|
124
161
|
density === 'compact'
|
|
125
|
-
? 'table table-sm w-full text-[12px] [&_thead_th]:px-3 [&_thead_th]:py-2 [&_tbody_td]:px-3 [&_tbody_td]:py-2'
|
|
126
|
-
: 'table w-full text-[13px] [&_thead_th]:px-4 [&_thead_th]:py-2.5 [&_tbody_td]:px-4 [&_tbody_td]:py-3',
|
|
162
|
+
? 'table table-sm w-full table-auto text-[12px] [&_thead_th]:px-3 [&_thead_th]:py-2 [&_tbody_td]:px-3 [&_tbody_td]:py-2'
|
|
163
|
+
: 'table w-full table-auto text-[13px] [&_thead_th]:px-4 [&_thead_th]:py-2.5 [&_tbody_td]:px-4 [&_tbody_td]:py-3',
|
|
127
164
|
)
|
|
128
165
|
|
|
166
|
+
/** 表头字号样式;分隔线由每个表头单元格独立绘制,避免 sticky 列形成不同的层叠效果。 */
|
|
129
167
|
const headerRowClass = $derived(
|
|
130
168
|
density === 'compact'
|
|
131
|
-
? 'text-[11px] text-base-content/55
|
|
132
|
-
: 'text-[12px] text-base-content/55
|
|
169
|
+
? 'text-[11px] text-base-content/55'
|
|
170
|
+
: 'text-[12px] text-base-content/55',
|
|
133
171
|
)
|
|
134
172
|
|
|
173
|
+
/** 所有表头单元格共用同一条分隔线,确保普通列与 sticky 操作列的颜色和位置完全一致。 */
|
|
174
|
+
const headerCellDividerClass = `after:absolute after:inset-x-0 after:-bottom-px after:h-px after:bg-base-300 after:content-['']`
|
|
175
|
+
|
|
176
|
+
/** 数据行背景、悬停和分隔线样式;背景直接下发到每个单元格,避免 sticky 层透出下方内容。 */
|
|
135
177
|
const rowClass = $derived(
|
|
136
178
|
hoverable
|
|
137
|
-
?
|
|
138
|
-
:
|
|
179
|
+
? `border-b border-base-content/5 transition-colors [&>td]:bg-base-100 ${striped ? 'even:[&>td]:bg-base-200' : ''} hover:[&>td]:[background-color:color-mix(in_oklab,var(--color-base-content)_3%,var(--color-base-100))] ${striped ? 'even:hover:[&>td]:[background-color:color-mix(in_oklab,var(--color-base-content)_3%,var(--color-base-200))]' : ''} last:border-b-0`
|
|
180
|
+
: `border-b border-base-content/5 [&>td]:bg-base-100 ${striped ? 'even:[&>td]:bg-base-200' : ''} last:border-b-0`,
|
|
139
181
|
)
|
|
140
182
|
|
|
141
|
-
|
|
142
|
-
|
|
183
|
+
/** 承载横向滚动的容器;固定操作列只在此容器中可能遮挡其他列。 */
|
|
184
|
+
let scrollContainer = $state<HTMLDivElement>()
|
|
185
|
+
/** 表格尺寸变化时需重新判断固定操作列是否已脱离其自然位置。 */
|
|
186
|
+
let tableElement = $state<HTMLTableElement>()
|
|
187
|
+
/** 仅在固定操作列覆盖滚动内容时显示其左侧分隔阴影。 */
|
|
188
|
+
let showActionColumnShadow = $state(false)
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 固定列在滚动起点和终点都处于自然位置,不应产生视觉分割;
|
|
192
|
+
* 仅在横向滚动的中间区间覆盖其他列时才显示阴影,避免静态表格多出竖线。
|
|
193
|
+
*/
|
|
194
|
+
function syncActionColumnShadow() {
|
|
195
|
+
if (!scrollContainer || !actions) {
|
|
196
|
+
showActionColumnShadow = false
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const maxScrollLeft = scrollContainer.scrollWidth - scrollContainer.clientWidth
|
|
201
|
+
showActionColumnShadow = scrollContainer.scrollLeft > 1 && scrollContainer.scrollLeft < maxScrollLeft - 1
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
$effect(() => {
|
|
205
|
+
if (!scrollContainer || !tableElement || !actions) {
|
|
206
|
+
showActionColumnShadow = false
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
syncActionColumnShadow()
|
|
211
|
+
const resizeObserver = new ResizeObserver(syncActionColumnShadow)
|
|
212
|
+
resizeObserver.observe(scrollContainer)
|
|
213
|
+
resizeObserver.observe(tableElement)
|
|
214
|
+
|
|
215
|
+
return () => resizeObserver.disconnect()
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* 操作列严格按按钮内容的 max-content 宽度计算,不在少列时预留无效空白。
|
|
220
|
+
* 内容过宽时由表格外层已有的横向滚动容器承接,避免浏览器压缩或裁剪操作按钮。
|
|
221
|
+
*/
|
|
222
|
+
const actionColumnClass = $derived(
|
|
223
|
+
`sticky right-0 z-10 w-max whitespace-nowrap text-center font-medium ${showActionColumnShadow ? 'shadow-[-1px_0_0_color-mix(in_srgb,var(--color-base-content)_10%,transparent)]' : ''}`,
|
|
224
|
+
)
|
|
225
|
+
/** 操作列表头复用统一分隔线,并保持不透明背景以遮挡横向滚动内容。 */
|
|
226
|
+
const actionHeaderClass = $derived(`${actionColumnClass} ${headerCellDividerClass} bg-base-100`)
|
|
227
|
+
/** 数据操作格的背景由 rowClass 统一下发,确保默认、斑马纹和 hover 状态一致且不透明。 */
|
|
228
|
+
const actionCellClass = $derived(actionColumnClass)
|
|
229
|
+
/** 操作按钮保持单行的固有宽度,既不强制填满单元格,也不会被其它列挤压。 */
|
|
230
|
+
const actionWrapClass = $derived(density === 'compact' ? 'inline-flex flex-nowrap items-center justify-center gap-0.5' : 'inline-flex flex-nowrap items-center justify-center gap-1')
|
|
231
|
+
/** 加载和空数据占位单元格样式。 */
|
|
143
232
|
const placeholderCellClass = $derived(density === 'compact' ? 'text-center py-6 text-base-content/40' : 'text-center py-8 text-base-content/40')
|
|
233
|
+
/** 未声明宽度的文本列最多占用的空间,保留其他列与操作列的可见区域。 */
|
|
234
|
+
const defaultContentMaxWidth = 'min(24rem, 40vw)'
|
|
144
235
|
|
|
236
|
+
/** 解析单元格值并应用自定义渲染、空值和时间戳格式化。 */
|
|
145
237
|
function getValue(item: T, col: Column): string {
|
|
146
238
|
if (col.render) {
|
|
147
239
|
return col.render(item)
|
|
148
240
|
}
|
|
241
|
+
/** 当前列的原始字段值。 */
|
|
149
242
|
const value = item[col.key as keyof T]
|
|
150
243
|
if (value === null || value === undefined) {
|
|
151
244
|
return '-'
|
|
@@ -157,6 +250,7 @@
|
|
|
157
250
|
return String(value)
|
|
158
251
|
}
|
|
159
252
|
|
|
253
|
+
/** 将列对齐配置转换为 Tailwind 文本对齐类。 */
|
|
160
254
|
function getAlignClass(align?: string): string {
|
|
161
255
|
switch (align) {
|
|
162
256
|
case 'center': return 'text-center'
|
|
@@ -164,25 +258,49 @@
|
|
|
164
258
|
default: return 'text-left'
|
|
165
259
|
}
|
|
166
260
|
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* 为每列提供可压缩的优先宽度与下限。
|
|
264
|
+
* 显式宽度是调用方的布局契约,允许小于默认列宽;未设置宽度的列才保留 7rem 下限。
|
|
265
|
+
*/
|
|
266
|
+
function getColumnStyle(col: Column): string {
|
|
267
|
+
if (!col.width) {
|
|
268
|
+
return 'width: auto; min-width: 7rem;'
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** 显式宽度仍受视口约束,同时通过 max-width 防止表格自动布局重新撑宽该列。 */
|
|
272
|
+
const boundedWidth = `min(${col.width}, 20vw)`
|
|
273
|
+
return `width: ${boundedWidth}; min-width: 0; max-width: ${boundedWidth};`
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* 限制表头和默认单元格内容的实际可用宽度。
|
|
278
|
+
* 仅给 th 设置 width 会被 table-auto 的内容尺寸重新撑开,必须同时限制 td 内的内容盒子。
|
|
279
|
+
*/
|
|
280
|
+
function getContentStyle(col: Column): string {
|
|
281
|
+
const maxWidth = col.width ? `min(${col.width}, 20vw)` : defaultContentMaxWidth
|
|
282
|
+
return `max-width: ${maxWidth};`
|
|
283
|
+
}
|
|
167
284
|
</script>
|
|
168
285
|
|
|
169
|
-
<div {...dataAttributes} class='overflow-x-auto {className}'>
|
|
170
|
-
<table
|
|
286
|
+
<div {...dataAttributes} bind:this={scrollContainer} class='overflow-x-auto {className}' onscroll={syncActionColumnShadow}>
|
|
287
|
+
<table bind:this={tableElement} class={tableClass}>
|
|
171
288
|
<thead>
|
|
172
289
|
<tr class={headerRowClass}>
|
|
173
290
|
{#each columns as col (String(col.key))}
|
|
174
291
|
{@const isSorted = activeSortKey === String(col.key)}
|
|
175
292
|
<th
|
|
176
|
-
style={col
|
|
177
|
-
class='{getAlignClass(col.align)} font-medium'
|
|
293
|
+
style={getColumnStyle(col)}
|
|
294
|
+
class='{getAlignClass(col.align)} {headerCellDividerClass} relative font-medium'
|
|
178
295
|
>
|
|
179
296
|
{#if col.sortable}
|
|
180
297
|
<button
|
|
181
298
|
type='button'
|
|
182
|
-
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' : ''}'
|
|
299
|
+
class='group inline-flex max-w-full min-w-0 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' : ''}'
|
|
300
|
+
style={getContentStyle(col)}
|
|
183
301
|
onclick={() => toggleSort(col)}
|
|
184
302
|
>
|
|
185
|
-
<span>{col.label}</span>
|
|
303
|
+
<span class='min-w-0 truncate'>{col.label}</span>
|
|
186
304
|
{#if isSorted && activeSortDir === 'asc'}
|
|
187
305
|
<span class='icon-[tabler--arrow-narrow-up] size-3.5 text-base-content/70'></span>
|
|
188
306
|
{:else if isSorted && activeSortDir === 'desc'}
|
|
@@ -192,12 +310,12 @@
|
|
|
192
310
|
{/if}
|
|
193
311
|
</button>
|
|
194
312
|
{:else}
|
|
195
|
-
{col.label}
|
|
313
|
+
<div class='block min-w-0 truncate' style={getContentStyle(col)} title={col.label}>{col.label}</div>
|
|
196
314
|
{/if}
|
|
197
315
|
</th>
|
|
198
316
|
{/each}
|
|
199
317
|
{#if actions}
|
|
200
|
-
<th class={
|
|
318
|
+
<th class='{actionHeaderClass} z-20'>{uiM('data_table_actions')}</th>
|
|
201
319
|
{/if}
|
|
202
320
|
</tr>
|
|
203
321
|
</thead>
|
|
@@ -220,12 +338,20 @@
|
|
|
220
338
|
</tr>
|
|
221
339
|
{:else}
|
|
222
340
|
{#each sortedData as item (item[keyField])}
|
|
223
|
-
<tr class={rowClass}>
|
|
341
|
+
<tr class='{rowClass} {onrowclick ? 'cursor-pointer' : ''}' onclick={() => onrowclick?.(item)}>
|
|
224
342
|
{#each columns as col (String(col.key))}
|
|
225
|
-
<td
|
|
343
|
+
<td style={getColumnStyle(col)} class={getAlignClass(col.align)}>
|
|
344
|
+
{#if cell}
|
|
345
|
+
<div class='min-w-0' style={getContentStyle(col)}>
|
|
346
|
+
{@render cell(item, String(col.key))}
|
|
347
|
+
</div>
|
|
348
|
+
{:else}
|
|
349
|
+
<div class='block min-w-0 truncate' style={getContentStyle(col)} title={getValue(item, col)}>{getValue(item, col)}</div>
|
|
350
|
+
{/if}
|
|
351
|
+
</td>
|
|
226
352
|
{/each}
|
|
227
353
|
{#if actions}
|
|
228
|
-
<td>
|
|
354
|
+
<td class={actionCellClass}>
|
|
229
355
|
<div class={actionWrapClass}>
|
|
230
356
|
{@render actions(item)}
|
|
231
357
|
</div>
|
|
@@ -237,3 +363,13 @@
|
|
|
237
363
|
</tbody>
|
|
238
364
|
</table>
|
|
239
365
|
</div>
|
|
366
|
+
|
|
367
|
+
<style>
|
|
368
|
+
/* 表头层级必须高于固定操作数据格,避免纵向滚动时操作按钮覆盖表头。 */
|
|
369
|
+
thead {
|
|
370
|
+
position: sticky;
|
|
371
|
+
top: 0;
|
|
372
|
+
z-index: 20;
|
|
373
|
+
background: var(--color-base-100);
|
|
374
|
+
}
|
|
375
|
+
</style>
|
|
@@ -12,7 +12,11 @@ declare function $$render<T>(): {
|
|
|
12
12
|
render?: (item: T) => string;
|
|
13
13
|
}[];
|
|
14
14
|
keyField: keyof T;
|
|
15
|
+
/** 自定义单元格内容;参数依次为行记录和列 key。 */
|
|
16
|
+
cell?: Snippet<[T, string]>;
|
|
15
17
|
actions?: Snippet<[T]>;
|
|
18
|
+
/** 点击数据行时的回调。 */
|
|
19
|
+
onrowclick?: (item: T) => void;
|
|
16
20
|
empty?: Snippet;
|
|
17
21
|
loading?: boolean;
|
|
18
22
|
striped?: boolean;
|
|
@@ -52,7 +56,9 @@ interface $$IsomorphicComponent {
|
|
|
52
56
|
* @prop {T[]} data - 表格数据
|
|
53
57
|
* @prop {Column<T>[]} columns - 列配置
|
|
54
58
|
* @prop {keyof T} keyField - 用于唯一标识行的字段
|
|
59
|
+
* @prop {Snippet<[T, string]>} cell - 单元格内容插槽
|
|
55
60
|
* @prop {Snippet<[T]>} actions - 操作按钮插槽
|
|
61
|
+
* @prop {(item: T) => void} onrowclick - 行点击回调
|
|
56
62
|
* @prop {Snippet} empty - 空状态插槽
|
|
57
63
|
* @prop {boolean} loading - 是否加载中
|
|
58
64
|
* @prop {boolean} striped - 是否斑马纹
|