第 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 交互的完整 Web 应用程序的整个过程。