mne-nuzhen-prostoy-udobnyy-i/script.js

444 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// === Хранилище данных ===
const STORAGE_KEYS = {
SETTINGS: 'costCalculator_settings',
HISTORY: 'costCalculator_history',
CURRENT_TYPE: 'costCalculator_currentType'
};
// Данные по умолчанию
const DEFAULT_SETTINGS = {
types: [
{
id: 'regional',
name: 'Областной',
varCosts: 15921.2583,
avgCapacity: 9.4317,
fixedCosts: 8129.6707
}
]
};
// Состояние приложения
let state = {
settings: null,
history: [],
currentTypeId: null
};
// === Инициализация ===
function init() {
loadSettings();
loadHistory();
renderTypeSelector();
renderTypeTabs();
renderHistory();
setupEventListeners();
// Загружаем последний выбранный тип
const lastType = localStorage.getItem(STORAGE_KEYS.CURRENT_TYPE);
if (lastType && state.settings.types.find(t => t.id === lastType)) {
state.currentTypeId = lastType;
document.getElementById('calcType').value = lastType;
}
}
// === Загрузка настроек ===
function loadSettings() {
const saved = localStorage.getItem(STORAGE_KEYS.SETTINGS);
if (saved) {
try {
state.settings = JSON.parse(saved);
} catch (e) {
state.settings = JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
}
} else {
state.settings = JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
}
if (!state.currentTypeId && state.settings.types.length > 0) {
state.currentTypeId = state.settings.types[0].id;
}
}
// === Сохранение настроек ===
function saveSettings() {
localStorage.setItem(STORAGE_KEYS.SETTINGS, JSON.stringify(state.settings));
}
// === Загрузка истории ===
function loadHistory() {
const saved = localStorage.getItem(STORAGE_KEYS.HISTORY);
if (saved) {
try {
state.history = JSON.parse(saved);
} catch (e) {
state.history = [];
}
}
}
// === Сохранение истории ===
function saveHistory() {
localStorage.setItem(STORAGE_KEYS.HISTORY, JSON.stringify(state.history));
}
// === Рендер селектора типов ===
function renderTypeSelector() {
const select = document.getElementById('calcType');
select.innerHTML = state.settings.types.map(t =>
`<option value="${t.id}">${escapeHtml(t.name)}</option>`
).join('');
if (state.currentTypeId) {
select.value = state.currentTypeId;
}
}
// === Рендер табов в настройках ===
function renderTypeTabs() {
const container = document.getElementById('typeTabs');
container.innerHTML = state.settings.types.map(t =>
`<button class="tab-btn${t.id === state.currentTypeId ? ' active' : ''}"
data-type-id="${t.id}">${escapeHtml(t.name)}</button>`
).join('');
}
// === Получение текущего типа ===
function getCurrentType() {
return state.settings.types.find(t => t.id === state.currentTypeId);
}
// === Заполнение формы настроек ===
function fillSettingsForm() {
const type = getCurrentType();
if (!type) return;
document.getElementById('typeName').value = type.name;
document.getElementById('varCosts').value = type.varCosts;
document.getElementById('avgCapacity').value = type.avgCapacity;
document.getElementById('fixedCosts').value = type.fixedCosts;
// Показываем кнопку удаления, если типов больше 1
document.getElementById('deleteTypeBtn').style.display =
state.settings.types.length > 1 ? 'inline-block' : 'none';
}
// === Расчет себестоимости ===
function calculateCost(speed, type) {
const variablePart = (type.varCosts / type.avgCapacity) * speed;
const total = variablePart + type.fixedCosts;
return {
variablePart,
fixedPart: type.fixedCosts,
total,
speed,
type
};
}
// === Форматирование числа ===
function formatNumber(num, decimals = 2) {
return num.toLocaleString('ru-RU', {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
});
}
// === Экранирование HTML ===
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// === Валидация ===
function validateSettings(name, varCosts, avgCapacity, fixedCosts, isEditing = false) {
const errors = [];
if (!name || name.trim() === '') {
errors.push('Введите название типа расчета');
}
const existingType = state.settings.types.find(t =>
t.name.toLowerCase() === name.toLowerCase().trim() && t.id !== (isEditing ? state.currentTypeId : null)
);
if (existingType) {
errors.push('Тип с таким названием уже существует');
}
if (varCosts === '' || isNaN(varCosts) || parseFloat(varCosts) < 0) {
errors.push('Переменные затраты должны быть неотрицательным числом');
}
if (avgCapacity === '' || isNaN(avgCapacity) || parseFloat(avgCapacity) <= 0) {
errors.push('Пропускная способность должна быть положительным числом');
}
if (fixedCosts === '' || isNaN(fixedCosts) || parseFloat(fixedCosts) < 0) {
errors.push('Постоянные затраты должны быть неотрицательным числом');
}
return errors;
}
function validateSpeed(speed) {
if (speed === '' || speed === null || speed === undefined) {
return 'Введите необходимую скорость';
}
const num = parseFloat(speed);
if (isNaN(num) || num < 0) {
return 'Скорость не может быть отрицательной';
}
return null;
}
// === Добавление записи в историю ===
function addToHistory(result, type) {
const record = {
id: Date.now(),
date: new Date().toISOString(),
typeId: type.id,
typeName: type.name,
speed: result.speed,
varCosts: type.varCosts,
avgCapacity: type.avgCapacity,
fixedCosts: type.fixedCosts,
total: result.total
};
state.history.unshift(record);
// Храним последние 100 записей
if (state.history.length > 100) {
state.history = state.history.slice(0, 100);
}
saveHistory();
}
// === Рендер истории ===
function renderHistory() {
const tbody = document.getElementById('historyBody');
const emptyMsg = document.getElementById('emptyHistory');
if (state.history.length === 0) {
tbody.innerHTML = '';
emptyMsg.classList.add('show');
return;
}
emptyMsg.classList.remove('show');
tbody.innerHTML = state.history.map(record => {
const date = new Date(record.date);
const dateStr = date.toLocaleDateString('ru-RU');
const timeStr = date.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
return `
<tr>
<td>${dateStr}<br><small style="color: #71717a">${timeStr}</small></td>
<td>${escapeHtml(record.typeName)}</td>
<td>${formatNumber(record.speed, 4)} Мбит/с</td>
<td><strong>${formatNumber(record.total)} ₸</strong></td>
</tr>
`;
}).join('');
}
// === Очистка истории ===
function clearHistory() {
if (confirm('Вы уверены, что хотите очистить всю историю расчетов?')) {
state.history = [];
saveHistory();
renderHistory();
}
}
// === Показать результат ===
function showResult(result, type) {
const section = document.getElementById('resultSection');
document.getElementById('resultValue').textContent = formatNumber(result.total) + ' ₸';
document.getElementById('variablePart').textContent = formatNumber(result.variablePart) + ' ₸';
document.getElementById('fixedPart').textContent = formatNumber(result.fixedPart) + ' ₸';
document.getElementById('speedValue').textContent = formatNumber(result.speed, 4) + ' Мбит/с';
// Формула
const formula = `(${formatNumber(type.varCosts, 4)} / ${formatNumber(type.avgCapacity, 4)}) × ${formatNumber(result.speed, 4)} + ${formatNumber(type.fixedCosts, 4)} = ${formatNumber(result.total)} ₸`;
document.getElementById('formulaText').textContent = formula;
section.classList.add('show');
section.style.display = 'block';
section.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
// === Setup event listeners ===
function setupEventListeners() {
// Калькулятор
document.getElementById('calculateBtn').addEventListener('click', handleCalculate);
document.getElementById('calcType').addEventListener('change', (e) => {
state.currentTypeId = e.target.value;
localStorage.setItem(STORAGE_KEYS.CURRENT_TYPE, state.currentTypeId);
});
// История
document.getElementById('clearHistory').addEventListener('click', clearHistory);
// Модальное окно настроек
document.getElementById('openSettings').addEventListener('click', openSettings);
document.getElementById('closeSettings').addEventListener('click', closeSettings);
document.getElementById('settingsModal').addEventListener('click', (e) => {
if (e.target.id === 'settingsModal') closeSettings();
});
// Табы и кнопки в настройках
document.getElementById('typeTabs').addEventListener('click', handleTabClick);
document.getElementById('addTypeBtn').addEventListener('click', addNewType);
document.getElementById('saveSettings').addEventListener('click', saveSettingsHandler);
document.getElementById('deleteTypeBtn').addEventListener('click', deleteType);
// Enter в поле скорости
document.getElementById('speed').addEventListener('keypress', (e) => {
if (e.key === 'Enter') handleCalculate();
});
// Enter в полях настроек
document.getElementById('typeName').addEventListener('keypress', (e) => {
if (e.key === 'Enter') saveSettingsHandler();
});
}
// === Обработчик расчета ===
function handleCalculate() {
const speedInput = document.getElementById('speed');
const speed = parseFloat(speedInput.value);
const validationError = validateSpeed(speedInput.value);
if (validationError) {
alert(validationError);
speedInput.focus();
return;
}
const type = getCurrentType();
if (!type) {
alert('Выберите тип расчета');
return;
}
// Проверка на деление на ноль
if (type.avgCapacity <= 0) {
alert('Ошибка: пропускная способность должна быть больше нуля. Проверьте настройки.');
openSettings();
return;
}
const result = calculateCost(speed, type);
showResult(result, type);
addToHistory(result, type);
renderHistory();
}
// === Открытие настроек ===
function openSettings() {
renderTypeTabs();
fillSettingsForm();
document.getElementById('saveMessage').style.display = 'none';
document.getElementById('settingsModal').style.display = 'flex';
document.getElementById('typeName').focus();
}
// === Закрытие настроек ===
function closeSettings() {
document.getElementById('settingsModal').style.display = 'none';
}
// === Обработчик клика по табам ===
function handleTabClick(e) {
if (!e.target.classList.contains('tab-btn')) return;
const typeId = e.target.dataset.typeId;
state.currentTypeId = typeId;
renderTypeTabs();
fillSettingsForm();
}
// === Добавление нового типа ===
function addNewType() {
const newId = 'type_' + Date.now();
const newType = {
id: newId,
name: 'Новый тип',
varCosts: 0,
avgCapacity: 1,
fixedCosts: 0
};
state.settings.types.push(newType);
state.currentTypeId = newId;
saveSettings();
renderTypeTabs();
fillSettingsForm();
}
// === Удаление типа ===
function deleteType() {
if (state.settings.types.length <= 1) {
alert('Нельзя удалить последний тип расчета');
return;
}
if (confirm('Вы уверены, что хотите удалить этот тип расчета?')) {
const index = state.settings.types.findIndex(t => t.id === state.currentTypeId);
state.settings.types.splice(index, 1);
// Выбираем первый оставшийся тип
state.currentTypeId = state.settings.types[0].id;
saveSettings();
renderTypeSelector();
renderTypeTabs();
fillSettingsForm();
}
}
// === Сохранение настроек ===
function saveSettingsHandler() {
const name = document.getElementById('typeName').value.trim();
const varCosts = parseFloat(document.getElementById('varCosts').value);
const avgCapacity = parseFloat(document.getElementById('avgCapacity').value);
const fixedCosts = parseFloat(document.getElementById('fixedCosts').value);
const errors = validateSettings(name, varCosts, avgCapacity, fixedCosts, true);
if (errors.length > 0) {
alert(errors.join('\n'));
return;
}
const type = getCurrentType();
type.name = name;
type.varCosts = varCosts;
type.avgCapacity = avgCapacity;
type.fixedCosts = fixedCosts;
saveSettings();
renderTypeSelector();
renderTypeTabs();
// Показываем сообщение об успехе
const msg = document.getElementById('saveMessage');
msg.style.display = 'block';
setTimeout(() => {
msg.style.display = 'none';
}, 3000);
}
// === Закрытие модального окна по Escape ===
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeSettings();
}
});
// === Старт ===
init();