Skip to content

第 4.6 章:创建简单的 API 接口

学习时间: 1 小时 15 分钟


1. 最终组装:“飞行控制中心”启动

我们已经单独研究了所有系统:设置了“天线”(Fetch),学会了发送“命令”(GETPOSTDELETE),并开发了“紧急协议”(错误处理)。

现在是时候将所有组件整合在一起并启动我们的飞控中心——一个功能齐全、交互式的宇宙飞船队管理界面。

我们的目标:

  • 创建一个统一、整洁、易于理解的界面。
  • 实现完整的 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. Web 界面中的模态窗口是...

2. `DOMContentLoaded` 事件在以下情况下发生...

3. 为什么在最终版本中,我们创建和编辑都使用同一个表单?

4. `data-ship-id="${ship.id}"` 是一个...的例子

5. 代码重构(例如,将逻辑移至 `api.js`)是为了...


🚀 本章总结

您已成功构建并启动您的“飞行控制中心”。

  • 🖥️ 您创建了一个结构化且样式化的 HTML/CSS 界面。
  • ⚙️ 您编写了清晰、模块化的 JavaScript 代码,实现了完整的 CRUD 循环。
  • 🛰️ 您的前端现在可以完全管理使用 FastAPI 构建的后端。

恭喜您成功完成第 4 章! 您已完成了从发送简单 fetch 请求到创建与您自己的 API 交互的完整 Web 应用程序的整个过程。