Skip to content

Глава 4.6: Создание простого интерфейса для API

Время изучения: 1 час 15 минут


1. Финальная сборка: Запуск "Центра Управления Полетами"

Мы изучили все системы по отдельности: настроили "антенну" (Fetch), научились отправлять "команды" (GET, POST, DELETE), разработали "аварийные протоколы" (обработка ошибок).

Настало время собрать все компоненты воедино и запустить наш ЦУП — полноценный, интерактивный интерфейс для управления космическим флотом.

Наша цель:

  • Создать единый, чистый и понятный интерфейс.
  • Реализовать полный CRUD-цикл: создание, отображение, обновление и удаление кораблей.
  • Добавить визуальную обратную связь для пользователя (загрузка, успех, ошибка).

💡 Космическая аналогия:

Мы переходим от отдельных тестовых консолей к главному экрану ЦУПа. На нем должны быть все необходимые кнопки и индикаторы, чтобы один оператор мог управлять всем флотом, не переключаясь между десятками разных систем.


2. Проектирование интерфейса: "Приборная панель"

Нам понадобится более структурированный HTML. Мы будем использовать "карточки" для отображения кораблей и модальное окно для их редактирования.

Обновляем index.html:

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>ЦУП v2.0 - Управление Флотом</title>
    <link rel="stylesheet" href="style.css"> <!-- Подключаем стили -->
</head>
<body>
    <header>
        <h1>Панель управления космическим флотом</h1>
    </header>

    <main>
        <section id="fleet-controls">
            <button id="load-fleet-btn">Обновить список флота</button>
            <button id="show-create-form-btn">Запустить новый аппарат</button>
        </section>

        <section id="fleet-display">
            <h2>Текущий состав флота</h2>
            <div id="fleet-list" class="cards-container">
                <!-- Карточки кораблей будут здесь -->
            </div>
        </section>
    </main>

    <!-- Модальное окно для создания/редактирования (изначально скрыто) -->
    <div id="modal" class="modal-overlay" style="display: none;">
        <div class="modal-content">
            <h2 id="modal-title">Запуск нового аппарата</h2>
            <form id="ship-form">
                <input type="hidden" id="ship-id">
                <input type="text" id="ship-name" placeholder="Название" required>
                <input type="text" id="ship-type" placeholder="Тип" required>
                <input type="number" id="ship-year" placeholder="Год запуска" required>
                <input type="text" id="ship-status" placeholder="Статус" required>
                <div class="modal-actions">
                    <button type="submit" id="save-btn">Сохранить</button>
                    <button type="button" id="cancel-btn">Отмена</button>
                </div>
            </form>
            <div id="notification-area"></div>
        </div>
    </div>

    <script src="api.js"></script>
    <script src="app.js"></script>
</body>
</html>


3. Добавляем "космический" дизайн: style.css

Создайте файл style.css, чтобы наш ЦУП выглядел достойно.

/* style.css */
body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background-color: #1a1a2e;
    color: #e0e0e0;
    margin: 0;
    padding: 20px;
}
header { text-align: center; margin-bottom: 20px; }
button {
    background-color: #4a4e69;
    color: white;
    border: none;
    padding: 10px 15px;
    border-radius: 5px;
    cursor: pointer;
    transition: background-color 0.3s;
}
button:hover { background-color: #6a6e94; }
.cards-container {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: 20px;
}
.card {
    background-color: #2a2a4e;
    border: 1px solid #4a4e69;
    border-radius: 8px;
    padding: 15px;
}
.card h3 { margin-top: 0; color: #9394a5; }
.card-actions { margin-top: 15px; }

/* Стили для модального окна */
.modal-overlay {
    position: fixed;
    top: 0; left: 0;
    width: 100%; height: 100%;
    background-color: rgba(0,0,0,0.7);
    display: flex;
    justify-content: center;
    align-items: center;
}
.modal-content {
    background: #1a1a2e;
    padding: 20px;
    border-radius: 8px;
    border: 1px solid #4a4e69;
    width: 90%;
    max-width: 500px;
}
#ship-form input {
    width: calc(100% - 20px);
    padding: 10px;
    margin-bottom: 10px;
    border-radius: 4px;
    border: 1px solid #4a4e69;
    background-color: #2a2a4e;
    color: white;
}
.modal-actions { text-align: right; }


4. Полная пересборка логики: app.js

Теперь мы напишем финальную версию app.js, объединяя все наши знания.

// app.js

// --- DOM Элементы ---
const loadFleetBtn = document.getElementById('load-fleet-btn');
const fleetListContainer = document.getElementById('fleet-list');
const modal = document.getElementById('modal');
const modalTitle = document.getElementById('modal-title');
const shipForm = document.getElementById('ship-form');
const saveBtn = document.getElementById('save-btn');
const cancelBtn = document.getElementById('cancel-btn');
const showCreateFormBtn = document.getElementById('show-create-form-btn');
const notificationArea = document.getElementById('notification-area');

// --- Функции для UI ---

function showNotification(message, isError = false) {
    notificationArea.textContent = message;
    notificationArea.style.color = isError ? '#ff6b6b' : '#6bff6b';
}

function openModalForCreate() {
    shipForm.reset();
    document.getElementById('ship-id').value = '';
    modalTitle.textContent = 'Запуск нового аппарата';
    modal.style.display = 'flex';
}

function openModalForEdit(ship) {
    shipForm.reset();
    document.getElementById('ship-id').value = ship.id;
    document.getElementById('ship-name').value = ship.name;
    document.getElementById('ship-type').value = ship.type;
    document.getElementById('ship-year').value = ship.launch_year;
    document.getElementById('ship-status').value = ship.status;
    modalTitle.textContent = `Редактирование: ${ship.name}`;
    modal.style.display = 'flex';
}

function closeModal() {
    modal.style.display = 'none';
    notificationArea.textContent = '';
}

function createShipCard(ship) {
    const card = document.createElement('div');
    card.className = 'card';
    card.innerHTML = `
        <h3>${ship.name} (ID: ${ship.id})</h3>
        <p>Тип: ${ship.type}</p>
        <p>Год запуска: ${ship.launch_year}</p>
        <p>Статус: ${ship.status}</p>
        <div class="card-actions">
            <button class="edit-btn" data-ship-id="${ship.id}">Изменить</button>
            <button class="delete-btn" data-ship-id="${ship.id}">Списать</button>
        </div>
    `;
    return card;
}

// --- Логика API и отображения ---

async function fetchAndDisplayFleet() {
    try {
        fleetListContainer.innerHTML = '<p>Загрузка телеметрии...</p>';
        const ships = await apiRequest('/spaceships');

        fleetListContainer.innerHTML = '';
        if (ships.length === 0) {
            fleetListContainer.innerHTML = '<p>В реестре нет аппаратов.</p>';
            return;
        }
        ships.forEach(ship => {
            const card = createShipCard(ship);
            fleetListContainer.appendChild(card);
        });
    } catch (error) {
        fleetListContainer.innerHTML = `<p style="color: #ff6b6b;">Ошибка загрузки флота: ${error.message}</p>`;
    }
}

async function handleSaveShip(event) {
    event.preventDefault();
    const shipId = document.getElementById('ship-id').value;
    const shipData = {
        name: document.getElementById('ship-name').value,
        type: document.getElementById('ship-type').value,
        launch_year: parseInt(document.getElementById('ship-year').value),
        status: document.getElementById('ship-status').value
    };

    try {
        let response;
        if (shipId) {
            // Обновление (PUT)
            response = await apiRequest(`/spaceships/${shipId}`, {
                method: 'PUT',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(shipData)
            });
            showNotification(`Аппарат "${response.name}" успешно обновлен!`);
        } else {
            // Создание (POST)
            response = await apiRequest('/spaceships', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(shipData)
            });
            showNotification(`Аппарат "${response.name}" успешно запущен! ID: ${response.id}`);
        }

        setTimeout(() => {
            closeModal();
            fetchAndDisplayFleet();
        }, 1500);

    } catch (error) {
        showNotification(error.message, true);
    }
}

async function handleDeleteShip(shipId) {
    if (!confirm(`Вы уверены, что хотите списать аппарат с ID ${shipId}?`)) return;

    try {
        await apiRequest(`/spaceships/${shipId}`, { method: 'DELETE' });
        alert('Аппарат успешно списан.');
        fetchAndDisplayFleet();
    } catch (error) {
        alert(`Ошибка при списании: ${error.message}`);
    }
}

// --- Обработчики событий ---

document.addEventListener('DOMContentLoaded', fetchAndDisplayFleet);
loadFleetBtn.addEventListener('click', fetchAndDisplayFleet);
showCreateFormBtn.addEventListener('click', openModalForCreate);
cancelBtn.addEventListener('click', closeModal);
shipForm.addEventListener('submit', handleSaveShip);

fleetListContainer.addEventListener('click', async (event) => {
    const target = event.target;
    if (target.classList.contains('delete-btn')) {
        handleDeleteShip(target.dataset.shipId);
    }
    if (target.classList.contains('edit-btn')) {
        try {
            const ship = await apiRequest(`/spaceships/${target.dataset.shipId}`);
            openModalForEdit(ship);
        } catch (error) {
            alert(`Не удалось загрузить данные для редактирования: ${error.message}`);
        }
    }
});

5. Финальные испытания

  1. Запустите FastAPI сервер: uvicorn main:app --reload
  2. Откройте index.html в браузере (через Live Server).
  3. Проверьте полный цикл:
    • Список кораблей должен загрузиться автоматически.
    • Нажмите "Запустить новый аппарат", заполните форму и сохраните. Убедитесь, что новый корабль появился в списке.
    • Нажмите "Изменить" на любом корабле, измените данные и сохраните. Убедитесь, что информация обновилась.
    • Нажмите "Списать" на любом корабле, подтвердите действие. Убедитесь, что он исчез из списка.
    • Проверьте все сценарии ошибок (неверные данные, остановка сервера).

Квиз для закрепления

1. Модальное окно в веб-интерфейсе — это...

2. Событие `DOMContentLoaded` происходит, когда...

3. Почему в финальной версии мы используем одну форму и для создания, и для редактирования?

4. `data-ship-id="${ship.id}"` — это пример...

5. Рефакторинг кода (например, вынос логики в `api.js`) нужен для...


🚀 Итог главы

Вы успешно завершили строительство и запуск своего "Центра Управления Полетами".

  • 🖥️ Вы создали структурированный и стилизованный HTML/CSS интерфейс.
  • ⚙️ Вы написали чистый, модульный JavaScript-код, реализующий полный CRUD-цикл.
  • 🛰️ Ваш фронтенд теперь может полноценно управлять бэкендом, созданным на FastAPI.

Поздравляем с успешным завершением Главы 4! Вы прошли полный путь от отправки простого fetch-запроса до создания полноценного веб-приложения, которое взаимодействует с вашим собственным API.