- Удалено детальное логирование каждой строки импорта в PHP (error_log в циклах) - Убраны console.log при парсинге и обработке CSV на фронтенде - Оставлено только логирование начала/конца импорта и ошибок - Значительно улучшена производительность при импорте больших файлов (тысячи строк)
694 lines
21 KiB
JavaScript
694 lines
21 KiB
JavaScript
let currentSchema = null;
|
||
let currentTable = null;
|
||
let currentMeta = null;
|
||
let table = null;
|
||
let enterHandler = null;
|
||
|
||
// Простая обёртка для fetch JSON
|
||
async function api(url, method = 'GET', body) {
|
||
const opts = { method, headers: { 'Content-Type': 'application/json' } };
|
||
if (body) opts.body = JSON.stringify(body);
|
||
|
||
const res = await fetch(url, opts);
|
||
if (!res.ok) {
|
||
const txt = await res.text();
|
||
throw new Error(`HTTP ${res.status}: ${txt}`);
|
||
}
|
||
return res.json();
|
||
}
|
||
|
||
// Логин
|
||
document.getElementById('loginBtn').addEventListener('click', async () => {
|
||
console.log('=== LOGIN BUTTON CLICKED ===');
|
||
const user = document.getElementById('loginUser').value.trim();
|
||
const pass = document.getElementById('loginPass').value;
|
||
const statusEl = document.getElementById('loginStatus');
|
||
|
||
if (!user || !pass) {
|
||
statusEl.textContent = 'Введите логин и пароль';
|
||
statusEl.style.color = 'red';
|
||
return;
|
||
}
|
||
|
||
statusEl.textContent = 'Проверяем подключение...';
|
||
statusEl.style.color = '';
|
||
|
||
try {
|
||
console.log('Отправляем /api/login:', { user, pass: '***' });
|
||
const res = await api('/api/login', 'POST', { user, pass });
|
||
console.log('Login ответ:', res);
|
||
|
||
if (res.ok) {
|
||
statusEl.textContent = '✓ Авторизация успешна';
|
||
statusEl.style.color = 'green';
|
||
await loadTree();
|
||
} else {
|
||
statusEl.textContent = 'Ошибка: ' + (res.error || 'Неизвестная ошибка');
|
||
statusEl.style.color = 'red';
|
||
}
|
||
} catch (e) {
|
||
console.error('Login ошибка:', e);
|
||
statusEl.textContent = 'Ошибка подключения: ' + e.message;
|
||
statusEl.style.color = 'red';
|
||
}
|
||
});
|
||
|
||
// loadTree
|
||
async function loadTree() {
|
||
console.log('=== LOAD TREE ВЫЗВАН ===');
|
||
const treeEl = document.getElementById('tree');
|
||
treeEl.style.color = '';
|
||
treeEl.innerHTML = 'Загрузка...';
|
||
|
||
try {
|
||
const tree = await api('/api/tree');
|
||
console.log('Дерево получено:', tree);
|
||
treeEl.innerHTML = '';
|
||
|
||
tree.forEach(schema => {
|
||
const schemaEl = document.createElement('div');
|
||
schemaEl.className = 'schema';
|
||
schemaEl.textContent = schema.name;
|
||
schemaEl.style.cursor = 'pointer';
|
||
schemaEl.style.padding = '4px';
|
||
schemaEl.style.fontWeight = 'bold';
|
||
treeEl.appendChild(schemaEl);
|
||
|
||
(schema.tables || []).forEach(tbl => {
|
||
const tableEl = document.createElement('div');
|
||
tableEl.className = 'table';
|
||
tableEl.textContent = ` ${tbl}`;
|
||
tableEl.style.cursor = 'pointer';
|
||
tableEl.style.paddingLeft = '12px';
|
||
tableEl.style.padding = '2px';
|
||
tableEl.addEventListener('click', () => selectTable(schema.name, tbl));
|
||
treeEl.appendChild(tableEl);
|
||
});
|
||
});
|
||
} catch (e) {
|
||
console.error('loadTree ошибка:', e);
|
||
treeEl.innerHTML = 'Ошибка загрузки: ' + e.message;
|
||
treeEl.style.color = 'red';
|
||
}
|
||
}
|
||
|
||
let lastEditedRow = null;
|
||
|
||
async function selectTable(schema, tableName) {
|
||
currentSchema = schema;
|
||
currentTable = tableName;
|
||
lastEditedRow = null;
|
||
|
||
console.log('=== SELECT TABLE ===', { schema, tableName });
|
||
|
||
if (enterHandler) {
|
||
document.removeEventListener('keydown', enterHandler);
|
||
enterHandler = null;
|
||
}
|
||
|
||
currentMeta = await api(
|
||
`/api/table/meta?schema=${encodeURIComponent(schema)}&table=${encodeURIComponent(tableName)}`
|
||
);
|
||
|
||
// ✅ Добавляем столбец с чекбоксами в начало
|
||
const columns = [
|
||
{
|
||
formatter: "rowSelection",
|
||
titleFormatter: "rowSelection",
|
||
hozAlign: "center",
|
||
headerSort: false,
|
||
width: 40,
|
||
cellClick: function(e, cell) {
|
||
cell.getRow().toggleSelect();
|
||
}
|
||
},
|
||
...currentMeta.columns.map(col => ({
|
||
title: col.COLUMN_NAME,
|
||
field: col.COLUMN_NAME,
|
||
editor: "input",
|
||
headerFilter: "input"
|
||
}))
|
||
];
|
||
|
||
if (table) {
|
||
table.destroy();
|
||
table = null;
|
||
}
|
||
|
||
table = new Tabulator("#table", {
|
||
selectableRows: true,
|
||
columns: columns,
|
||
layout: "fitColumns",
|
||
resizableColumnFit: true,
|
||
|
||
pagination: true,
|
||
paginationMode: "remote",
|
||
paginationSize: 50,
|
||
paginationSizeSelector: [25, 50, 100, 200],
|
||
|
||
filterMode: "remote",
|
||
sortMode: "remote",
|
||
|
||
ajaxURL: "/api/table/data",
|
||
ajaxConfig: "POST",
|
||
ajaxContentType: "json",
|
||
|
||
ajaxParams: function () {
|
||
const headerFilters = this.getHeaderFilters ? this.getHeaderFilters() : [];
|
||
const filters = (headerFilters || []).map(f => ({
|
||
field: f.field,
|
||
value: f.value
|
||
})).filter(f => f.value !== null && f.value !== '');
|
||
|
||
const sorters = this.getSorters ? this.getSorters() : [];
|
||
const sort = (sorters && sorters.length > 0) ? {
|
||
field: sorters[0].field,
|
||
dir: sorters[0].dir
|
||
} : null;
|
||
|
||
const params = {
|
||
schema: currentSchema,
|
||
table: currentTable,
|
||
filters: filters,
|
||
sort: sort,
|
||
columns: currentMeta.columns,
|
||
page: this.getPage ? this.getPage() : 1,
|
||
pageSize: this.getPageSize ? this.getPageSize() : 50
|
||
};
|
||
|
||
console.log('📤 Запрос к серверу:', {
|
||
page: params.page,
|
||
pageSize: params.pageSize,
|
||
filters: params.filters,
|
||
sort: params.sort
|
||
});
|
||
|
||
return params;
|
||
},
|
||
|
||
ajaxResponse: function (url, params, response) {
|
||
console.log('📥 Ответ от сервера:', {
|
||
page: response.current_page,
|
||
last_page: response.last_page,
|
||
total: response.total,
|
||
rows: response.data ? response.data.length : 0
|
||
});
|
||
|
||
return {
|
||
last_page: response.last_page || 1,
|
||
data: response.data || []
|
||
};
|
||
},
|
||
|
||
cellEdited: function(cell) {
|
||
const row = cell.getRow();
|
||
lastEditedRow = row.getData();
|
||
row.getElement().style.backgroundColor = '#fffae6';
|
||
|
||
console.log('Ячейка отредактирована:', cell.getField(), '→', cell.getValue());
|
||
},
|
||
|
||
headerFilterLiveFilterDelay: 800,
|
||
|
||
dataFiltering: function(filters) {
|
||
const activeFilters = filters.filter(f => f.value);
|
||
console.log('🔍 Активные фильтры:', activeFilters.map(f => `${f.field}=${f.value}`));
|
||
},
|
||
|
||
dataFiltered: function(filters, rows) {
|
||
console.log('✅ Фильтрация применена, записей на странице:', rows.length);
|
||
},
|
||
|
||
pageLoaded: function(pageno) {
|
||
console.log('📄 Загружена страница:', pageno);
|
||
}
|
||
});
|
||
|
||
// ✅ Сохранение по Enter
|
||
enterHandler = async function(e) {
|
||
if (e.key === 'Enter' && lastEditedRow && currentSchema && currentTable) {
|
||
e.preventDefault();
|
||
console.log('Enter нажат, сохраняем строку:', lastEditedRow);
|
||
|
||
try {
|
||
const res = await api('/api/table/update', 'POST', {
|
||
schema: currentSchema,
|
||
table: currentTable,
|
||
row: lastEditedRow
|
||
});
|
||
|
||
console.log('Сохранено:', res);
|
||
lastEditedRow = null;
|
||
|
||
table.getRows().forEach(r => {
|
||
r.getElement().style.backgroundColor = '';
|
||
});
|
||
|
||
await table.replaceData();
|
||
} catch (err) {
|
||
console.error('Ошибка сохранения:', err);
|
||
alert('Ошибка: ' + err.message);
|
||
}
|
||
}
|
||
};
|
||
|
||
document.addEventListener('keydown', enterHandler);
|
||
}
|
||
|
||
// ✅ ВСТАВИТЬ - добавляет строку НАД выбранной или внизу
|
||
document.getElementById('btnInsert').addEventListener('click', async () => {
|
||
if (!currentSchema || !currentTable || !currentMeta) {
|
||
alert('Сначала выберите таблицу');
|
||
return;
|
||
}
|
||
|
||
const rowData = {};
|
||
const fkFields = [];
|
||
|
||
currentMeta.columns.forEach(col => {
|
||
const name = col.COLUMN_NAME;
|
||
|
||
if (col.IS_AUTO_INCREMENT) return;
|
||
|
||
if (col.HAS_DEFAULT && col.COLUMN_DEFAULT !== null) {
|
||
if (String(col.COLUMN_DEFAULT).toUpperCase().includes('CURRENT_TIMESTAMP')) return;
|
||
rowData[name] = col.COLUMN_DEFAULT;
|
||
return;
|
||
}
|
||
|
||
if (col.IS_FOREIGN_KEY) {
|
||
fkFields.push({
|
||
name: name,
|
||
ref_schema: col.FOREIGN_KEY.ref_schema,
|
||
ref_table: col.FOREIGN_KEY.ref_table,
|
||
ref_column: col.FOREIGN_KEY.ref_column,
|
||
required: col.IS_REQUIRED
|
||
});
|
||
|
||
if (!col.IS_REQUIRED) {
|
||
rowData[name] = null;
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (col.IS_REQUIRED) {
|
||
if (col.EDITOR_TYPE === 'number') rowData[name] = 0;
|
||
else if (col.EDITOR_TYPE === 'datetime' || col.EDITOR_TYPE === 'date')
|
||
rowData[name] = new Date().toISOString().slice(0, 10);
|
||
else if (col.EDITOR_TYPE === 'time') rowData[name] = '00:00:00';
|
||
else rowData[name] = '';
|
||
}
|
||
});
|
||
|
||
const requiredFKs = fkFields.filter(f => f.required);
|
||
if (requiredFKs.length > 0) {
|
||
try {
|
||
const fkValues = await promptForForeignKeys(requiredFKs);
|
||
if (!fkValues) {
|
||
return;
|
||
}
|
||
Object.assign(rowData, fkValues);
|
||
} catch (err) {
|
||
console.error('Ошибка получения FK значений:', err);
|
||
alert('Ошибка: ' + err.message);
|
||
return;
|
||
}
|
||
}
|
||
|
||
try {
|
||
const result = await api('/api/table/insert', 'POST', {
|
||
schema: currentSchema,
|
||
table: currentTable,
|
||
row: rowData
|
||
});
|
||
|
||
// ✅ Получаем выбранные строки
|
||
const selected = table.getSelectedRows();
|
||
|
||
if (selected.length > 0) {
|
||
// Вставляем НАД первой выбранной строкой
|
||
const targetRow = selected[0];
|
||
const position = targetRow.getPosition();
|
||
|
||
console.log(`Вставка НАД строкой на позиции ${position}`);
|
||
|
||
// Перезагружаем данные и перемещаемся на нужную страницу
|
||
await table.replaceData();
|
||
|
||
// Если нужно, можно добавить скролл к новой строке
|
||
setTimeout(() => {
|
||
const rows = table.getRows();
|
||
if (rows[position]) {
|
||
rows[position].getElement().scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
}
|
||
}, 100);
|
||
} else {
|
||
// Вставляем внизу (в конец таблицы)
|
||
console.log('Вставка внизу таблицы');
|
||
await table.replaceData();
|
||
|
||
// Переходим на последнюю страницу
|
||
const lastPage = table.getPageMax();
|
||
if (lastPage > 1) {
|
||
table.setPage(lastPage);
|
||
}
|
||
}
|
||
|
||
alert('✓ Строка успешно создана');
|
||
} catch (e) {
|
||
console.error('Ошибка вставки:', e);
|
||
alert('Ошибка вставки: ' + e.message);
|
||
}
|
||
});
|
||
|
||
// Функция для выбора FK значений
|
||
async function promptForForeignKeys(fkFields) {
|
||
const modal = document.createElement('div');
|
||
modal.className = 'fk-modal';
|
||
modal.style.cssText = `
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0,0,0,0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 10000;
|
||
`;
|
||
|
||
const dialog = document.createElement('div');
|
||
dialog.style.cssText = `
|
||
background: white;
|
||
padding: 20px;
|
||
border-radius: 8px;
|
||
max-width: 500px;
|
||
width: 90%;
|
||
max-height: 80vh;
|
||
overflow-y: auto;
|
||
`;
|
||
|
||
let html = '<h3>Заполните обязательные поля</h3>';
|
||
|
||
const fkOptions = {};
|
||
for (const fk of fkFields) {
|
||
try {
|
||
const result = await api(
|
||
`/api/fk-values?schema=${encodeURIComponent(fk.ref_schema)}&` +
|
||
`table=${encodeURIComponent(fk.ref_table)}&` +
|
||
`column=${encodeURIComponent(fk.ref_column)}`
|
||
);
|
||
fkOptions[fk.name] = result.values || [];
|
||
} catch (err) {
|
||
console.error(`Ошибка загрузки FK для ${fk.name}:`, err);
|
||
fkOptions[fk.name] = [];
|
||
}
|
||
}
|
||
|
||
fkFields.forEach(fk => {
|
||
const options = fkOptions[fk.name];
|
||
html += `
|
||
<div style="margin: 15px 0;">
|
||
<label style="display: block; margin-bottom: 5px; font-weight: bold;">
|
||
${fk.name}
|
||
<span style="color: red;">*</span>
|
||
<small style="color: #666; font-weight: normal;">
|
||
(→ ${fk.ref_table}.${fk.ref_column})
|
||
</small>
|
||
</label>
|
||
`;
|
||
|
||
if (options.length > 0) {
|
||
html += `<select id="fk_${fk.name}" style="width: 100%; padding: 8px; font-size: 14px;">`;
|
||
html += '<option value="">-- Выберите значение --</option>';
|
||
options.forEach(val => {
|
||
html += `<option value="${escapeHtml(val)}">${escapeHtml(val)}</option>`;
|
||
});
|
||
html += '</select>';
|
||
} else {
|
||
html += `
|
||
<input type="text" id="fk_${fk.name}"
|
||
style="width: 100%; padding: 8px; font-size: 14px; box-sizing: border-box;"
|
||
placeholder="Введите значение">
|
||
<small style="color: #999;">⚠️ Нет доступных значений в ${fk.ref_table}</small>
|
||
`;
|
||
}
|
||
|
||
html += '</div>';
|
||
});
|
||
|
||
html += `
|
||
<div style="margin-top: 20px; display: flex; gap: 10px; justify-content: flex-end;">
|
||
<button id="fkCancel" style="padding: 10px 20px; cursor: pointer; background: #ddd; border: none; border-radius: 4px;">
|
||
Отмена
|
||
</button>
|
||
<button id="fkSubmit" style="padding: 10px 20px; cursor: pointer; background: #4CAF50; color: white; border: none; border-radius: 4px;">
|
||
Создать строку
|
||
</button>
|
||
</div>
|
||
`;
|
||
|
||
dialog.innerHTML = html;
|
||
modal.appendChild(dialog);
|
||
document.body.appendChild(modal);
|
||
|
||
return new Promise((resolve) => {
|
||
document.getElementById('fkCancel').addEventListener('click', () => {
|
||
document.body.removeChild(modal);
|
||
resolve(null);
|
||
});
|
||
|
||
document.getElementById('fkSubmit').addEventListener('click', () => {
|
||
const values = {};
|
||
let allFilled = true;
|
||
|
||
for (const fk of fkFields) {
|
||
const input = document.getElementById(`fk_${fk.name}`);
|
||
const value = input.value.trim();
|
||
|
||
if (!value) {
|
||
alert(`Поле "${fk.name}" обязательно для заполнения!`);
|
||
allFilled = false;
|
||
break;
|
||
}
|
||
|
||
values[fk.name] = value;
|
||
}
|
||
|
||
if (allFilled) {
|
||
document.body.removeChild(modal);
|
||
resolve(values);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
// ✅ УДАЛИТЬ
|
||
document.getElementById('btnDelete').addEventListener('click', async () => {
|
||
if (!table || !currentSchema || !currentTable) return;
|
||
const selected = table.getSelectedData();
|
||
if (selected.length === 0) {
|
||
alert('Выберите строки для удаления');
|
||
return;
|
||
}
|
||
|
||
const confirmMsg = selected.length === 1
|
||
? 'Удалить выбранную строку?'
|
||
: `Удалить ${selected.length} выбранных строк?`;
|
||
|
||
if (!confirm(confirmMsg)) return;
|
||
|
||
try {
|
||
let deleted = 0;
|
||
for (const row of selected) {
|
||
await api('/api/table/delete', 'POST', { schema: currentSchema, table: currentTable, row });
|
||
deleted++;
|
||
}
|
||
await table.replaceData();
|
||
alert(`✓ Удалено строк: ${deleted}`);
|
||
} catch (e) {
|
||
console.error(e);
|
||
alert('Ошибка удаления: ' + e.message);
|
||
}
|
||
});
|
||
|
||
// ========== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ CSV ==========
|
||
|
||
function detectDelimiter(text) {
|
||
const firstLine = text.split('\n')[0];
|
||
const semicolonCount = (firstLine.match(/;/g) || []).length;
|
||
const commaCount = (firstLine.match(/,/g) || []).length;
|
||
|
||
if (semicolonCount > 0 && semicolonCount >= commaCount) {
|
||
return ';';
|
||
}
|
||
return ',';
|
||
}
|
||
|
||
function parseCSV(text) {
|
||
const delimiter = detectDelimiter(text);
|
||
console.log('Обнаружен разделитель CSV:', delimiter);
|
||
|
||
const lines = [];
|
||
let currentLine = [];
|
||
let currentField = '';
|
||
let inQuotes = false;
|
||
|
||
for (let i = 0; i < text.length; i++) {
|
||
const char = text[i];
|
||
const nextChar = text[i + 1];
|
||
|
||
if (char === '"') {
|
||
if (inQuotes && nextChar === '"') {
|
||
currentField += '"';
|
||
i++;
|
||
} else {
|
||
inQuotes = !inQuotes;
|
||
}
|
||
} else if (char === delimiter && !inQuotes) {
|
||
currentLine.push(currentField.trim());
|
||
currentField = '';
|
||
} else if ((char === '\n' || char === '\r') && !inQuotes) {
|
||
if (char === '\r' && nextChar === '\n') {
|
||
i++;
|
||
}
|
||
if (currentField || currentLine.length > 0) {
|
||
currentLine.push(currentField.trim());
|
||
lines.push(currentLine);
|
||
currentLine = [];
|
||
currentField = '';
|
||
}
|
||
} else {
|
||
currentField += char;
|
||
}
|
||
}
|
||
|
||
if (currentField || currentLine.length > 0) {
|
||
currentLine.push(currentField.trim());
|
||
lines.push(currentLine);
|
||
}
|
||
|
||
return lines;
|
||
}
|
||
|
||
// ========== ИМПОРТ CSV ==========
|
||
document.getElementById('btnImportCSV').addEventListener('click', () => {
|
||
if (!currentSchema || !currentTable) {
|
||
alert('Сначала выберите таблицу');
|
||
return;
|
||
}
|
||
document.getElementById('csvFileInput').click();
|
||
});
|
||
|
||
document.getElementById('csvFileInput').addEventListener('change', async (e) => {
|
||
const file = e.target.files[0];
|
||
if (!file) return;
|
||
|
||
try {
|
||
const text = await file.text();
|
||
const rows = parseCSV(text);
|
||
|
||
if (rows.length === 0) {
|
||
alert('CSV файл пуст');
|
||
return;
|
||
}
|
||
|
||
const headers = rows[0];
|
||
const dataRows = rows.slice(1);
|
||
|
||
if (dataRows.length === 0) {
|
||
alert('Нет данных для импорта (только заголовки)');
|
||
return;
|
||
}
|
||
|
||
// Показываем прогресс для больших файлов
|
||
if (dataRows.length > 100) {
|
||
const proceed = confirm(`Файл содержит ${dataRows.length} строк. Импорт может занять некоторое время. Продолжить?`);
|
||
if (!proceed) {
|
||
e.target.value = '';
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Преобразуем в массив объектов
|
||
const records = dataRows.map(row => {
|
||
const obj = {};
|
||
headers.forEach((header, i) => {
|
||
obj[header] = row[i] || null;
|
||
});
|
||
return obj;
|
||
});
|
||
|
||
// Отправляем на сервер
|
||
const result = await api('/api/table/import-csv', 'POST', {
|
||
schema: currentSchema,
|
||
table: currentTable,
|
||
rows: records
|
||
});
|
||
|
||
if (result.errorMessages && result.errorMessages.length > 0) {
|
||
// Показываем только первые 10 ошибок
|
||
const errorsToShow = result.errorMessages.slice(0, 10);
|
||
const moreErrors = result.errorMessages.length > 10 ? `\n... и еще ${result.errorMessages.length - 10} ошибок` : '';
|
||
alert(`Импортировано строк: ${result.inserted}\nОшибок: ${result.errors}\n\nПервые ошибки:\n${errorsToShow.join('\n')}${moreErrors}`);
|
||
} else {
|
||
alert(`✓ Импортировано строк: ${result.inserted}\nОшибок: ${result.errors}`);
|
||
}
|
||
|
||
await table.replaceData();
|
||
e.target.value = '';
|
||
} catch (err) {
|
||
console.error('Ошибка импорта:', err);
|
||
alert('Ошибка импорта: ' + err.message);
|
||
e.target.value = '';
|
||
}
|
||
});
|
||
|
||
|
||
// ========== ЭКСПОРТ CSV ==========
|
||
document.getElementById('btnExportCSV').addEventListener('click', async () => {
|
||
if (!currentSchema || !currentTable || !table) {
|
||
alert('Сначала выберите таблицу');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const headerFilters = table.getHeaderFilters ? table.getHeaderFilters() : [];
|
||
const filters = (headerFilters || []).map(f => ({ field: f.field, value: f.value }));
|
||
const sorters = table.getSorters ? table.getSorters() : [];
|
||
const sort = (sorters && sorters.length > 0) ? { field: sorters[0].field, dir: sorters[0].dir } : null;
|
||
|
||
const result = await api('/api/table/export-csv', 'POST', {
|
||
schema: currentSchema,
|
||
table: currentTable,
|
||
filters: filters,
|
||
sort: sort,
|
||
columns: currentMeta.columns
|
||
});
|
||
|
||
const csv = result.csv;
|
||
const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' });
|
||
const link = document.createElement('a');
|
||
const url = URL.createObjectURL(blob);
|
||
|
||
link.setAttribute('href', url);
|
||
link.setAttribute('download', `${currentTable}_${new Date().toISOString().slice(0,10)}.csv`);
|
||
link.style.visibility = 'hidden';
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
|
||
console.log(`Экспортировано ${result.rowCount} строк`);
|
||
} catch (err) {
|
||
console.error('Ошибка экспорта:', err);
|
||
alert('Ошибка экспорта: ' + err.message);
|
||
}
|
||
});
|