Удаление файла
This commit is contained in:
@@ -1,248 +0,0 @@
|
||||
/**
|
||||
* ui.js — Компоненты интерфейса
|
||||
* Рендер файлов, таблиц, сортировка, вкладки, управление темой
|
||||
*/
|
||||
|
||||
import { formatMoney } from './utils.js';
|
||||
|
||||
/* ========== Управление файлами ========== */
|
||||
|
||||
/**
|
||||
* Создать DOM-элемент для отображения загруженного файла
|
||||
* @param {File} file
|
||||
* @param {number} index
|
||||
* @param {string} type - 'vedomost' | 'transaction'
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
export function renderFileItem(file, index, type) {
|
||||
const isTrans = type === 'transaction';
|
||||
const el = document.createElement('div');
|
||||
el.className = 'file-item';
|
||||
el.dataset.index = index;
|
||||
el.innerHTML = `
|
||||
<div class="file-icon ${isTrans ? 'transaction' : 'vedomost'}">
|
||||
${isTrans ? '📊' : '📋'}
|
||||
</div>
|
||||
<div class="file-info">
|
||||
<div class="file-name">${escapeHtml(file.name)}</div>
|
||||
<div class="file-type">${isTrans ? 'Выгрузка транзакций' : 'Ведомость'} · ${(file.size / 1024).toFixed(0)} КБ</div>
|
||||
</div>
|
||||
<button class="file-remove" data-i="${index}" title="Удалить">✕</button>`;
|
||||
return el;
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать DOM-элемент для сообщения об ошибке в файле
|
||||
* @param {string} fileName
|
||||
* @param {string} message
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
export function renderFileError(fileName, message) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'file-item file-error';
|
||||
el.innerHTML = `
|
||||
<div class="file-icon" style="background:#fee2e2;">⚠️</div>
|
||||
<div class="file-info">
|
||||
<div class="file-name">${escapeHtml(fileName)}</div>
|
||||
<div class="file-type" style="color:#ef4444;">${escapeHtml(message)}</div>
|
||||
</div>`;
|
||||
return el;
|
||||
}
|
||||
|
||||
/* ========== Сортировка таблиц ========== */
|
||||
|
||||
/**
|
||||
* Отсортировать массив данных по ключу
|
||||
* @param {Object[]} data
|
||||
* @param {string} key
|
||||
* @param {boolean} asc
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
export function sortData(data, key, asc = true) {
|
||||
return [...data].sort((a, b) => {
|
||||
let av = a[key], bv = b[key];
|
||||
if (av === null || av === undefined) av = '';
|
||||
if (bv === null || bv === undefined) bv = '';
|
||||
if (typeof av === 'number' && typeof bv === 'number') {
|
||||
return asc ? av - bv : bv - av;
|
||||
}
|
||||
// Числовые строки сравниваем как числа
|
||||
const na = parseFloat(String(av).replace(/\s/g, '').replace(',', '.'));
|
||||
const nb = parseFloat(String(bv).replace(/\s/g, '').replace(',', '.'));
|
||||
if (!isNaN(na) && !isNaN(nb)) {
|
||||
return asc ? na - nb : nb - na;
|
||||
}
|
||||
return asc
|
||||
? String(av).localeCompare(String(bv), 'ru')
|
||||
: String(bv).localeCompare(String(av), 'ru');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Включить сортировку по клику на заголовки таблицы
|
||||
* @param {string} tbodyId - ID tbody
|
||||
* @param {Object[]} sourceData - ссылка на исходный массив
|
||||
* @param {Function} renderFn - функция перерисовки
|
||||
*/
|
||||
export function enableSorting(tbodyId, sourceData, renderFn) {
|
||||
const tbody = document.getElementById(tbodyId);
|
||||
if (!tbody) return;
|
||||
const table = tbody.closest('table');
|
||||
if (!table) return;
|
||||
const ths = table.querySelectorAll('th[data-key]');
|
||||
let sortState = { key: null, asc: true };
|
||||
|
||||
ths.forEach(th => {
|
||||
th.style.cursor = 'pointer';
|
||||
th.addEventListener('click', () => {
|
||||
const key = th.dataset.key;
|
||||
if (sortState.key === key) {
|
||||
sortState.asc = !sortState.asc;
|
||||
} else {
|
||||
sortState.key = key;
|
||||
sortState.asc = true;
|
||||
}
|
||||
// Визуальный индикатор
|
||||
ths.forEach(t => t.classList.remove('sort-asc', 'sort-desc'));
|
||||
th.classList.add(sortState.asc ? 'sort-asc' : 'sort-desc');
|
||||
const sorted = sortData(sourceData, key, sortState.asc);
|
||||
renderFn(sorted);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* ========== Рендер таблиц ========== */
|
||||
|
||||
/**
|
||||
* Рендер тела таблицы из массива объектов
|
||||
* @param {HTMLElement} tbody
|
||||
* @param {Object[]} rows
|
||||
* @param {string[]} keys - порядок/набор ключей для отображения
|
||||
* @param {Function} formatter - функция форматирования ячейки (value, key, row) => string
|
||||
*/
|
||||
export function renderTableBody(tbody, rows, keys, formatter) {
|
||||
if (!rows || rows.length === 0) {
|
||||
tbody.innerHTML = `<tr><td colspan="${(keys || Object.keys(rows[0] || {})).length}" style="text-align:center;padding:2rem;color:var(--text-muted)">Нет данных</td></tr>`;
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = rows.map(row => {
|
||||
const cells = (keys || Object.keys(row)).map(key => {
|
||||
const val = row[key];
|
||||
const formatted = formatter ? formatter(val, key, row) : formatCell(val);
|
||||
return `<td>${formatted}</td>`;
|
||||
}).join('');
|
||||
return `<tr>${cells}</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Форматирование значения ячейки по умолчанию
|
||||
*/
|
||||
function formatCell(val) {
|
||||
if (val === null || val === undefined || val === '') return '—';
|
||||
if (typeof val === 'number') return formatMoney(val);
|
||||
return escapeHtml(String(val));
|
||||
}
|
||||
|
||||
/**
|
||||
* Экранирование HTML
|
||||
*/
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/* ========== Управление вкладками ========== */
|
||||
|
||||
/**
|
||||
* Инициализировать переключатели вкладок
|
||||
*/
|
||||
export function initTabs() {
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.tab-btn').forEach(x => x.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-panel').forEach(x => x.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
const panel = document.getElementById('panel-' + btn.dataset.tab);
|
||||
if (panel) panel.classList.add('active');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* ========== Тема ========== */
|
||||
|
||||
let currentTheme = localStorage.getItem('theme') || 'light';
|
||||
|
||||
/**
|
||||
* Применить тему
|
||||
*/
|
||||
export function applyTheme() {
|
||||
document.documentElement.setAttribute('data-theme', currentTheme);
|
||||
const btn = document.getElementById('btnToggleTheme');
|
||||
if (btn) {
|
||||
btn.textContent = currentTheme === 'light'
|
||||
? '🌙 Тёмная тема'
|
||||
: '☀️ Светлая тема';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Переключить тему
|
||||
*/
|
||||
export function toggleTheme() {
|
||||
currentTheme = currentTheme === 'light' ? 'dark' : 'light';
|
||||
localStorage.setItem('theme', currentTheme);
|
||||
applyTheme();
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализировать тему
|
||||
*/
|
||||
export function initTheme(toggleBtnId) {
|
||||
applyTheme();
|
||||
const btn = document.getElementById(toggleBtnId);
|
||||
if (btn) btn.addEventListener('click', toggleTheme);
|
||||
}
|
||||
|
||||
/* ========== Прогресс-бар ========== */
|
||||
|
||||
/**
|
||||
* Показать прогресс-бар с анимацией
|
||||
* @param {number} duration - длительность анимации в мс
|
||||
* @returns {Promise} - разрешается по завершении
|
||||
*/
|
||||
export function showProgress(duration = 800) {
|
||||
const bar = document.getElementById('progressBar');
|
||||
const fill = document.getElementById('progressFill');
|
||||
if (!bar || !fill) return Promise.resolve();
|
||||
bar.classList.add('active');
|
||||
fill.style.width = '0%';
|
||||
return new Promise(resolve => {
|
||||
// Плавная анимация до 95%
|
||||
requestAnimationFrame(() => {
|
||||
fill.style.transition = `width ${duration}ms cubic-bezier(0.4, 0, 0.2, 1)`;
|
||||
fill.style.width = '95%';
|
||||
});
|
||||
// Завершение
|
||||
setTimeout(() => {
|
||||
fill.style.width = '100%';
|
||||
setTimeout(() => {
|
||||
bar.classList.remove('active');
|
||||
fill.style.width = '0%';
|
||||
fill.style.transition = 'none';
|
||||
resolve();
|
||||
}, 300);
|
||||
}, duration + 200);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Сбросить прогресс-бар
|
||||
*/
|
||||
export function resetProgress() {
|
||||
const bar = document.getElementById('progressBar');
|
||||
const fill = document.getElementById('progressFill');
|
||||
if (bar) bar.classList.remove('active');
|
||||
if (fill) { fill.style.width = '0%'; fill.style.transition = 'none'; }
|
||||
}
|
||||
Reference in New Issue
Block a user