plugin-ui-for-kzt 0.0.28 → 0.0.30
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/dist/components/BaseCheckbox/BaseCheckbox.vue.d.ts +2 -2
- package/dist/components/BaseDropdown/BaseDropdown.vue.d.ts +1 -1
- package/dist/components/BaseInput/BaseInput.vue.d.ts +3 -3
- package/dist/components/BaseInputCalendar/BaseInputCalendar.vue.d.ts +4 -4
- package/dist/components/BaseInputCurrency/BaseInputCurrency.vue.d.ts +3 -3
- package/dist/components/BaseInputEmail/BaseInputEmail.vue.d.ts +3 -3
- package/dist/components/BaseInputPhone/BaseInputPhone.vue.d.ts +3 -3
- package/dist/components/BaseRadio/BaseRadio.vue.d.ts +2 -2
- package/dist/components/BaseSelect/BaseSelect.vue.d.ts +16 -3
- package/dist/components/BaseTable/BaseTable.vue.d.ts +14 -1
- package/dist/components/BaseTextarea/BaseTextarea.vue.d.ts +3 -3
- package/dist/components/BaseToggle/BaseToggle.vue.d.ts +2 -2
- package/dist/components/BaseTooltip/BaseTooltip.vue.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/sprite.svg +1 -1
- package/example/App.vue +271 -7
- package/package.json +1 -1
- package/src/assets/icons/search.svg +4 -0
- package/src/components/BaseButton/BaseButton.vue +20 -20
- package/src/components/BaseSelect/BaseSelect.vue +164 -10
- package/src/components/BaseSelect/README.md +110 -1
- package/src/components/BaseTable/BaseTable.vue +278 -26
- package/src/components/BaseTable/README.md +96 -1
- package/src/components/BaseTooltip/BaseTooltip.vue +9 -5
- package/src/components/BaseUpload/BaseUpload.vue +97 -25
- package/src/types/input.d.ts +1 -0
- package/src/types/table.d.ts +6 -0
- package/src/vue-virtual-scroller.d.ts +4 -0
|
@@ -4,21 +4,22 @@
|
|
|
4
4
|
<table class="base-table__table">
|
|
5
5
|
<thead v-if="hasHeader">
|
|
6
6
|
<slot name="header">
|
|
7
|
-
<tr class="base-table__header">
|
|
7
|
+
<tr class="base-table__header" :style="headerStyle">
|
|
8
8
|
<!-- Чек‑бокс «Выбрать всё» -->
|
|
9
|
-
<th v-if="showRowSelection" class="base-table__header-column">
|
|
9
|
+
<th v-if="showRowSelection" class="base-table__header-column" :style="getColumnStyle(0)">
|
|
10
10
|
<span v-if="checkboxTitle">{{ checkboxTitle }}</span>
|
|
11
11
|
<BaseCheckbox id="selectAll" size="small" v-model="isAllSelected" />
|
|
12
12
|
</th>
|
|
13
13
|
|
|
14
14
|
<!-- Обычные колонки -->
|
|
15
|
-
<th v-for="col in columns"
|
|
15
|
+
<th v-for="(col, index) in columns"
|
|
16
16
|
:key="String(col.key)"
|
|
17
17
|
class="base-table__header-column"
|
|
18
18
|
@click="onHeaderClick(col)"
|
|
19
19
|
:title="col.tooltip"
|
|
20
|
+
:style="{ ...getColumnStyle(getColumnIndex(Number(index))), ...getAlignStyle(col) }"
|
|
20
21
|
>
|
|
21
|
-
<div class="column-header">
|
|
22
|
+
<div class="column-header" :style="{ justifyContent: getAlignStyle(col).justifyContent }">
|
|
22
23
|
<component
|
|
23
24
|
v-if="col.icons?.[0]"
|
|
24
25
|
:is="col.icons[0]"
|
|
@@ -41,11 +42,84 @@
|
|
|
41
42
|
</th>
|
|
42
43
|
|
|
43
44
|
<!-- Колонка‑действий -->
|
|
44
|
-
<th v-if="showActions" class="base-table__cell base-table__header-column base-table__actions-column"></th>
|
|
45
|
+
<th v-if="showActions" class="base-table__cell base-table__header-column base-table__actions-column" :style="getColumnStyle(actionsColumnIndex)"></th>
|
|
46
|
+
|
|
47
|
+
<!-- Колонка меню -->
|
|
48
|
+
<th v-if="$slots['row-menu']" class="base-table__cell base-table__header-column base-table__menu-column" :style="getColumnStyle(menuColumnIndex)"></th>
|
|
45
49
|
</tr>
|
|
46
50
|
</slot>
|
|
47
51
|
</thead>
|
|
48
|
-
|
|
52
|
+
<!-- Виртуальный скролл для больших списков -->
|
|
53
|
+
<template v-if="useVirtualScroll">
|
|
54
|
+
<tbody class="base-table__virtual-tbody">
|
|
55
|
+
<tr>
|
|
56
|
+
<td :colspan="colspan" class="base-table__virtual-cell">
|
|
57
|
+
<recycle-scroller
|
|
58
|
+
:items="paginatedData"
|
|
59
|
+
:item-size="60"
|
|
60
|
+
key-field="id"
|
|
61
|
+
class="base-table__scroller"
|
|
62
|
+
:style="{ maxHeight: virtualScrollHeight + 'px' }"
|
|
63
|
+
@scroll="handleScroll"
|
|
64
|
+
>
|
|
65
|
+
<template #default="{ item, index }: { item: Row, index: number }">
|
|
66
|
+
<div class="base-table__virtual-row" :style="{ gridTemplateColumns: virtualGridColumns }">
|
|
67
|
+
<!-- Чек‑бокс отдельной строки -->
|
|
68
|
+
<div v-if="showRowSelection" class="base-table__virtual-cell-checkbox">
|
|
69
|
+
<BaseCheckbox
|
|
70
|
+
:id="String(item.id || index)"
|
|
71
|
+
size="small"
|
|
72
|
+
:modelValue="isRowSelected(item)"
|
|
73
|
+
:disabled="Boolean(item.disabled)"
|
|
74
|
+
@update:modelValue="toggleRow(item)"
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<!-- Ячейки -->
|
|
79
|
+
<div v-for="col in columns"
|
|
80
|
+
:key="String(col.key)"
|
|
81
|
+
class="base-table__virtual-cell-content"
|
|
82
|
+
:style="getAlignStyle(col)">
|
|
83
|
+
<!-- Пользовательский рендеринг ячейки -->
|
|
84
|
+
<slot
|
|
85
|
+
:name="`cell-${String(col.key)}`"
|
|
86
|
+
:row="item"
|
|
87
|
+
:value="item[col.key]"
|
|
88
|
+
>
|
|
89
|
+
{{ item[col.key] }}
|
|
90
|
+
</slot>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<!-- Колонка‑действий -->
|
|
94
|
+
<div v-if="showActions" class="base-table__virtual-cell-actions">
|
|
95
|
+
<slot name="actions" :row="item">
|
|
96
|
+
<BaseButton
|
|
97
|
+
v-for="action in actions"
|
|
98
|
+
:key="action.key"
|
|
99
|
+
:disabled="Boolean(item.disabled)"
|
|
100
|
+
size="extra-small"
|
|
101
|
+
color="quaternary-gray"
|
|
102
|
+
only-icon
|
|
103
|
+
@click="action.handler(item)"
|
|
104
|
+
>
|
|
105
|
+
<BaseIcon :name="typeof action.icon === 'string' ? action.icon : 'help'" size="custom" />
|
|
106
|
+
</BaseButton>
|
|
107
|
+
</slot>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<div v-if="$slots['row-menu']" class="base-table__virtual-cell-menu">
|
|
111
|
+
<slot name="row-menu" :row="item"></slot>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</template>
|
|
115
|
+
</recycle-scroller>
|
|
116
|
+
</td>
|
|
117
|
+
</tr>
|
|
118
|
+
</tbody>
|
|
119
|
+
</template>
|
|
120
|
+
|
|
121
|
+
<!-- Обычный рендеринг для небольших списков -->
|
|
122
|
+
<transition-group v-else tag="tbody"
|
|
49
123
|
name="row"
|
|
50
124
|
class="base-table__tbody"
|
|
51
125
|
appear
|
|
@@ -54,18 +128,18 @@
|
|
|
54
128
|
<template v-if="paginatedData.length">
|
|
55
129
|
<tr
|
|
56
130
|
v-for="(row, rowIndex) in paginatedData"
|
|
57
|
-
:key="rowId(row)"
|
|
58
|
-
:class="{ '--is-disabled': row.disabled }"
|
|
131
|
+
:key="String(rowId(row))"
|
|
132
|
+
:class="{ '--is-disabled': Boolean(row.disabled) }"
|
|
59
133
|
class="base-table__row"
|
|
60
134
|
>
|
|
61
135
|
|
|
62
136
|
<!-- Чек‑бокс отдельной строки -->
|
|
63
137
|
<td v-if="showRowSelection" class="base-table__cell">
|
|
64
138
|
<BaseCheckbox
|
|
65
|
-
:id="row.id"
|
|
139
|
+
:id="String(row.id || rowIndex)"
|
|
66
140
|
size="small"
|
|
67
141
|
:modelValue="isRowSelected(row)"
|
|
68
|
-
:disabled="row.disabled"
|
|
142
|
+
:disabled="Boolean(row.disabled)"
|
|
69
143
|
@update:modelValue="toggleRow(row)"
|
|
70
144
|
/>
|
|
71
145
|
</td>
|
|
@@ -73,7 +147,8 @@
|
|
|
73
147
|
<!-- Ячейки -->
|
|
74
148
|
<td v-for="col in columns"
|
|
75
149
|
:key="String(col.key)"
|
|
76
|
-
class="base-table__cell"
|
|
150
|
+
class="base-table__cell"
|
|
151
|
+
:style="getAlignStyle(col)">
|
|
77
152
|
<!-- Пользовательский рендеринг ячейки -->
|
|
78
153
|
<slot
|
|
79
154
|
:name="`cell-${String(col.key)}`"
|
|
@@ -90,12 +165,13 @@
|
|
|
90
165
|
<BaseButton
|
|
91
166
|
v-for="action in actions"
|
|
92
167
|
:key="action.key"
|
|
93
|
-
:disabled="row.disabled"
|
|
168
|
+
:disabled="Boolean(row.disabled)"
|
|
94
169
|
size="extra-small"
|
|
95
170
|
color="quaternary-gray"
|
|
171
|
+
only-icon
|
|
96
172
|
@click="action.handler(row)"
|
|
97
173
|
>
|
|
98
|
-
<BaseIcon :name="action.icon" size="
|
|
174
|
+
<BaseIcon :name="typeof action.icon === 'string' ? action.icon : 'help'" size="custom" />
|
|
99
175
|
</BaseButton>
|
|
100
176
|
</slot>
|
|
101
177
|
</td>
|
|
@@ -135,6 +211,8 @@
|
|
|
135
211
|
|
|
136
212
|
<script setup lang="ts">
|
|
137
213
|
import { ref, computed, watch, defineProps, defineEmits, useSlots } from 'vue'
|
|
214
|
+
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
|
215
|
+
import { RecycleScroller } from 'vue-virtual-scroller'
|
|
138
216
|
import BasePagination from '../BasePagination/BasePagination.vue'
|
|
139
217
|
import BaseCheckbox from '../BaseCheckbox/BaseCheckbox.vue'
|
|
140
218
|
import BaseButton from '../BaseButton/BaseButton.vue'
|
|
@@ -152,6 +230,7 @@ const emit = defineEmits<{
|
|
|
152
230
|
(e: 'select-all', selected: boolean): void
|
|
153
231
|
(e: 'sort-change', payload: { key: string; order: 'asc' | 'desc' }): void
|
|
154
232
|
(e: 'page-change', page: number): void
|
|
233
|
+
(e: 'scroll-end'): void
|
|
155
234
|
}>()
|
|
156
235
|
|
|
157
236
|
const colspan = computed(() => {
|
|
@@ -195,14 +274,15 @@ function onHeaderClick(col: Column) {
|
|
|
195
274
|
|
|
196
275
|
/* ---------- ПАГИНАЦИЯ ---------- */
|
|
197
276
|
const pagination = computed<TPaginationProps>(() => props.pagination ?? { perPage: 10 })
|
|
198
|
-
const perPage = computed(() => pagination.value.
|
|
277
|
+
const perPage = computed(() => pagination.value.perPage ?? 10)
|
|
199
278
|
|
|
200
279
|
const totalPages = computed(() => {
|
|
201
280
|
if (!props.pagination) return 1
|
|
202
|
-
|
|
281
|
+
const perPageValue = Number(perPage.value) || 10
|
|
282
|
+
return Math.max(1, Math.ceil(props.data.length / perPageValue))
|
|
203
283
|
})
|
|
204
284
|
|
|
205
|
-
const showPagination = computed(() => totalPages.value > 1)
|
|
285
|
+
const showPagination = computed(() => props.pagination && totalPages.value > 1)
|
|
206
286
|
|
|
207
287
|
const currentPage = ref(pagination.value.currentPage ?? 1)
|
|
208
288
|
|
|
@@ -212,11 +292,121 @@ function onPageChange(page: number) {
|
|
|
212
292
|
}
|
|
213
293
|
|
|
214
294
|
const paginatedData = computed(() => {
|
|
215
|
-
|
|
216
|
-
|
|
295
|
+
if (!props.pagination) {
|
|
296
|
+
return props.data
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const perPageValue = Number(perPage.value) || 10
|
|
300
|
+
const start = (currentPage.value - 1) * perPageValue
|
|
301
|
+
const end = start + perPageValue
|
|
217
302
|
return props.data.slice(start, end)
|
|
218
303
|
})
|
|
219
304
|
|
|
305
|
+
const useVirtualScroll = computed<boolean>(() => props.virtualScroll && props.data.length > (props.virtualScrollThreshold ?? 100))
|
|
306
|
+
const virtualScrollHeight = computed<number>(() => props.virtualScrollHeight ?? 400)
|
|
307
|
+
|
|
308
|
+
const scrollbarWidth = ref<number>(0)
|
|
309
|
+
|
|
310
|
+
function getScrollbarWidth(): number {
|
|
311
|
+
const outer = document.createElement('div')
|
|
312
|
+
outer.style.visibility = 'hidden'
|
|
313
|
+
outer.style.overflow = 'scroll'
|
|
314
|
+
document.body.appendChild(outer)
|
|
315
|
+
|
|
316
|
+
const inner = document.createElement('div')
|
|
317
|
+
outer.appendChild(inner)
|
|
318
|
+
|
|
319
|
+
const width = outer.offsetWidth - inner.offsetWidth
|
|
320
|
+
document.body.removeChild(outer)
|
|
321
|
+
|
|
322
|
+
return width
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
watch(useVirtualScroll, (isVirtual) => {
|
|
326
|
+
if (isVirtual) {
|
|
327
|
+
scrollbarWidth.value = getScrollbarWidth()
|
|
328
|
+
}
|
|
329
|
+
}, { immediate: true })
|
|
330
|
+
|
|
331
|
+
const allColumnWidths = computed(() => {
|
|
332
|
+
const widths = []
|
|
333
|
+
|
|
334
|
+
if (props.showRowSelection) {
|
|
335
|
+
widths.push('60px')
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
for (let i = 0; i < props.columns.length; i++) {
|
|
339
|
+
const col = props.columns[i]
|
|
340
|
+
if (col.width) {
|
|
341
|
+
widths.push(typeof col.width === 'number' ? `${col.width}px` : col.width)
|
|
342
|
+
} else {
|
|
343
|
+
widths.push('1fr')
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (props.showActions) {
|
|
348
|
+
widths.push('auto')
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (useSlots()['row-menu']) {
|
|
352
|
+
widths.push('auto')
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return widths as string[]
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
const virtualGridColumns = computed<string>(() => allColumnWidths.value.join(' '))
|
|
359
|
+
|
|
360
|
+
const actionsColumnIndex = computed<number>(() => {
|
|
361
|
+
const total = allColumnWidths.value.length
|
|
362
|
+
return useSlots()['row-menu'] ? total - 2 : total - 1
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
const menuColumnIndex = computed<number>(() => allColumnWidths.value.length - 1)
|
|
366
|
+
|
|
367
|
+
function getColumnIndex(colIndex: number): number {
|
|
368
|
+
return props.showRowSelection ? colIndex + 1 : colIndex
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const headerStyle = computed(() => {
|
|
372
|
+
if (!useVirtualScroll.value) return {}
|
|
373
|
+
return {
|
|
374
|
+
display: 'grid' as const,
|
|
375
|
+
gridTemplateColumns: virtualGridColumns.value,
|
|
376
|
+
paddingRight: `${scrollbarWidth.value}px`
|
|
377
|
+
}
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
function getColumnStyle(index: number): Record<string, string | undefined> {
|
|
381
|
+
if (!useVirtualScroll.value) return {}
|
|
382
|
+
|
|
383
|
+
const width = allColumnWidths.value[index]
|
|
384
|
+
if (!width) return {}
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
width: width === 'auto' || width.includes('fr') ? width : width,
|
|
388
|
+
minWidth: width === 'auto' || width.includes('fr') ? undefined : width,
|
|
389
|
+
maxWidth: width === 'auto' || width.includes('fr') ? undefined : width,
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function getAlignStyle(col: Column): { textAlign: 'left' | 'center' | 'right'; justifyContent: string } {
|
|
394
|
+
const align = col.align || 'left'
|
|
395
|
+
return {
|
|
396
|
+
textAlign: align,
|
|
397
|
+
justifyContent: align === 'right' ? 'flex-end' : align === 'center' ? 'center' : 'flex-start'
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const handleScroll = (event: Event) => {
|
|
402
|
+
const target = event.target as HTMLElement
|
|
403
|
+
const { scrollTop, scrollHeight, clientHeight } = target
|
|
404
|
+
|
|
405
|
+
if (scrollTop + clientHeight >= scrollHeight - 100) {
|
|
406
|
+
emit('scroll-end')
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
220
410
|
/* ---------- ВЫБОР СТРОК (чек‑боксы) ---------- */
|
|
221
411
|
const selectedIds = ref<Set<unknown>>(new Set())
|
|
222
412
|
|
|
@@ -255,7 +445,7 @@ function emitSelected() {
|
|
|
255
445
|
watch(
|
|
256
446
|
() => props.data,
|
|
257
447
|
() => {
|
|
258
|
-
const newIds = new Set(props.data.map(rowId))
|
|
448
|
+
const newIds = new Set(props.data.map((row: Row) => rowId(row)))
|
|
259
449
|
selectedIds.value.forEach(id => {
|
|
260
450
|
if (!newIds.has(id)) selectedIds.value.delete(id)
|
|
261
451
|
})
|
|
@@ -263,12 +453,6 @@ watch(
|
|
|
263
453
|
{ deep: true }
|
|
264
454
|
)
|
|
265
455
|
|
|
266
|
-
/* ---------- Подтаблица (expand / collapse) ---------- */
|
|
267
|
-
const expandedRows = ref<Set<number>>(new Set())
|
|
268
|
-
function toggleSubRow(idx: number) {
|
|
269
|
-
if (expandedRows.value.has(idx)) expandedRows.value.delete(idx)
|
|
270
|
-
else expandedRows.value.add(idx)
|
|
271
|
-
}
|
|
272
456
|
</script>
|
|
273
457
|
|
|
274
458
|
<style scoped lang="scss">
|
|
@@ -290,7 +474,7 @@ function toggleSubRow(idx: number) {
|
|
|
290
474
|
&__table {
|
|
291
475
|
width: 100%;
|
|
292
476
|
border-collapse: collapse;
|
|
293
|
-
table-layout:
|
|
477
|
+
table-layout: auto;
|
|
294
478
|
}
|
|
295
479
|
|
|
296
480
|
&__header-column,
|
|
@@ -353,6 +537,11 @@ function toggleSubRow(idx: number) {
|
|
|
353
537
|
text-align: right;
|
|
354
538
|
}
|
|
355
539
|
|
|
540
|
+
&__actions-column {
|
|
541
|
+
display: flex;
|
|
542
|
+
align-items: center;
|
|
543
|
+
}
|
|
544
|
+
|
|
356
545
|
&__footer {
|
|
357
546
|
padding: 20px;
|
|
358
547
|
display: flex;
|
|
@@ -408,4 +597,67 @@ function toggleSubRow(idx: number) {
|
|
|
408
597
|
.base-table__tbody {
|
|
409
598
|
position: relative;
|
|
410
599
|
}
|
|
600
|
+
|
|
601
|
+
.base-table__virtual-tbody {
|
|
602
|
+
width: 100%;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.base-table__virtual-cell {
|
|
606
|
+
padding: 0;
|
|
607
|
+
border: none;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
.base-table__scroller {
|
|
611
|
+
width: 100%;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
.base-table__virtual-row {
|
|
615
|
+
display: grid;
|
|
616
|
+
align-items: center;
|
|
617
|
+
width: 100%;
|
|
618
|
+
min-height: 60px;
|
|
619
|
+
border-bottom: 1px solid var(--primary-black-100);
|
|
620
|
+
padding: var(--spacing-l) 0;
|
|
621
|
+
gap: 0;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
.base-table__virtual-row:hover {
|
|
625
|
+
background: var(--primary-black-100);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
.base-table__virtual-row.--is-disabled {
|
|
629
|
+
cursor: not-allowed;
|
|
630
|
+
background: var(--primary-black-50);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
.base-table__virtual-cell-checkbox {
|
|
634
|
+
display: flex;
|
|
635
|
+
align-items: center;
|
|
636
|
+
padding: 0 var(--spacing-xl);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
.base-table__virtual-cell-content {
|
|
640
|
+
display: flex;
|
|
641
|
+
align-items: center;
|
|
642
|
+
text-align: left;
|
|
643
|
+
white-space: nowrap;
|
|
644
|
+
padding: 0 var(--spacing-xl);
|
|
645
|
+
|
|
646
|
+
@include text-clamp(1);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
.base-table__virtual-cell-actions {
|
|
650
|
+
display: flex;
|
|
651
|
+
align-items: center;
|
|
652
|
+
gap: var(--spacing-xs);
|
|
653
|
+
justify-content: flex-end;
|
|
654
|
+
padding: 0 var(--spacing-xl);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
.base-table__virtual-cell-menu {
|
|
658
|
+
display: flex;
|
|
659
|
+
align-items: center;
|
|
660
|
+
justify-content: center;
|
|
661
|
+
padding: 0 var(--spacing-xl);
|
|
662
|
+
}
|
|
411
663
|
</style>
|
|
@@ -73,8 +73,26 @@ export interface Column {
|
|
|
73
73
|
icons?: [any, any] // любая vue‑компонента
|
|
74
74
|
/** Делает колонку сортируемой */
|
|
75
75
|
sortable?: boolean
|
|
76
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* Ширина колонки (необязательно)
|
|
78
|
+
* Поддерживаемые форматы:
|
|
79
|
+
* - Строка с единицами: '120px', '1fr', '2fr', 'auto'
|
|
80
|
+
* - Число (будет преобразовано в пиксели): 120 → '120px'
|
|
81
|
+
* - Если не указано, используется '1fr' (равномерное распределение)
|
|
82
|
+
*
|
|
83
|
+
* Важно: ширины применяются как к обычной таблице, так и к виртуальному скроллу,
|
|
84
|
+
* обеспечивая полную синхронизацию thead и tbody
|
|
85
|
+
*/
|
|
77
86
|
width?: string | number
|
|
87
|
+
/**
|
|
88
|
+
* Выравнивание содержимого колонки (необязательно)
|
|
89
|
+
* - 'left' - выравнивание влево (по умолчанию)
|
|
90
|
+
* - 'center' - выравнивание по центру
|
|
91
|
+
* - 'right' - выравнивание вправо
|
|
92
|
+
*
|
|
93
|
+
* Применяется как к заголовку, так и к ячейкам колонки
|
|
94
|
+
*/
|
|
95
|
+
align?: 'left' | 'center' | 'right'
|
|
78
96
|
}
|
|
79
97
|
|
|
80
98
|
export interface TPaginationProps {
|
|
@@ -292,3 +310,80 @@ const rowActions = [
|
|
|
292
310
|
</BaseDropdown>
|
|
293
311
|
</template>
|
|
294
312
|
</BaseTable>
|
|
313
|
+
|
|
314
|
+
### 3.8. Настройка ширины колонок
|
|
315
|
+
|
|
316
|
+
Можно точно контролировать ширину каждой колонки через свойство `width`:
|
|
317
|
+
|
|
318
|
+
```vue
|
|
319
|
+
<template>
|
|
320
|
+
<BaseTable
|
|
321
|
+
:data="data"
|
|
322
|
+
:columns="columns"
|
|
323
|
+
:virtual-scroll="true"
|
|
324
|
+
:virtual-scroll-height="400"
|
|
325
|
+
:show-row-selection="true"
|
|
326
|
+
:show-actions="true"
|
|
327
|
+
/>
|
|
328
|
+
</template>
|
|
329
|
+
|
|
330
|
+
<script setup lang="ts">
|
|
331
|
+
import BaseTable from '@/components/BaseTable/BaseTable.vue'
|
|
332
|
+
|
|
333
|
+
const columns = [
|
|
334
|
+
{ key: 'id', label: 'ID', width: '80px' }, // Фиксированная ширина
|
|
335
|
+
{ key: 'name', label: 'Имя', width: 200 }, // Число автоматически → '200px'
|
|
336
|
+
{ key: 'email', label: 'Email', width: '1fr' }, // Гибкая ширина (CSS Grid)
|
|
337
|
+
{ key: 'status', label: 'Статус', width: '120px' } // Фиксированная ширина
|
|
338
|
+
]
|
|
339
|
+
|
|
340
|
+
const data = [
|
|
341
|
+
{ id: 1, name: 'Иван', email: 'ivan@example.com', status: 'Активен' },
|
|
342
|
+
// ...
|
|
343
|
+
]
|
|
344
|
+
</script>
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**Преимущества:**
|
|
348
|
+
- ✅ Ширины колонок **синхронизированы** между `<thead>` и виртуальным скроллом
|
|
349
|
+
- ✅ Поддержка различных единиц: `px`, `fr`, `auto`, числа
|
|
350
|
+
- ✅ Если `width` не указан, используется `1fr` (равномерное распределение)
|
|
351
|
+
- ✅ Особенно важно для виртуального скролла, где `thead` и `tbody` используют разные системы рендеринга
|
|
352
|
+
- ✅ Автоматическая компенсация ширины скроллбара - thead получает `padding-right`, предотвращая смещение колонок
|
|
353
|
+
|
|
354
|
+
### 3.9. Выравнивание содержимого колонок
|
|
355
|
+
|
|
356
|
+
Вы можете настроить выравнивание для каждой колонки через свойство `align`:
|
|
357
|
+
|
|
358
|
+
```vue
|
|
359
|
+
<template>
|
|
360
|
+
<BaseTable
|
|
361
|
+
:data="data"
|
|
362
|
+
:columns="columns"
|
|
363
|
+
/>
|
|
364
|
+
</template>
|
|
365
|
+
|
|
366
|
+
<script setup lang="ts">
|
|
367
|
+
import BaseTable from '@/components/BaseTable/BaseTable.vue'
|
|
368
|
+
|
|
369
|
+
const columns = [
|
|
370
|
+
{ key: 'id', label: 'ID', width: '80px', align: 'center' }, // ID по центру
|
|
371
|
+
{ key: 'name', label: 'Имя', width: '200px', align: 'left' }, // Текст влево (по умолчанию)
|
|
372
|
+
{ key: 'price', label: 'Цена', width: '120px', align: 'right' }, // Числа вправо
|
|
373
|
+
{ key: 'status', label: 'Статус', width: '100px', align: 'center' } // Статус по центру
|
|
374
|
+
]
|
|
375
|
+
|
|
376
|
+
const data = [
|
|
377
|
+
{ id: 1, name: 'Товар 1', price: '1000₸', status: 'Активен' },
|
|
378
|
+
{ id: 2, name: 'Товар 2', price: '2500₸', status: 'Неактивен' },
|
|
379
|
+
// ...
|
|
380
|
+
]
|
|
381
|
+
</script>
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
**Когда использовать:**
|
|
385
|
+
- `align: 'left'` - текстовые данные (имена, описания) - по умолчанию
|
|
386
|
+
- `align: 'center'` - короткие значения (ID, статусы, иконки)
|
|
387
|
+
- `align: 'right'` - числовые данные (цены, количество, проценты)
|
|
388
|
+
|
|
389
|
+
**Важно:** Выравнивание применяется и к заголовку колонки, и к её содержимому, обеспечивая визуальную согласованность.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="base-tooltip-container">
|
|
2
|
+
<div class="base-tooltip-container" ref="containerRef">
|
|
3
3
|
<div
|
|
4
|
+
ref="triggerRef"
|
|
4
5
|
class="base-tooltip__trigger"
|
|
5
6
|
@mouseenter="showTooltip"
|
|
6
7
|
@mouseleave="hideTooltip"
|
|
@@ -17,6 +18,7 @@
|
|
|
17
18
|
<Transition name="tooltip">
|
|
18
19
|
<div
|
|
19
20
|
v-if="isVisible"
|
|
21
|
+
ref="tooltipRef"
|
|
20
22
|
class="base-tooltip"
|
|
21
23
|
:class="classList"
|
|
22
24
|
>
|
|
@@ -42,6 +44,9 @@ const props = withDefaults(defineProps<ITooltipProps>(), {
|
|
|
42
44
|
});
|
|
43
45
|
|
|
44
46
|
const isVisible = ref(false);
|
|
47
|
+
const containerRef = ref<HTMLElement>();
|
|
48
|
+
const triggerRef = ref<HTMLElement>();
|
|
49
|
+
const tooltipRef = ref<HTMLElement>();
|
|
45
50
|
|
|
46
51
|
function showTooltip() {
|
|
47
52
|
if (props.trigger === 'hover') {
|
|
@@ -62,10 +67,9 @@ function toggleTooltip() {
|
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
function handleClickOutside(event: MouseEvent) {
|
|
65
|
-
if (props.trigger === 'click' && isVisible.value) {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
if (tooltip && trigger && !tooltip.contains(event.target as Node) && !trigger.contains(event.target as Node)) {
|
|
70
|
+
if (props.trigger === 'click' && isVisible.value && containerRef.value) {
|
|
71
|
+
const target = event.target as Node;
|
|
72
|
+
if (!containerRef.value.contains(target)) {
|
|
69
73
|
isVisible.value = false;
|
|
70
74
|
}
|
|
71
75
|
}
|