// ===== IO.JS - Импорт/Экспорт =====
/**
* Определить разделитель 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 ',';
}
/**
* Парсинг CSV
*/
function parseCSV(text) {
const delimiter = detectDelimiter(text);
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;
}
/**
* Инициализация обработчиков импорта/экспорта
*/
function initIOHandlers() {
// Импорт 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.errors > 0 && result.failedRows && result.failedRows.length > 0) {
showImportErrorDialog(result, headers);
} else if (result.errors > 0) {
alert(`📊 Импорт завершён:\n✅ Импортировано: ${result.inserted}\n❌ Ошибок: ${result.errors}`);
} else {
alert(`✅ Импортировано строк: ${result.inserted}`);
}
await table.replaceData();
e.target.value = '';
} catch (err) {
console.error('Ошибка импорта:', err);
alert('Ошибка импорта: ' + err.message);
e.target.value = '';
}
});
// Анализ CSV
document.getElementById('btnAnalyzeCSV').addEventListener('click', () => {
if (!currentSchema || !currentTable) {
alert('Сначала выберите таблицу');
return;
}
const input = document.createElement('input');
input.type = 'file';
input.accept = '.csv';
input.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);
const fkFields = currentMeta.columns.filter(c => c.IS_FOREIGN_KEY);
if (fkFields.length === 0) {
alert('В таблице нет полей со связями (FK)');
return;
}
let analysis = '🔍 АНАЛИЗ СВЯЗАННЫХ ПОЛЕЙ:\n\n';
for (const fk of fkFields) {
const colIndex = headers.indexOf(fk.COLUMN_NAME);
if (colIndex === -1) continue;
const uniqueValues = new Set();
dataRows.forEach(row => {
const val = row[colIndex];
if (val && val !== 'NULL') {
uniqueValues.add(val);
}
});
analysis += `📌 ${fk.COLUMN_NAME} → ${fk.FOREIGN_KEY.ref_table}.${fk.FOREIGN_KEY.ref_column}\n`;
analysis += ` Уникальных значений: ${uniqueValues.size}\n`;
analysis += ` Значения: ${Array.from(uniqueValues).slice(0, 10).join(', ')}`;
if (uniqueValues.size > 10) {
analysis += `, ... (еще ${uniqueValues.size - 10})`;
}
analysis += '\n\n';
}
analysis += '\n⚠️ Убедитесь что эти значения существуют в связанных таблицах!';
alert(analysis);
} catch (err) {
alert('Ошибка анализа: ' + err.message);
}
});
input.click();
});
// Экспорт
document.getElementById('btnExport').addEventListener('click', async () => {
showExportDialog();
});
}
/**
* Диалог результатов импорта с ошибками
*/
function showImportErrorDialog(result, headers) {
const modal = document.createElement('div');
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: 600px; width: 90%; max-height: 80vh; overflow-y: auto;
`;
const errorGroups = {};
result.failedRows.forEach(item => {
const errType = item.error || 'Неизвестная ошибка';
if (!errorGroups[errType]) {
errorGroups[errType] = [];
}
errorGroups[errType].push(item);
});
let errorSummary = '';
Object.entries(errorGroups).forEach(([errType, items]) => {
errorSummary += `
${escapeHtml(errType)}: ${items.length} строк
`;
});
dialog.innerHTML = `
📊 Результат импорта
${result.inserted}
Импортировано
Типы ошибок:
${errorSummary}
Скачайте CSV файл с проблемными строками, исправьте данные и импортируйте повторно.
`;
modal.appendChild(dialog);
document.body.appendChild(modal);
modal.onclick = (e) => { if (e.target === modal) modal.remove(); };
dialog.querySelector('#importErrorClose').onclick = () => modal.remove();
dialog.querySelector('#importErrorDownload').onclick = () => {
const delimiter = ';';
const errorHeaders = [...headers, '_ОШИБКА', '_СТРОКА'];
let csvContent = errorHeaders.join(delimiter) + '\n';
result.failedRows.forEach(item => {
const rowValues = headers.map(h => {
let val = item.row[h];
if (val === null || val === undefined) val = '';
val = String(val);
if (val.includes(delimiter) || val.includes('"') || val.includes('\n')) {
val = '"' + val.replace(/"/g, '""') + '"';
}
return val;
});
let errorVal = item.error || '';
if (errorVal.includes(delimiter) || errorVal.includes('"')) {
errorVal = '"' + errorVal.replace(/"/g, '""') + '"';
}
rowValues.push(errorVal);
rowValues.push(item.line || '');
csvContent += rowValues.join(delimiter) + '\n';
});
const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
const filename = new Date().toISOString().slice(0, 10) + '-' + currentTable + '_errors.csv';
link.download = filename;
link.click();
URL.revokeObjectURL(link.href);
modal.remove();
};
}
/**
* Диалог экспорта
*/
async function showExportDialog() {
const tree = await api('/api/tree');
const databases = tree.map(s => s.name);
const hasTable = currentSchema && currentTable && table;
let selectedCount = 0;
if (hasTable) {
const allSelectedData = new Map();
table.getSelectedData().forEach(rowData => {
const key = getRowKey(rowData);
allSelectedData.set(key, rowData);
});
selectedRowsDataGlobal.forEach((rowData, key) => {
allSelectedData.set(key, rowData);
});
selectedCount = allSelectedData.size;
}
const modal = document.createElement('div');
modal.className = 'export-modal-overlay';
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: 550px; width: 90%; max-height: 85vh; overflow-y: auto;
`;
let columnsHtml = '';
if (hasTable && currentMeta?.columns) {
currentMeta.columns.forEach(col => {
const comment = col.COLUMN_COMMENT ? ` - ${escapeHtml(col.COLUMN_COMMENT)}` : '';
columnsHtml += `
`;
});
}
dialog.innerHTML = `
📤 Экспорт данных
${hasTable ? `
Столбцы:
${columnsHtml}
` : `
Сначала выберите таблицу для экспорта в CSV
`}
Скачать SQL дамп базы данных:
${databases.map(db => `
`).join('')}
`;
modal.appendChild(dialog);
document.body.appendChild(modal);
const tabCSV = dialog.querySelector('#tabCSV');
const tabBackup = dialog.querySelector('#tabBackup');
const panelCSV = dialog.querySelector('#panelCSV');
const panelBackup = dialog.querySelector('#panelBackup');
tabCSV.onclick = () => {
tabCSV.style.background = '#4CAF50'; tabCSV.style.color = 'white';
tabBackup.style.background = '#e0e0e0'; tabBackup.style.color = 'black';
panelCSV.style.display = 'block'; panelBackup.style.display = 'none';
};
tabBackup.onclick = () => {
tabBackup.style.background = '#2196F3'; tabBackup.style.color = 'white';
tabCSV.style.background = '#e0e0e0'; tabCSV.style.color = 'black';
panelBackup.style.display = 'block'; panelCSV.style.display = 'none';
};
modal.onclick = (e) => { if (e.target === modal) modal.remove(); };
dialog.querySelector('#exportClose').onclick = () => modal.remove();
if (hasTable) {
dialog.querySelector('#exportSelectAllCols')?.addEventListener('click', () => {
dialog.querySelectorAll('[id^="export_col_"]').forEach(cb => cb.checked = true);
});
dialog.querySelector('#exportDeselectAllCols')?.addEventListener('click', () => {
dialog.querySelectorAll('[id^="export_col_"]').forEach(cb => cb.checked = false);
});
dialog.querySelector('#exportCSVBtn')?.addEventListener('click', async () => {
const mode = dialog.querySelector('input[name="exportMode"]:checked')?.value || 'all';
const selectedColumns = [];
dialog.querySelectorAll('[id^="export_col_"]:checked').forEach(cb => {
selectedColumns.push(cb.dataset.field);
});
if (selectedColumns.length === 0) {
alert('Выберите хотя бы один столбец');
return;
}
modal.remove();
await performExport(mode, selectedColumns);
});
}
dialog.querySelectorAll('.backup-btn').forEach(btn => {
btn.onmouseover = () => btn.style.opacity = '0.8';
btn.onmouseout = () => btn.style.opacity = '1';
btn.onclick = async () => {
const db = btn.dataset.db;
const originalText = btn.textContent;
btn.disabled = true;
btn.textContent = '⏳ Создание дампа...';
try {
const url = db === '__all__' ? '/api/backup/all' : `/api/backup/database/${encodeURIComponent(db)}`;
const response = await fetch(url);
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Ошибка создания бэкапа');
}
const blob = await response.blob();
const filename = response.headers.get('Content-Disposition')?.match(/filename="(.+)"/)?.[1]
|| (db === '__all__' ? 'backup_all.sql.gz' : `${db}.sql.gz`);
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
URL.revokeObjectURL(link.href);
modal.remove();
} catch (err) {
alert('❌ Ошибка: ' + err.message);
btn.disabled = false;
btn.textContent = originalText;
}
};
});
}
/**
* Выполнение экспорта CSV
*/
async function performExport(mode, selectedColumns) {
const headerFilters = table.getHeaderFilters ? table.getHeaderFilters() : [];
const filters = (headerFilters || []).map(f => ({ field: f.field, value: f.value })).filter(f => f.value);
const sorters = table.getSorters ? table.getSorters() : [];
const sort = (sorters && sorters.length > 0) ? { field: sorters[0].field, dir: sorters[0].dir } : null;
const columnsToExport = currentMeta.columns.filter(c => selectedColumns.includes(c.COLUMN_NAME));
let csvContent = '';
const delimiter = ';';
if (mode === 'template') {
csvContent = selectedColumns.join(delimiter);
} else if (mode === 'selected') {
const tabulatorSelected = table.getSelectedData();
const allSelectedData = new Map();
tabulatorSelected.forEach(rowData => {
const key = getRowKey(rowData);
allSelectedData.set(key, rowData);
});
selectedRowsDataGlobal.forEach((rowData, key) => {
allSelectedData.set(key, rowData);
});
const rows = Array.from(allSelectedData.values());
csvContent = selectedColumns.join(delimiter) + '\n';
rows.forEach(row => {
const rowValues = selectedColumns.map(col => {
let val = row[col];
if (val === null || val === undefined) val = '';
val = String(val);
if (val.includes(delimiter) || val.includes('"') || val.includes('\n')) {
val = '"' + val.replace(/"/g, '""') + '"';
}
return val;
});
csvContent += rowValues.join(delimiter) + '\n';
});
} else {
const result = await api('/api/table/export-csv', 'POST', {
schema: currentSchema,
table: currentTable,
filters: filters,
sort: sort,
columns: columnsToExport
});
csvContent = result.csv;
}
const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
let basename = currentTable;
if (mode === 'template') {
basename += '_template';
} else if (mode === 'selected') {
basename += '_selected';
}
const filename = new Date().toISOString().slice(0, 10) + '-' + basename + '.csv';
link.setAttribute('href', url);
link.setAttribute('download', filename);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
console.log('✅ io.js загружен');