Глава 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. Финальные испытания
- Запустите FastAPI сервер:
uvicorn main:app --reload
- Откройте
index.html
в браузере (через Live Server). - Проверьте полный цикл:
- Список кораблей должен загрузиться автоматически.
- Нажмите "Запустить новый аппарат", заполните форму и сохраните. Убедитесь, что новый корабль появился в списке.
- Нажмите "Изменить" на любом корабле, измените данные и сохраните. Убедитесь, что информация обновилась.
- Нажмите "Списать" на любом корабле, подтвердите действие. Убедитесь, что он исчез из списка.
- Проверьте все сценарии ошибок (неверные данные, остановка сервера).
Квиз для закрепления
🚀 Итог главы
Вы успешно завершили строительство и запуск своего "Центра Управления Полетами".
- 🖥️ Вы создали структурированный и стилизованный HTML/CSS интерфейс.
- ⚙️ Вы написали чистый, модульный JavaScript-код, реализующий полный CRUD-цикл.
- 🛰️ Ваш фронтенд теперь может полноценно управлять бэкендом, созданным на FastAPI.
Поздравляем с успешным завершением Главы 4! Вы прошли полный путь от отправки простого fetch
-запроса до создания полноценного веб-приложения, которое взаимодействует с вашим собственным API.