/
seafteam
/
seaf-archtool-core
Обзор
Документация
Войти
/
seafteam
/
seaf-archtool-core
Код
Запросы
11
Задачи
Пакеты
2
Релизы
21
Аналитика
java-back
plugins/editable-table/components/Table/Table.vue
509 строк
14 KB
Alexandr Anenburg
Запрос на слияние 'bugfix/ERA-2762-editable-table-ai-button-color-fix' (
#632
) из bugfix/ERA-2762-editable-table-ai-button-color-fix в dev
26 июн 2026, 15:40
Верифицирован
26 июн 2026, 15:40
e3c5ca9
Код
Авторство
О чём код?
<!-- Copyright (C) 2023 Sber Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Maintainers: Alexandr Anenburg <anenburg.alexandr@mail.ru>, Sber Contributors: Alexandr Anenburg <anenburg.alexandr@mail.ru>, Sber - 2025 --> <template> <div class="table-wrapper"> <action-bar class="action-bar" v-bind:table-options="tableOptions" v-bind:selected-rows="selectedRows" v-bind:is-filter-active="isFilterActive" v-on:saveTable="$emit('saveTable')" v-on:exportToExcel="$emit('exportToExcel', getExcelExportData())" v-on:addRow="startNewRowCreating" v-on:removeRow="removeRow" v-on:fillSelected="$emit('fillSelected')" v-on:resetFilters="onResetFilters" v-on:reload="$emit('reload')"> <v-pagination v-if="filteredAndSortedItems.length > tableOptions.pageSize" class="pagination" v-bind:model-value="currentPage > numberOfPages ? numberOfPages : currentPage" v-bind:length="numberOfPages" v-bind:total-visible="4" v-on:update:modelValue="onChangePage" /> </action-bar> <scroll-container> <div ref="table" class="table-body" v-bind:style="headerStyle"> <template v-if="true"> <checkbox v-if="tableOptions.selection" key="header-select" class="cell cell_header" v-bind:checked="selectedRows.length === filteredAndSortedItems.length" v-on:change="onSelectAllRows" /> <template v-for="header in headers"> <cell-header v-if="header.display" v-bind:ref="resizeObserve" v-bind:key="`header:${header.headerID}`" class="cell cell_header" v-bind:header="header" v-bind:sort-map="sortMap" v-bind:resizable="tableOptions.direction === 'ltr'" v-on:click="onSetSort(header)" v-on:resize-start="onHeaderResize"> <filter-dropdown v-if="header.filterable" v-bind:header="header" v-bind:filter="filters[header.headerID]" v-on:on-save="handleChangeFilter" /> <button v-if="header.agent" class="button" v-on:click.stop="() => fillColumnWithAI(header.headerID)"> <icon class="button-icon" type="creation" /> </button> </cell-header> </template> </template> <template v-for="([rowID, row], rowIndex) in filteredAndSortedItemsSlice"> <checkbox v-if="tableOptions.selection" v-bind:key="`item-select:${rowID}`" v-bind:checked="selectedRows.includes(rowID)" class="cell cell_body" v-on:change="onSelectRowItem(rowID)" /> <template v-for="(header, columnIndex) in headers"> <cell-body v-if="header.display" v-bind:key="`item:${rowID}:${header.headerID}`" v-bind:ref="resizeObserve" v-bind:data-index="getElementIndex(rowIndex, columnIndex)" v-bind:class="`cell cell_body ${header.type}`" v-bind:header="header" v-bind:row="row" v-bind:row-i-d="rowID" v-bind:pull-profile-data="pullProfileData" v-on:keydown="handleTableKeydown" /> </template> </template> </div> </scroll-container> <v-dialog v-model="isDialogOpen" max-width="600" persistent> <v-card> <new-row-card v-bind:table-rows="Object.keys(tableData)" v-on:click-save="createNewRow" v-on:click-cancel="isDialogOpen = false" /> </v-card> </v-dialog> </div> </template> <script> import { SELECT_ROW_WIDTH } from '../../lib/const'; import { checkIsValueEmpty, getFilteredTableData, getMultipleRowSorter } from '../../lib/helpers'; import ResizeObserverMixin from './ResizeObserverMixin.vue'; import TableOptionsCacheMixinVue from './TableOptionsCacheMixin.vue'; import NavigationMixin from './NavigationMixin.vue'; import NewRowMixin from './NewRowMixin.vue'; import ActionBar from '../ActionBar/ActionBar.vue'; import CellHeader from '../TableCell/CellHeader.vue'; import CellBody from '../TableCell/CellBody.vue'; import ScrollContainer from '../ScrollContainer/ScrollContainer.vue'; import Checkbox from '../TableCell/Checkbox/Checkbox.vue'; import FilterDropdown from '../Filter/FilterDropdown.vue'; import Icon from '../Icon/Icon.vue'; export default { components: { ActionBar, CellHeader, CellBody, ScrollContainer, Checkbox, FilterDropdown, Icon }, mixins: [ ResizeObserverMixin, TableOptionsCacheMixinVue, NavigationMixin, NewRowMixin ], props: { tableData: { type: Object, required: true }, headers: { type: Array, required: true }, tableOptions: { type: Object, required: true }, onSelect: { type: Function, required: true }, selectedRows: { type: Array, required: true }, createdRows: { type: Array, required: true }, pullProfileData: { type: Function, required: true } }, emits: ['saveTable', 'exportToExcel', 'fillSelected', 'reload', 'reset-created-rows-list', 'fill-column-with-ai', 'change-sorting', 'remove-row', 'add-row'], data() { return { filters: this.getOptionsCache()?.filters ?? {}, sortList: this.getOptionsCache()?.sortList ?? [], currentPage: this.getOptionsCache()?.page ?? 1, isDialogOpen: false, columnWidths: this.getOptionsCache()?.columnWidths ?? {} }; }, computed: { visibleHeaders() { return this.headers.filter(({display}) => display); }, headerStyle() { const headerWidthList = this.visibleHeaders.reduce((acc, header) => { acc.push(this.columnWidths[header.headerID] || header.width); return acc; }, this.tableOptions.selection ? [SELECT_ROW_WIDTH] : []); const direction = this.tableOptions.direction; const tableHeadersSize = this.visibleHeaders.length + Number(this.tableOptions.selection); const tableRowsSize = Math.min(Object.keys(this.tableData).length, this.filteredAndSortedItemsSlice.length) + Number(this.tableOptions.filtration); const columnsLength = direction === 'ltr' ? tableHeadersSize : tableRowsSize; const rowsLength = direction === 'ltr' ? tableRowsSize : tableHeadersSize; const gridTemplateColumns = direction === 'ltr' ? headerWidthList.join(' ') : `repeat(${columnsLength}, auto)`; const gridAutoFlow = direction === 'ltr' ? 'row' : 'column'; return { 'grid-template-columns': gridTemplateColumns, 'grid-template-rows': `repeat(${rowsLength}, auto)`, 'grid-auto-flow': gridAutoFlow }; }, items() { return Object.entries(this.tableData); }, itemsLength() { return this.items.length; }, filteredItems() { return this.isFilterActive ? getFilteredTableData(this.items, this.filters, this.headers, this.createdRows) : this.items; }, filteredAndSortedItems() { return this.sortList.length > 0 ? [...this.filteredItems].sort(getMultipleRowSorter(this.sortList, this.createdRows)) : this.filteredItems; }, numberOfPages() { return Math.ceil(this.filteredAndSortedItems.length / this.tableOptions.pageSize); }, filteredAndSortedItemsSlice() { const startIndex = (this.currentPage - 1) * this.tableOptions.pageSize; return this.filteredAndSortedItems.slice( startIndex, startIndex + this.tableOptions.pageSize ); }, isFilterActive() { for (let i = 0; i < this.headers.length; i++) { const id = this.headers[i].headerID; const value = this.filters[id]; if (!checkIsValueEmpty(value)) { return true; } } return false; }, sortMap() { const result = {}; this.sortList.forEach((sorter, index) => { result[sorter.value] = { ...sorter, priority: index }; }); return result; }, isSortActive() { return this.sortList.length > 0; } }, watch: { filters: { deep: true, handler() { this.currentPage = 1; this.updateOptionsCache(); this.$emit('reset-created-rows-list'); } }, sortList: { deep: true, handler() { this.updateOptionsCache(); } }, itemsLength() { this.currentPage = this.numberOfPages; setTimeout(() => { const lastIndex = this.$refs?.table?.children?.length - this.headers.length - +Boolean(this.tableOptions.selection); if(lastIndex) this.$refs.table.children[lastIndex].scrollIntoView(); }, 100); } }, methods: { getExcelExportData() { return this.tableOptions.isExcelExportFiltered ? this.filteredAndSortedItems : this.items; }, fillColumnWithAI(headerID) { this.$emit('fill-column-with-ai', headerID); }, handleChangeFilter(newFilter) { this.filters = Object.assign({}, this.filters, newFilter); }, onChangePage(newPage) { this.currentPage = newPage; this.updateOptionsCache(); }, onResetFilters() { this.filters = {}; }, onSelectRowItem(rowID) { const newValue = this.selectedRows.includes(rowID) ? this.selectedRows.filter((selected) => selected !== rowID) : [...this.selectedRows, rowID]; this.onSelect(newValue); }, onSelectAllRows() { const newValue = this.selectedRows.length === this.filteredAndSortedItems.length ? [] : this.filteredAndSortedItems.map(([rowID]) => rowID); this.onSelect(newValue); }, onSetSort(header) { this.$emit('change-sorting'); const { headerID, type } = header; let options = null; if (header?.options) { options = (Array.isArray(header.options) ? header.options : header.options[header.optionID] || [] ).reduce((acc, option) => Object.assign(acc, {[option.value] : option.text || option.value}), {}); } if (!this.sortMap[headerID]) { this.sortList = [ ...this.sortList, { value: headerID, direction: 'inc', type, options } ]; return; } if (this.sortMap[headerID].direction === 'inc') { const index = this.sortMap[headerID].priority; this.sortList[index].direction = 'dec'; return; } this.sortList = this.sortList.filter(({ value }) => value !== headerID); }, onHeaderResize({ event, headerID }) { const MIN_WIDTH = 40; const startX = event.clientX; const columnEl = event.target.closest('.cell_header'); if (!columnEl) return; const startWidth = columnEl.offsetWidth; const onMouseMove = (moveEvent) => { const delta = moveEvent.clientX - startX; const newWidth = Math.max(MIN_WIDTH, startWidth + delta); this.columnWidths[headerID] = `${newWidth}px`; this.updateOptionsCache(); }; const onMouseUp = () => { window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('mouseup', onMouseUp); }; window.addEventListener('mousemove', onMouseMove); window.addEventListener('mouseup', onMouseUp); } } }; </script> <style scoped> .table-wrapper { display: flex; flex-direction: column; } .action-bar { position: sticky; top: 2px; left: 0; z-index: 8; background-color: rgba(255, 255, 255, 0.85); border-radius: 4px; box-shadow: 1px 1px 1px 1px rgba(34, 60, 80, 0.3); margin: 2px 0 2px 4px; align-self: flex-start; } .table-body { position: relative; padding: 2px; display: grid; width: fit-content; background-color: var(--color-border); gap: 2px; } .cell { position: relative; min-height: 30px; outline: 2px solid transparent; } .cell > :first-child { padding: 4px; height: 100%; } .cell.cell_body { background-color: var(--color-bg-cell); } .cell.cell_header { background-color: var(--color-bg-header); padding: 4px 6px; } .cell.cell_pinned { position: sticky; left: 0; top: 0; z-index: 6; outline-color: var(--color-border); } .cell.cell_editable:hover:not(:focus-within):not(.cell_header) { outline-color: var(--color-focus); outline-style: dashed; } .cell.cell_editable:focus-within { outline-color: var(--color-focus); } .cell:not(.cell_editable):focus-within { outline-color: var(--color-gray); } .pagination { padding-left: 32px; color: var(--color-focus); position: relative; } .pagination::before { content: ""; height: 100%; border-left: 1px dashed var(--color-focus); opacity: 0.75; position: absolute; left: 16px; top: 0; } .button { background-color: var(--color-gray); padding: 2px; } .pagination >>> .v-pagination { height: 28px; margin-bottom: 0; } .pagination >>> button { width: auto; height: 24px; font-size: 14px; font-weight: 600; margin-top: 0; margin-bottom: 0; } </style>