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.
@@ -1,279 +1,283 @@
1
1
  <template>
2
- <div class="base-table">
3
- <div class="base-table__wrapper">
4
- <table class="base-table__table">
5
- <thead v-if="hasHeader">
6
- <slot name="header">
7
- <tr class="base-table__header" :style="headerStyle">
8
- <!-- Чек‑бокс «Выбрать всё» -->
9
- <th v-if="showRowSelection" class="base-table__header-column" :style="getColumnStyle(0)">
10
- <span v-if="checkboxTitle">{{ checkboxTitle }}</span>
11
- <BaseCheckbox id="selectAll" size="small" v-model="isAllSelected" />
12
- </th>
13
-
14
- <!-- Обычные колонки -->
15
- <th v-for="(col, index) in columns"
16
- :key="String(col.key)"
17
- class="base-table__header-column"
18
- @click="onHeaderClick(col)"
19
- :title="col.tooltip"
20
- :style="{ ...getColumnStyle(getColumnIndex(Number(index))), ...getAlignStyle(col) }"
21
- >
22
- <div class="column-header" :style="{ justifyContent: getAlignStyle(col).justifyContent }">
23
- <component
24
- v-if="col.icons?.[0]"
25
- :is="col.icons[0]"
26
- class="header-icon"
27
- />
28
- <span v-if="col.label">{{ col.label }}</span>
29
- <component
30
- v-if="col.icons?.[1]"
31
- :is="col.icons[1]"
32
- class="header-icon"
33
- />
34
- <BaseIcon
35
- v-if="col.sortable"
36
- name="arrow-up"
37
- size="extra-small"
38
- class="base-table__sort-icon"
39
- :class="{ '--active': sortOrder === 'desc' && sortKey === col.key }"
40
- />
41
- </div>
42
- </th>
43
-
44
- <!-- Колонка‑действий -->
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>
49
- </tr>
50
- </slot>
51
- </thead>
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"
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
- <template v-if="paginatedData.length">
129
- <tr
130
- v-for="(row, rowIndex) in paginatedData"
131
- :key="String(rowId(row))"
132
- :class="{ '--is-disabled': Boolean(row.disabled) }"
133
- class="base-table__row"
134
- >
135
-
136
- <!-- Чек‑бокс отдельной строки -->
137
- <td v-if="showRowSelection" class="base-table__cell">
138
- <BaseCheckbox
139
- :id="String(row.id || rowIndex)"
140
- size="small"
141
- :modelValue="isRowSelected(row)"
142
- :disabled="Boolean(row.disabled)"
143
- @update:modelValue="toggleRow(row)"
144
- />
145
- </td>
146
-
147
- <!-- Ячейки -->
148
- <td v-for="col in columns"
149
- :key="String(col.key)"
150
- class="base-table__cell"
151
- :style="getAlignStyle(col)">
152
- <!-- Пользовательский рендеринг ячейки -->
153
- <slot
154
- :name="`cell-${String(col.key)}`"
155
- :row="row"
156
- :value="row[col.key]"
157
- >
158
- {{ row[col.key] }}
159
- </slot>
160
- </td>
161
-
162
- <!-- Колонка‑действий -->
163
- <td v-if="showActions" class="base-table__cell base-table__actions-column">
164
- <slot name="actions" :row="row">
165
- <BaseButton
166
- v-for="action in actions"
167
- :key="action.key"
168
- :disabled="Boolean(row.disabled)"
169
- size="extra-small"
170
- color="quaternary-gray"
171
- only-icon
172
- @click="action.handler(row)"
173
- >
174
- <BaseIcon :name="typeof action.icon === 'string' ? action.icon : 'help'" size="custom" />
175
- </BaseButton>
176
- </slot>
177
- </td>
178
-
179
- <td v-if="$slots['row-menu']" class="base-table__cell base-table__menu-column">
180
- <slot name="row-menu" :row="row"></slot>
181
- </td>
182
- </tr>
183
- </template>
184
-
185
- <!-- Пустой набор данных -->
186
- <tr v-else>
187
- <td :colspan="colspan" class="base-table__empty">
188
- <slot name="empty">
189
- <div>
190
- Нет данных
191
- </div>
192
- </slot>
193
- </td>
194
- </tr>
195
- </transition-group>
196
-
197
- </table>
198
-
199
- <div v-if="showPagination" class="base-table__footer">
200
- <BasePagination
201
- :totalPages="totalPages"
202
- :currentPage="currentPage"
203
- size="small"
204
- v-bind="pagination"
205
- @update:currentPage="onPageChange"
206
- />
207
- </div>
208
- </div>
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 { ref, computed, watch, defineProps, defineEmits, useSlots } from 'vue'
217
+ import {ref, computed, watch, defineProps, defineEmits, useSlots} from 'vue'
214
218
  import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
215
- import { RecycleScroller } from 'vue-virtual-scroller'
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
- Row,
222
- Column,
223
- BaseTableProps,
225
+ Row,
226
+ Column,
227
+ BaseTableProps,
224
228
  } from '../../types/table'
225
- import type { TPaginationProps } from '../../types/pagination'
229
+ import type {TPaginationProps} from '../../types/pagination'
226
230
 
227
231
  const props = defineProps<BaseTableProps>()
228
232
  const emit = defineEmits<{
229
- (e: 'update:selected', rows: Row[]): void
230
- (e: 'select-all', selected: boolean): void
231
- (e: 'sort-change', payload: { key: string; order: 'asc' | 'desc' }): void
232
- (e: 'page-change', page: number): void
233
- (e: 'scroll-end'): void
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
- const base = props.columns.length
238
- const extra =
239
- (props.showRowSelection ? 1 : 0) +
240
- (props.showActions ? 1 : 0) +
241
- (useSlots()['row-menu'] ? 1 : 0)
242
- return base + extra
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
- if (props.rowKey) {
250
- return typeof props.rowKey === 'function'
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
- return (row as any).__uid ?? JSON.stringify(row)
259
+ return (row as any).__uid ?? JSON.stringify(row)
256
260
  }
257
261
 
258
262
  /* ---------- СОРТИРОВКА ---------- */
259
- const sortKey = ref<string | null>(null)
263
+ const sortKey = ref<string | null>(null)
260
264
  const sortOrder = ref<'asc' | 'desc'>('asc')
261
265
 
262
266
  function onHeaderClick(col: Column) {
263
- if (!col.sortable) return
267
+ if (!col.sortable) return
264
268
 
265
- if (sortKey.value === col.key) {
266
- sortOrder.value = sortOrder.value === 'desc' ? 'asc' : 'desc'
267
- } else {
268
- sortKey.value = col.key
269
- sortOrder.value = 'desc'
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
- emit('sort-change', { key: col.key, order: sortOrder.value })
276
+ emit('sort-change', {key: col.key, order: sortOrder.value})
273
277
  }
274
278
 
275
279
  /* ---------- ПАГИНАЦИЯ ---------- */
276
- const pagination = computed<TPaginationProps>(() => props.pagination ?? { perPage: 10 })
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
- currentPage.value = page
301
- emit('page-change', page)
304
+ currentPage.value = page
305
+ emit('page-change', page)
302
306
  }
303
307
 
304
308
  const paginatedData = computed(() => {
305
- if (!props.pagination) {
306
- return props.data
307
- }
309
+ if (!props.pagination) {
310
+ return props.data
311
+ }
308
312
 
309
- if (typeof props.pagination.totalItems === 'number') {
310
- return props.data
311
- }
312
-
313
- const perPageValue = Number(perPage.value) || 10
314
- const start = (currentPage.value - 1) * perPageValue
315
- const end = start + perPageValue
316
- return props.data.slice(start, end)
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
- const outer = document.createElement('div')
326
- outer.style.visibility = 'hidden'
327
- outer.style.overflow = 'scroll'
328
- document.body.appendChild(outer)
329
-
330
- const inner = document.createElement('div')
331
- outer.appendChild(inner)
332
-
333
- const width = outer.offsetWidth - inner.offsetWidth
334
- document.body.removeChild(outer)
335
-
336
- return width
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
- if (isVirtual) {
341
- scrollbarWidth.value = getScrollbarWidth()
342
- }
343
- }, { immediate: true })
344
+ if (isVirtual) {
345
+ scrollbarWidth.value = getScrollbarWidth()
346
+ }
347
+ }, {immediate: true})
344
348
 
345
349
  const allColumnWidths = computed(() => {
346
- const widths = []
350
+ const widths = []
347
351
 
348
- if (props.showRowSelection) {
349
- widths.push('60px')
350
- }
351
-
352
- for (let i = 0; i < props.columns.length; i++) {
353
- const col = props.columns[i]
354
- if (col.width) {
355
- widths.push(typeof col.width === 'number' ? `${col.width}px` : col.width)
356
- } else {
357
- widths.push('1fr')
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
- return widths as string[]
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
- const total = allColumnWidths.value.length
376
- return useSlots()['row-menu'] ? total - 2 : total - 1
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
- return props.showRowSelection ? colIndex + 1 : colIndex
386
+ return props.showRowSelection ? colIndex + 1 : colIndex
383
387
  }
384
388
 
385
389
  const headerStyle = computed(() => {
386
- if (!useVirtualScroll.value) return {}
387
- return {
388
- display: 'grid' as const,
389
- gridTemplateColumns: virtualGridColumns.value,
390
- paddingRight: `${scrollbarWidth.value}px`
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
- if (!useVirtualScroll.value) return {}
396
-
397
- const width = allColumnWidths.value[index]
398
- if (!width) return {}
399
-
400
- return {
401
- width: width === 'auto' || width.includes('fr') ? width : width,
402
- minWidth: width === 'auto' || width.includes('fr') ? undefined : width,
403
- maxWidth: width === 'auto' || width.includes('fr') ? undefined : width,
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
- const align = col.align || 'left'
409
- return {
410
- textAlign: align,
411
- justifyContent: align === 'right' ? 'flex-end' : align === 'center' ? 'center' : 'flex-start'
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
- const target = event.target as HTMLElement
417
- const { scrollTop, scrollHeight, clientHeight } = target
418
-
419
- if (scrollTop + clientHeight >= scrollHeight - 100) {
420
- emit('scroll-end')
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
- return selectedIds.value.has(rowId(row))
432
+ return selectedIds.value.has(rowId(row))
429
433
  }
430
434
 
431
435
  function toggleRow(row: Row) {
432
- const id = rowId(row)
433
- if (selectedIds.value.has(id)) selectedIds.value.delete(id)
434
- else selectedIds.value.add(id)
435
- emitSelected()
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
- get: () => {
441
- if (!props.showRowSelection) return false
442
- return props.data.length > 0 && props.data.every((r: Row) => selectedIds.value.has(rowId(r)))
443
- },
444
- set: (val: boolean) => {
445
- if (!props.showRowSelection) return
446
- if (val) props.data.forEach((r: Row) => selectedIds.value.add(rowId(r)))
447
- else selectedIds.value.clear()
448
- emitSelected()
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
- const rows = props.data.filter((r: Row) => selectedIds.value.has(rowId(r)))
455
- emit('update:selected', rows)
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
- const newIds = new Set(props.data.map((row: Row) => rowId(row)))
463
- selectedIds.value.forEach(id => {
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
- { deep: true }
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
- width: 100%;
481
+ width: 100%;
478
482
 
479
- &__wrapper {
480
- display: flex;
481
- flex-direction: column;
482
- border: 1px solid var(--primary-black-200);
483
- border-radius: var(--corner-radius-m);
484
- background: var(--bg-light);
485
- overflow: hidden;
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
- &__table {
489
- width: 100%;
490
- border-collapse: collapse;
491
- table-layout: auto;
492
- }
493
+ &__scroll-area {
494
+ flex: 1;
495
+ overflow-y: auto;
496
+ }
493
497
 
494
- &__header-column,
495
- &__cell {
496
- text-align: left;
497
- vertical-align: middle;
498
- }
498
+ &__table {
499
+ width: 100%;
500
+ border-collapse: collapse;
501
+ table-layout: auto;
502
+ }
499
503
 
500
- &__header {
501
- width: 100%;
502
- background: var(--primary-black-50);
503
- border: 1px solid var(--primary-black-200);
504
- color: var(--primary-black-600);
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
- &__cell {
509
- padding: var(--spacing-l) var(--spacing-xl);
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
- &__cell:nth-child(odd) {
513
- color: var(--primary-black-600);
514
- }
518
+ &__cell {
519
+ padding: var(--spacing-l) var(--spacing-xl);
520
+ }
515
521
 
516
- &__cell:nth-child(even) {
517
- color: var(--primary-text-primary);
518
- }
522
+ &__cell:nth-child(odd) {
523
+ color: var(--primary-black-600);
524
+ }
519
525
 
520
- &__header-column {
521
- padding: var(--spacing-m) var(--spacing-xl);
522
- user-select: none;
523
- }
526
+ &__cell:nth-child(even) {
527
+ color: var(--primary-text-primary);
528
+ }
524
529
 
525
- &__empty {
526
- text-align: center;
527
- font: var(--typography-text-xl-medium);
528
- padding: var(--spacing-m);
529
- width: 100%;
530
- }
530
+ &__header-column {
531
+ padding: var(--spacing-m) var(--spacing-xl);
532
+ user-select: none;
533
+ }
531
534
 
532
- .column-header {
533
- display: flex;
534
- align-items: center;
535
- gap: 4px;
536
- cursor: pointer;
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
- &__sort-icon {
540
- color: var(--primary-black-600);
541
- transition: transform 0.2s;
542
- }
542
+ .column-header {
543
+ display: flex;
544
+ align-items: center;
545
+ gap: 4px;
546
+ cursor: pointer;
547
+ }
543
548
 
544
- &__sort-icon.--active {
545
- transform: rotate(180deg);
546
- }
549
+ &__sort-icon {
550
+ color: var(--primary-black-600);
551
+ transition: transform 0.2s;
552
+ }
547
553
 
548
- &__actions-column,
549
- &__menu-column {
550
- width: auto;
551
- text-align: right;
552
- }
554
+ &__sort-icon.--active {
555
+ transform: rotate(180deg);
556
+ }
553
557
 
554
- &__actions-column {
555
- display: flex;
556
- align-items: center;
557
- }
558
+ &__actions-column,
559
+ &__menu-column {
560
+ width: auto;
561
+ text-align: right;
562
+ }
558
563
 
559
- &__footer {
560
- padding: 20px;
561
- display: flex;
562
- justify-content: center;
563
- background: var(--bg-light);
564
- }
564
+ &__actions-column {
565
+ display: flex;
566
+ align-items: center;
567
+ }
565
568
 
566
- &__row {
567
- border-bottom: 1px solid var(--primary-black-100);
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
- &:hover {
570
- background: var(--primary-black-100);
571
- }
580
+ &:hover {
581
+ background: var(--primary-black-100);
582
+ }
572
583
 
573
- &.--is-disabled {
574
- cursor: not-allowed;
575
- background: var(--primary-black-50);
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
- transition: opacity 0.3s ease, transform 0.3s ease;
593
+ transition: opacity 0.3s ease, transform 0.3s ease;
583
594
  }
584
595
 
585
596
  .row-enter-from {
586
- opacity: 0;
587
- transform: translateY(12px);
597
+ opacity: 0;
598
+ transform: translateY(12px);
588
599
  }
600
+
589
601
  .row-enter-to {
590
- opacity: 1;
591
- transform: translateY(0);
602
+ opacity: 1;
603
+ transform: translateY(0);
592
604
  }
593
605
 
594
606
  .row-leave-from {
595
- opacity: 1;
596
- transform: translateY(0);
607
+ opacity: 1;
608
+ transform: translateY(0);
597
609
  }
610
+
598
611
  .row-leave-to {
599
- opacity: 0;
600
- transform: translateY(-12px);
612
+ opacity: 0;
613
+ transform: translateY(-12px);
601
614
  }
602
615
 
603
616
  .row-leave-active {
604
- position: absolute;
605
- width: 100%;
606
- left: 0;
607
- top: -250px;
608
- opacity: 0;
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
- position: relative;
625
+ position: relative;
613
626
  }
614
627
 
615
628
  .base-table__virtual-tbody {
616
- width: 100%;
629
+ width: 100%;
617
630
  }
618
631
 
619
632
  .base-table__virtual-cell {
620
- padding: 0;
621
- border: none;
633
+ padding: 0;
634
+ border: none;
622
635
  }
623
636
 
624
637
  .base-table__scroller {
625
- width: 100%;
638
+ width: 100%;
626
639
  }
627
640
 
628
641
  .base-table__virtual-row {
629
- display: grid;
630
- align-items: center;
631
- width: 100%;
632
- min-height: 60px;
633
- border-bottom: 1px solid var(--primary-black-100);
634
- padding: var(--spacing-l) 0;
635
- gap: 0;
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
- background: var(--primary-black-100);
652
+ background: var(--primary-black-100);
640
653
  }
641
654
 
642
655
  .base-table__virtual-row.--is-disabled {
643
- cursor: not-allowed;
644
- background: var(--primary-black-50);
656
+ cursor: not-allowed;
657
+ background: var(--primary-black-50);
645
658
  }
646
659
 
647
660
  .base-table__virtual-cell-checkbox {
648
- display: flex;
649
- align-items: center;
650
- padding: 0 var(--spacing-xl);
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
- display: flex;
655
- align-items: center;
656
- text-align: left;
657
- white-space: nowrap;
658
- padding: 0 var(--spacing-xl);
667
+ display: flex;
668
+ align-items: center;
669
+ text-align: left;
670
+ white-space: nowrap;
671
+ padding: 0 var(--spacing-xl);
659
672
 
660
- @include text-clamp(1);
673
+ @include text-clamp(1);
661
674
  }
662
675
 
663
676
  .base-table__virtual-cell-actions {
664
- display: flex;
665
- align-items: center;
666
- gap: var(--spacing-xs);
667
- justify-content: flex-end;
668
- padding: 0 var(--spacing-xl);
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
- display: flex;
673
- align-items: center;
674
- justify-content: center;
675
- padding: 0 var(--spacing-xl);
685
+ display: flex;
686
+ align-items: center;
687
+ justify-content: center;
688
+ padding: 0 var(--spacing-xl);
676
689
  }
677
690
  </style>