plugin-ui-for-kzt 0.0.52 → 0.0.53
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/index.js +1 -1
- package/package.json +1 -1
- package/src/components/BaseTable/BaseTable.vue +481 -468
|
@@ -1,279 +1,283 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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"
|
|
123
|
-
name="row"
|
|
124
|
-
class="base-table__tbody"
|
|
125
|
-
appear
|
|
2
|
+
<div class="base-table">
|
|
3
|
+
<div class="base-table__wrapper">
|
|
4
|
+
<div class="base-table__scroll-area">
|
|
5
|
+
<table class="base-table__table">
|
|
6
|
+
<thead v-if="hasHeader">
|
|
7
|
+
<slot name="header">
|
|
8
|
+
<tr class="base-table__header" :style="headerStyle">
|
|
9
|
+
<!-- Чек‑бокс «Выбрать всё» -->
|
|
10
|
+
<th v-if="showRowSelection" class="base-table__header-column" :style="getColumnStyle(0)">
|
|
11
|
+
<span v-if="checkboxTitle">{{ checkboxTitle }}</span>
|
|
12
|
+
<BaseCheckbox id="selectAll" size="small" v-model="isAllSelected"/>
|
|
13
|
+
</th>
|
|
14
|
+
|
|
15
|
+
<!-- Обычные колонки -->
|
|
16
|
+
<th v-for="(col, index) in columns"
|
|
17
|
+
:key="String(col.key)"
|
|
18
|
+
class="base-table__header-column"
|
|
19
|
+
@click="onHeaderClick(col)"
|
|
20
|
+
:title="col.tooltip"
|
|
21
|
+
:style="{ ...getColumnStyle(getColumnIndex(Number(index))), ...getAlignStyle(col) }"
|
|
22
|
+
>
|
|
23
|
+
<div class="column-header" :style="{ justifyContent: getAlignStyle(col).justifyContent }">
|
|
24
|
+
<component
|
|
25
|
+
v-if="col.icons?.[0]"
|
|
26
|
+
:is="col.icons[0]"
|
|
27
|
+
class="header-icon"
|
|
28
|
+
/>
|
|
29
|
+
<span v-if="col.label">{{ col.label }}</span>
|
|
30
|
+
<component
|
|
31
|
+
v-if="col.icons?.[1]"
|
|
32
|
+
:is="col.icons[1]"
|
|
33
|
+
class="header-icon"
|
|
34
|
+
/>
|
|
35
|
+
<BaseIcon
|
|
36
|
+
v-if="col.sortable"
|
|
37
|
+
name="arrow-up"
|
|
38
|
+
size="extra-small"
|
|
39
|
+
class="base-table__sort-icon"
|
|
40
|
+
:class="{ '--active': sortOrder === 'desc' && sortKey === col.key }"
|
|
41
|
+
/>
|
|
42
|
+
</div>
|
|
43
|
+
</th>
|
|
44
|
+
|
|
45
|
+
<!-- Колонка‑действий -->
|
|
46
|
+
<th v-if="showActions" class="base-table__cell base-table__header-column base-table__actions-column"
|
|
47
|
+
:style="getColumnStyle(actionsColumnIndex)"></th>
|
|
48
|
+
|
|
49
|
+
<!-- Колонка меню -->
|
|
50
|
+
<th v-if="$slots['row-menu']" class="base-table__cell base-table__header-column base-table__menu-column"
|
|
51
|
+
:style="getColumnStyle(menuColumnIndex)"></th>
|
|
52
|
+
</tr>
|
|
53
|
+
</slot>
|
|
54
|
+
</thead>
|
|
55
|
+
<!-- Виртуальный скролл для больших списков -->
|
|
56
|
+
<template v-if="useVirtualScroll">
|
|
57
|
+
<tbody class="base-table__virtual-tbody">
|
|
58
|
+
<tr>
|
|
59
|
+
<td :colspan="colspan" class="base-table__virtual-cell">
|
|
60
|
+
<recycle-scroller
|
|
61
|
+
:items="paginatedData"
|
|
62
|
+
:item-size="60"
|
|
63
|
+
key-field="id"
|
|
64
|
+
class="base-table__scroller"
|
|
65
|
+
:style="{ maxHeight: virtualScrollHeight + 'px' }"
|
|
66
|
+
@scroll="handleScroll"
|
|
126
67
|
>
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
68
|
+
<template #default="{ item, index }: { item: Row, index: number }">
|
|
69
|
+
<div class="base-table__virtual-row" :style="{ gridTemplateColumns: virtualGridColumns }">
|
|
70
|
+
<!-- Чек‑бокс отдельной строки -->
|
|
71
|
+
<div v-if="showRowSelection" class="base-table__virtual-cell-checkbox">
|
|
72
|
+
<BaseCheckbox
|
|
73
|
+
:id="String(item.id || index)"
|
|
74
|
+
size="small"
|
|
75
|
+
:modelValue="isRowSelected(item)"
|
|
76
|
+
:disabled="Boolean(item.disabled)"
|
|
77
|
+
@update:modelValue="toggleRow(item)"
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<!-- Ячейки -->
|
|
82
|
+
<div v-for="col in columns"
|
|
83
|
+
:key="String(col.key)"
|
|
84
|
+
class="base-table__virtual-cell-content"
|
|
85
|
+
:style="getAlignStyle(col)">
|
|
86
|
+
<!-- Пользовательский рендеринг ячейки -->
|
|
87
|
+
<slot
|
|
88
|
+
:name="`cell-${String(col.key)}`"
|
|
89
|
+
:row="item"
|
|
90
|
+
:value="item[col.key]"
|
|
91
|
+
>
|
|
92
|
+
{{ item[col.key] }}
|
|
93
|
+
</slot>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<!-- Колонка‑действий -->
|
|
97
|
+
<div v-if="showActions" class="base-table__virtual-cell-actions">
|
|
98
|
+
<slot name="actions" :row="item">
|
|
99
|
+
<BaseButton
|
|
100
|
+
v-for="action in actions"
|
|
101
|
+
:key="action.key"
|
|
102
|
+
:disabled="Boolean(item.disabled)"
|
|
103
|
+
size="extra-small"
|
|
104
|
+
color="quaternary-gray"
|
|
105
|
+
only-icon
|
|
106
|
+
@click="action.handler(item)"
|
|
107
|
+
>
|
|
108
|
+
<BaseIcon :name="typeof action.icon === 'string' ? action.icon : 'help'" size="custom"/>
|
|
109
|
+
</BaseButton>
|
|
110
|
+
</slot>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<div v-if="$slots['row-menu']" class="base-table__virtual-cell-menu">
|
|
114
|
+
<slot name="row-menu" :row="item"></slot>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</template>
|
|
118
|
+
</recycle-scroller>
|
|
119
|
+
</td>
|
|
120
|
+
</tr>
|
|
121
|
+
</tbody>
|
|
122
|
+
</template>
|
|
123
|
+
|
|
124
|
+
<!-- Обычный рендеринг для небольших списков -->
|
|
125
|
+
<transition-group v-else tag="tbody"
|
|
126
|
+
name="row"
|
|
127
|
+
class="base-table__tbody"
|
|
128
|
+
appear
|
|
129
|
+
>
|
|
130
|
+
<!-- Список строк -->
|
|
131
|
+
<template v-if="paginatedData.length">
|
|
132
|
+
<tr
|
|
133
|
+
v-for="(row, rowIndex) in paginatedData"
|
|
134
|
+
:key="String(rowId(row))"
|
|
135
|
+
:class="{ '--is-disabled': Boolean(row.disabled) }"
|
|
136
|
+
class="base-table__row"
|
|
137
|
+
>
|
|
138
|
+
|
|
139
|
+
<!-- Чек‑бокс отдельной строки -->
|
|
140
|
+
<td v-if="showRowSelection" class="base-table__cell">
|
|
141
|
+
<BaseCheckbox
|
|
142
|
+
:id="String(row.id || rowIndex)"
|
|
143
|
+
size="small"
|
|
144
|
+
:modelValue="isRowSelected(row)"
|
|
145
|
+
:disabled="Boolean(row.disabled)"
|
|
146
|
+
@update:modelValue="toggleRow(row)"
|
|
147
|
+
/>
|
|
148
|
+
</td>
|
|
149
|
+
|
|
150
|
+
<!-- Ячейки -->
|
|
151
|
+
<td v-for="col in columns"
|
|
152
|
+
:key="String(col.key)"
|
|
153
|
+
class="base-table__cell"
|
|
154
|
+
:style="getAlignStyle(col)">
|
|
155
|
+
<!-- Пользовательский рендеринг ячейки -->
|
|
156
|
+
<slot
|
|
157
|
+
:name="`cell-${String(col.key)}`"
|
|
158
|
+
:row="row"
|
|
159
|
+
:value="row[col.key]"
|
|
160
|
+
>
|
|
161
|
+
{{ row[col.key] }}
|
|
162
|
+
</slot>
|
|
163
|
+
</td>
|
|
164
|
+
|
|
165
|
+
<!-- Колонка‑действий -->
|
|
166
|
+
<td v-if="showActions" class="base-table__cell base-table__actions-column">
|
|
167
|
+
<slot name="actions" :row="row">
|
|
168
|
+
<BaseButton
|
|
169
|
+
v-for="action in actions"
|
|
170
|
+
:key="action.key"
|
|
171
|
+
:disabled="Boolean(row.disabled)"
|
|
172
|
+
size="extra-small"
|
|
173
|
+
color="quaternary-gray"
|
|
174
|
+
only-icon
|
|
175
|
+
@click="action.handler(row)"
|
|
176
|
+
>
|
|
177
|
+
<BaseIcon :name="typeof action.icon === 'string' ? action.icon : 'help'" size="custom"/>
|
|
178
|
+
</BaseButton>
|
|
179
|
+
</slot>
|
|
180
|
+
</td>
|
|
181
|
+
|
|
182
|
+
<td v-if="$slots['row-menu']" class="base-table__cell base-table__menu-column">
|
|
183
|
+
<slot name="row-menu" :row="row"></slot>
|
|
184
|
+
</td>
|
|
185
|
+
</tr>
|
|
186
|
+
</template>
|
|
187
|
+
|
|
188
|
+
<!-- Пустой набор данных -->
|
|
189
|
+
<tr v-else>
|
|
190
|
+
<td :colspan="colspan" class="base-table__empty">
|
|
191
|
+
<slot name="empty">
|
|
192
|
+
<div>
|
|
193
|
+
Нет данных
|
|
194
|
+
</div>
|
|
195
|
+
</slot>
|
|
196
|
+
</td>
|
|
197
|
+
</tr>
|
|
198
|
+
</transition-group>
|
|
199
|
+
|
|
200
|
+
</table>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<div v-if="showPagination" class="base-table__footer">
|
|
204
|
+
<BasePagination
|
|
205
|
+
:totalPages="totalPages"
|
|
206
|
+
:currentPage="currentPage"
|
|
207
|
+
size="small"
|
|
208
|
+
v-bind="pagination"
|
|
209
|
+
@update:currentPage="onPageChange"
|
|
210
|
+
/>
|
|
211
|
+
</div>
|
|
209
212
|
</div>
|
|
213
|
+
</div>
|
|
210
214
|
</template>
|
|
211
215
|
|
|
212
216
|
<script setup lang="ts">
|
|
213
|
-
import {
|
|
217
|
+
import {ref, computed, watch, defineProps, defineEmits, useSlots} from 'vue'
|
|
214
218
|
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
|
215
|
-
import {
|
|
219
|
+
import {RecycleScroller} from 'vue-virtual-scroller'
|
|
216
220
|
import BasePagination from '../BasePagination/BasePagination.vue'
|
|
217
221
|
import BaseCheckbox from '../BaseCheckbox/BaseCheckbox.vue'
|
|
218
222
|
import BaseButton from '../BaseButton/BaseButton.vue'
|
|
219
223
|
import BaseIcon from '../BaseIcon/BaseIcon.vue'
|
|
220
224
|
import type {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
225
|
+
Row,
|
|
226
|
+
Column,
|
|
227
|
+
BaseTableProps,
|
|
224
228
|
} from '../../types/table'
|
|
225
|
-
import type {
|
|
229
|
+
import type {TPaginationProps} from '../../types/pagination'
|
|
226
230
|
|
|
227
231
|
const props = defineProps<BaseTableProps>()
|
|
228
232
|
const emit = defineEmits<{
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
233
|
+
(e: 'update:selected', rows: Row[]): void
|
|
234
|
+
(e: 'select-all', selected: boolean): void
|
|
235
|
+
(e: 'sort-change', payload: { key: string; order: 'asc' | 'desc' }): void
|
|
236
|
+
(e: 'page-change', page: number): void
|
|
237
|
+
(e: 'scroll-end'): void
|
|
234
238
|
}>()
|
|
235
239
|
|
|
236
240
|
const colspan = computed(() => {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
241
|
+
const base = props.columns.length
|
|
242
|
+
const extra =
|
|
243
|
+
(props.showRowSelection ? 1 : 0) +
|
|
244
|
+
(props.showActions ? 1 : 0) +
|
|
245
|
+
(useSlots()['row-menu'] ? 1 : 0)
|
|
246
|
+
return base + extra
|
|
243
247
|
})
|
|
244
248
|
|
|
245
249
|
const hasHeader = computed(() => props.columns.length > 0 || !!useSlots().header)
|
|
246
250
|
|
|
247
251
|
/* ---------- Утилиты ---------- */
|
|
248
252
|
function rowId(row: Row): unknown {
|
|
249
|
-
|
|
250
|
-
|
|
253
|
+
if (props.rowKey) {
|
|
254
|
+
return typeof props.rowKey === 'function'
|
|
251
255
|
? (props.rowKey as (r: Row) => unknown)(row)
|
|
252
256
|
: (row as any)[props.rowKey]
|
|
253
|
-
|
|
257
|
+
}
|
|
254
258
|
|
|
255
|
-
|
|
259
|
+
return (row as any).__uid ?? JSON.stringify(row)
|
|
256
260
|
}
|
|
257
261
|
|
|
258
262
|
/* ---------- СОРТИРОВКА ---------- */
|
|
259
|
-
const sortKey
|
|
263
|
+
const sortKey = ref<string | null>(null)
|
|
260
264
|
const sortOrder = ref<'asc' | 'desc'>('asc')
|
|
261
265
|
|
|
262
266
|
function onHeaderClick(col: Column) {
|
|
263
|
-
|
|
267
|
+
if (!col.sortable) return
|
|
264
268
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
269
|
+
if (sortKey.value === col.key) {
|
|
270
|
+
sortOrder.value = sortOrder.value === 'desc' ? 'asc' : 'desc'
|
|
271
|
+
} else {
|
|
272
|
+
sortKey.value = col.key
|
|
273
|
+
sortOrder.value = 'desc'
|
|
274
|
+
}
|
|
271
275
|
|
|
272
|
-
|
|
276
|
+
emit('sort-change', {key: col.key, order: sortOrder.value})
|
|
273
277
|
}
|
|
274
278
|
|
|
275
279
|
/* ---------- ПАГИНАЦИЯ ---------- */
|
|
276
|
-
const pagination = computed<TPaginationProps>(() => props.pagination ?? {
|
|
280
|
+
const pagination = computed<TPaginationProps>(() => props.pagination ?? {perPage: 10})
|
|
277
281
|
const perPage = computed(() => pagination.value.perPage ?? 10)
|
|
278
282
|
|
|
279
283
|
const totalItems = computed(() =>
|
|
@@ -297,23 +301,23 @@ const showPagination = computed(() => props.pagination && totalPages.value > 1)
|
|
|
297
301
|
const currentPage = ref(pagination.value.currentPage ?? 1)
|
|
298
302
|
|
|
299
303
|
function onPageChange(page: number) {
|
|
300
|
-
|
|
301
|
-
|
|
304
|
+
currentPage.value = page
|
|
305
|
+
emit('page-change', page)
|
|
302
306
|
}
|
|
303
307
|
|
|
304
308
|
const paginatedData = computed(() => {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
309
|
+
if (!props.pagination) {
|
|
310
|
+
return props.data
|
|
311
|
+
}
|
|
308
312
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
313
|
+
if (typeof props.pagination.totalItems === 'number') {
|
|
314
|
+
return props.data
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const perPageValue = Number(perPage.value) || 10
|
|
318
|
+
const start = (currentPage.value - 1) * perPageValue
|
|
319
|
+
const end = start + perPageValue
|
|
320
|
+
return props.data.slice(start, end)
|
|
317
321
|
})
|
|
318
322
|
|
|
319
323
|
const useVirtualScroll = computed<boolean>(() => props.virtualScroll && props.data.length > (props.virtualScrollThreshold ?? 100))
|
|
@@ -322,149 +326,149 @@ const virtualScrollHeight = computed<number>(() => props.virtualScrollHeight ??
|
|
|
322
326
|
const scrollbarWidth = ref<number>(0)
|
|
323
327
|
|
|
324
328
|
function getScrollbarWidth(): number {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
329
|
+
const outer = document.createElement('div')
|
|
330
|
+
outer.style.visibility = 'hidden'
|
|
331
|
+
outer.style.overflow = 'scroll'
|
|
332
|
+
document.body.appendChild(outer)
|
|
333
|
+
|
|
334
|
+
const inner = document.createElement('div')
|
|
335
|
+
outer.appendChild(inner)
|
|
336
|
+
|
|
337
|
+
const width = outer.offsetWidth - inner.offsetWidth
|
|
338
|
+
document.body.removeChild(outer)
|
|
339
|
+
|
|
340
|
+
return width
|
|
337
341
|
}
|
|
338
342
|
|
|
339
343
|
watch(useVirtualScroll, (isVirtual) => {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}, {
|
|
344
|
+
if (isVirtual) {
|
|
345
|
+
scrollbarWidth.value = getScrollbarWidth()
|
|
346
|
+
}
|
|
347
|
+
}, {immediate: true})
|
|
344
348
|
|
|
345
349
|
const allColumnWidths = computed(() => {
|
|
346
|
-
|
|
350
|
+
const widths = []
|
|
347
351
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
if (props.showActions) {
|
|
362
|
-
widths.push('auto')
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
if (useSlots()['row-menu']) {
|
|
366
|
-
widths.push('auto')
|
|
352
|
+
if (props.showRowSelection) {
|
|
353
|
+
widths.push('60px')
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
for (let i = 0; i < props.columns.length; i++) {
|
|
357
|
+
const col = props.columns[i]
|
|
358
|
+
if (col.width) {
|
|
359
|
+
widths.push(typeof col.width === 'number' ? `${col.width}px` : col.width)
|
|
360
|
+
} else {
|
|
361
|
+
widths.push('1fr')
|
|
367
362
|
}
|
|
368
|
-
|
|
369
|
-
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (props.showActions) {
|
|
366
|
+
widths.push('auto')
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (useSlots()['row-menu']) {
|
|
370
|
+
widths.push('auto')
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return widths as string[]
|
|
370
374
|
})
|
|
371
375
|
|
|
372
376
|
const virtualGridColumns = computed<string>(() => allColumnWidths.value.join(' '))
|
|
373
377
|
|
|
374
378
|
const actionsColumnIndex = computed<number>(() => {
|
|
375
|
-
|
|
376
|
-
|
|
379
|
+
const total = allColumnWidths.value.length
|
|
380
|
+
return useSlots()['row-menu'] ? total - 2 : total - 1
|
|
377
381
|
})
|
|
378
382
|
|
|
379
383
|
const menuColumnIndex = computed<number>(() => allColumnWidths.value.length - 1)
|
|
380
384
|
|
|
381
385
|
function getColumnIndex(colIndex: number): number {
|
|
382
|
-
|
|
386
|
+
return props.showRowSelection ? colIndex + 1 : colIndex
|
|
383
387
|
}
|
|
384
388
|
|
|
385
389
|
const headerStyle = computed(() => {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
390
|
+
if (!useVirtualScroll.value) return {}
|
|
391
|
+
return {
|
|
392
|
+
display: 'grid' as const,
|
|
393
|
+
gridTemplateColumns: virtualGridColumns.value,
|
|
394
|
+
paddingRight: `${scrollbarWidth.value}px`
|
|
395
|
+
}
|
|
392
396
|
})
|
|
393
397
|
|
|
394
398
|
function getColumnStyle(index: number): Record<string, string | undefined> {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
399
|
+
if (!useVirtualScroll.value) return {}
|
|
400
|
+
|
|
401
|
+
const width = allColumnWidths.value[index]
|
|
402
|
+
if (!width) return {}
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
width: width === 'auto' || width.includes('fr') ? width : width,
|
|
406
|
+
minWidth: width === 'auto' || width.includes('fr') ? undefined : width,
|
|
407
|
+
maxWidth: width === 'auto' || width.includes('fr') ? undefined : width,
|
|
408
|
+
}
|
|
405
409
|
}
|
|
406
410
|
|
|
407
411
|
function getAlignStyle(col: Column): { textAlign: 'left' | 'center' | 'right'; justifyContent: string } {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
412
|
+
const align = col.align || 'left'
|
|
413
|
+
return {
|
|
414
|
+
textAlign: align,
|
|
415
|
+
justifyContent: align === 'right' ? 'flex-end' : align === 'center' ? 'center' : 'flex-start'
|
|
416
|
+
}
|
|
413
417
|
}
|
|
414
418
|
|
|
415
419
|
const handleScroll = (event: Event) => {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
420
|
+
const target = event.target as HTMLElement
|
|
421
|
+
const {scrollTop, scrollHeight, clientHeight} = target
|
|
422
|
+
|
|
423
|
+
if (scrollTop + clientHeight >= scrollHeight - 100) {
|
|
424
|
+
emit('scroll-end')
|
|
425
|
+
}
|
|
422
426
|
}
|
|
423
427
|
|
|
424
428
|
/* ---------- ВЫБОР СТРОК (чек‑боксы) ---------- */
|
|
425
429
|
const selectedIds = ref<Set<unknown>>(new Set())
|
|
426
430
|
|
|
427
431
|
function isRowSelected(row: Row): boolean {
|
|
428
|
-
|
|
432
|
+
return selectedIds.value.has(rowId(row))
|
|
429
433
|
}
|
|
430
434
|
|
|
431
435
|
function toggleRow(row: Row) {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
+
const id = rowId(row)
|
|
437
|
+
if (selectedIds.value.has(id)) selectedIds.value.delete(id)
|
|
438
|
+
else selectedIds.value.add(id)
|
|
439
|
+
emitSelected()
|
|
436
440
|
}
|
|
437
441
|
|
|
438
442
|
/* computed‑getter/setter «Выбрать всё» */
|
|
439
443
|
const isAllSelected = computed<boolean>({
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
444
|
+
get: () => {
|
|
445
|
+
if (!props.showRowSelection) return false
|
|
446
|
+
return props.data.length > 0 && props.data.every((r: Row) => selectedIds.value.has(rowId(r)))
|
|
447
|
+
},
|
|
448
|
+
set: (val: boolean) => {
|
|
449
|
+
if (!props.showRowSelection) return
|
|
450
|
+
if (val) props.data.forEach((r: Row) => selectedIds.value.add(rowId(r)))
|
|
451
|
+
else selectedIds.value.clear()
|
|
452
|
+
emitSelected()
|
|
453
|
+
},
|
|
450
454
|
})
|
|
451
455
|
|
|
452
456
|
/** Эмитим массив выбранных строк */
|
|
453
457
|
function emitSelected() {
|
|
454
|
-
|
|
455
|
-
|
|
458
|
+
const rows = props.data.filter((r: Row) => selectedIds.value.has(rowId(r)))
|
|
459
|
+
emit('update:selected', rows)
|
|
456
460
|
}
|
|
457
461
|
|
|
458
462
|
/* Синхронзация при изменении props.data */
|
|
459
463
|
watch(
|
|
460
464
|
() => props.data,
|
|
461
465
|
() => {
|
|
462
|
-
|
|
463
|
-
|
|
466
|
+
const newIds = new Set(props.data.map((row: Row) => rowId(row)))
|
|
467
|
+
selectedIds.value.forEach(id => {
|
|
464
468
|
if (!newIds.has(id)) selectedIds.value.delete(id)
|
|
465
|
-
|
|
469
|
+
})
|
|
466
470
|
},
|
|
467
|
-
{
|
|
471
|
+
{deep: true}
|
|
468
472
|
)
|
|
469
473
|
|
|
470
474
|
</script>
|
|
@@ -474,204 +478,213 @@ watch(
|
|
|
474
478
|
@import '@/styles/root';
|
|
475
479
|
|
|
476
480
|
.base-table {
|
|
477
|
-
|
|
481
|
+
width: 100%;
|
|
478
482
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
483
|
+
&__wrapper {
|
|
484
|
+
display: flex;
|
|
485
|
+
flex-direction: column;
|
|
486
|
+
border: 1px solid var(--primary-black-200);
|
|
487
|
+
border-radius: var(--corner-radius-m);
|
|
488
|
+
background: var(--bg-light);
|
|
489
|
+
overflow: hidden;
|
|
490
|
+
height: 100%;
|
|
491
|
+
}
|
|
487
492
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
}
|
|
493
|
+
&__scroll-area {
|
|
494
|
+
flex: 1;
|
|
495
|
+
overflow-y: auto;
|
|
496
|
+
}
|
|
493
497
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
498
|
+
&__table {
|
|
499
|
+
width: 100%;
|
|
500
|
+
border-collapse: collapse;
|
|
501
|
+
table-layout: auto;
|
|
502
|
+
}
|
|
499
503
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
font: var(--typography-text-s-medium);
|
|
506
|
-
}
|
|
504
|
+
&__header-column,
|
|
505
|
+
&__cell {
|
|
506
|
+
text-align: left;
|
|
507
|
+
vertical-align: middle;
|
|
508
|
+
}
|
|
507
509
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
510
|
+
&__header {
|
|
511
|
+
width: 100%;
|
|
512
|
+
background: var(--primary-black-50);
|
|
513
|
+
border: 1px solid var(--primary-black-200);
|
|
514
|
+
color: var(--primary-black-600);
|
|
515
|
+
font: var(--typography-text-s-medium);
|
|
516
|
+
}
|
|
511
517
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
518
|
+
&__cell {
|
|
519
|
+
padding: var(--spacing-l) var(--spacing-xl);
|
|
520
|
+
}
|
|
515
521
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
522
|
+
&__cell:nth-child(odd) {
|
|
523
|
+
color: var(--primary-black-600);
|
|
524
|
+
}
|
|
519
525
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
}
|
|
526
|
+
&__cell:nth-child(even) {
|
|
527
|
+
color: var(--primary-text-primary);
|
|
528
|
+
}
|
|
524
529
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
width: 100%;
|
|
530
|
-
}
|
|
530
|
+
&__header-column {
|
|
531
|
+
padding: var(--spacing-m) var(--spacing-xl);
|
|
532
|
+
user-select: none;
|
|
533
|
+
}
|
|
531
534
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
535
|
+
&__empty {
|
|
536
|
+
text-align: center;
|
|
537
|
+
font: var(--typography-text-xl-medium);
|
|
538
|
+
padding: var(--spacing-m);
|
|
539
|
+
width: 100%;
|
|
540
|
+
}
|
|
538
541
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
542
|
+
.column-header {
|
|
543
|
+
display: flex;
|
|
544
|
+
align-items: center;
|
|
545
|
+
gap: 4px;
|
|
546
|
+
cursor: pointer;
|
|
547
|
+
}
|
|
543
548
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
549
|
+
&__sort-icon {
|
|
550
|
+
color: var(--primary-black-600);
|
|
551
|
+
transition: transform 0.2s;
|
|
552
|
+
}
|
|
547
553
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
text-align: right;
|
|
552
|
-
}
|
|
554
|
+
&__sort-icon.--active {
|
|
555
|
+
transform: rotate(180deg);
|
|
556
|
+
}
|
|
553
557
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
+
&__actions-column,
|
|
559
|
+
&__menu-column {
|
|
560
|
+
width: auto;
|
|
561
|
+
text-align: right;
|
|
562
|
+
}
|
|
558
563
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
background: var(--bg-light);
|
|
564
|
-
}
|
|
564
|
+
&__actions-column {
|
|
565
|
+
display: flex;
|
|
566
|
+
align-items: center;
|
|
567
|
+
}
|
|
565
568
|
|
|
566
|
-
|
|
567
|
-
|
|
569
|
+
&__footer {
|
|
570
|
+
padding: 20px;
|
|
571
|
+
display: flex;
|
|
572
|
+
justify-content: center;
|
|
573
|
+
background: var(--bg-light);
|
|
574
|
+
flex-shrink: 0;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
&__row {
|
|
578
|
+
border-bottom: 1px solid var(--primary-black-100);
|
|
568
579
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
580
|
+
&:hover {
|
|
581
|
+
background: var(--primary-black-100);
|
|
582
|
+
}
|
|
572
583
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
}
|
|
584
|
+
&.--is-disabled {
|
|
585
|
+
cursor: not-allowed;
|
|
586
|
+
background: var(--primary-black-50);
|
|
577
587
|
}
|
|
588
|
+
}
|
|
578
589
|
}
|
|
579
590
|
|
|
580
591
|
.row-enter-active,
|
|
581
592
|
.row-leave-active {
|
|
582
|
-
|
|
593
|
+
transition: opacity 0.3s ease, transform 0.3s ease;
|
|
583
594
|
}
|
|
584
595
|
|
|
585
596
|
.row-enter-from {
|
|
586
|
-
|
|
587
|
-
|
|
597
|
+
opacity: 0;
|
|
598
|
+
transform: translateY(12px);
|
|
588
599
|
}
|
|
600
|
+
|
|
589
601
|
.row-enter-to {
|
|
590
|
-
|
|
591
|
-
|
|
602
|
+
opacity: 1;
|
|
603
|
+
transform: translateY(0);
|
|
592
604
|
}
|
|
593
605
|
|
|
594
606
|
.row-leave-from {
|
|
595
|
-
|
|
596
|
-
|
|
607
|
+
opacity: 1;
|
|
608
|
+
transform: translateY(0);
|
|
597
609
|
}
|
|
610
|
+
|
|
598
611
|
.row-leave-to {
|
|
599
|
-
|
|
600
|
-
|
|
612
|
+
opacity: 0;
|
|
613
|
+
transform: translateY(-12px);
|
|
601
614
|
}
|
|
602
615
|
|
|
603
616
|
.row-leave-active {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
617
|
+
position: absolute;
|
|
618
|
+
width: 100%;
|
|
619
|
+
left: 0;
|
|
620
|
+
top: -250px;
|
|
621
|
+
opacity: 0;
|
|
609
622
|
}
|
|
610
623
|
|
|
611
624
|
.base-table__tbody {
|
|
612
|
-
|
|
625
|
+
position: relative;
|
|
613
626
|
}
|
|
614
627
|
|
|
615
628
|
.base-table__virtual-tbody {
|
|
616
|
-
|
|
629
|
+
width: 100%;
|
|
617
630
|
}
|
|
618
631
|
|
|
619
632
|
.base-table__virtual-cell {
|
|
620
|
-
|
|
621
|
-
|
|
633
|
+
padding: 0;
|
|
634
|
+
border: none;
|
|
622
635
|
}
|
|
623
636
|
|
|
624
637
|
.base-table__scroller {
|
|
625
|
-
|
|
638
|
+
width: 100%;
|
|
626
639
|
}
|
|
627
640
|
|
|
628
641
|
.base-table__virtual-row {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
642
|
+
display: grid;
|
|
643
|
+
align-items: center;
|
|
644
|
+
width: 100%;
|
|
645
|
+
min-height: 60px;
|
|
646
|
+
border-bottom: 1px solid var(--primary-black-100);
|
|
647
|
+
padding: var(--spacing-l) 0;
|
|
648
|
+
gap: 0;
|
|
636
649
|
}
|
|
637
650
|
|
|
638
651
|
.base-table__virtual-row:hover {
|
|
639
|
-
|
|
652
|
+
background: var(--primary-black-100);
|
|
640
653
|
}
|
|
641
654
|
|
|
642
655
|
.base-table__virtual-row.--is-disabled {
|
|
643
|
-
|
|
644
|
-
|
|
656
|
+
cursor: not-allowed;
|
|
657
|
+
background: var(--primary-black-50);
|
|
645
658
|
}
|
|
646
659
|
|
|
647
660
|
.base-table__virtual-cell-checkbox {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
661
|
+
display: flex;
|
|
662
|
+
align-items: center;
|
|
663
|
+
padding: 0 var(--spacing-xl);
|
|
651
664
|
}
|
|
652
665
|
|
|
653
666
|
.base-table__virtual-cell-content {
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
667
|
+
display: flex;
|
|
668
|
+
align-items: center;
|
|
669
|
+
text-align: left;
|
|
670
|
+
white-space: nowrap;
|
|
671
|
+
padding: 0 var(--spacing-xl);
|
|
659
672
|
|
|
660
|
-
|
|
673
|
+
@include text-clamp(1);
|
|
661
674
|
}
|
|
662
675
|
|
|
663
676
|
.base-table__virtual-cell-actions {
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
677
|
+
display: flex;
|
|
678
|
+
align-items: center;
|
|
679
|
+
gap: var(--spacing-xs);
|
|
680
|
+
justify-content: flex-end;
|
|
681
|
+
padding: 0 var(--spacing-xl);
|
|
669
682
|
}
|
|
670
683
|
|
|
671
684
|
.base-table__virtual-cell-menu {
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
685
|
+
display: flex;
|
|
686
|
+
align-items: center;
|
|
687
|
+
justify-content: center;
|
|
688
|
+
padding: 0 var(--spacing-xl);
|
|
676
689
|
}
|
|
677
690
|
</style>
|