// ===== 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}
Импортировано
${result.errors}
Ошибок

Типы ошибок:

${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 ? `

Таблица: ${currentSchema}.${currentTable}

Что экспортировать:

Столбцы:

${columnsHtml}
` : `

Сначала выберите таблицу для экспорта в CSV

`}
`; 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 загружен');