【前端从0到1实战】第7篇:构建“多步骤表单向导” (Multi-Step Form)

【前端从0到1实战】第7篇:构建“多步骤表单向导” (Multi-Step Form)

欢迎来到本系列的第三篇。我们即将从“内容展示”组件(如 Tab 和手风琴)毕业,进入一个更高级、更具挑战的领域:“复杂交互与状态管理”。

“多步骤表单”是解决复杂信息收集(如注册流程、结账页面、问卷调查)的最佳方案。它通过将一个庞大的表单分解为多个小步骤,极大地提升了用户体验,减少了用户的心理压力。

本篇我们将手写一个功能完整的表单向导,它将包含:

一个动态的“进度条”。

平滑的“步骤切换”动画。

“上一步”/“下一步”/“提交”的逻辑切换。

第一部分:HTML 结构搭建 (骨架)

一个多步骤表单的结构比之前的组件都要复杂。我们需要:

进度条 (.progress-bar):用于显示当前进度。

表单步骤容器 (.form-steps-container):一个“视口”,它将隐藏所有非当前步骤的面板。

多个步骤面板 (.form-step):包含表单字段的实际内容。

导航按钮 (.form-navigation):用于前进和后退。

<!-- 1. 进度条区域 -->
<!-- 我们使用 data-step 属性来让 JS 识别它们 -->
<div class="progress-bar">
    <div class="progress-step is-active" data-step="1">
        <div class="progress-step-icon">1</div>
        <div class="progress-step-label">账户信息</div>
    </div>
    <div class="progress-line"></div>
    <div class="progress-step" data-step="2">
        <div class="progress-step-icon">2</div>
        <div class="progress-step-label">个人资料</div>
    </div>
    <div class="progress-line"></div>
    <div class="progress-step" data-step="3">
        <div class="progress-step-icon">3</div>
        <div class="progress-step-label">确认</div>
    </div>
</div>

<!-- 2. 表单步骤容器 (实现滑动效果) -->
<!-- 
  .form-viewport 是一个技巧, 
  它固定高度并隐藏溢出, 为内部的 .form-steps-container 提供滑动动画的“舞台”
-->
<div class="form-viewport">
    <div class="form-steps-container">
        <!-- 
          步骤 1 (默认激活)

        <div class="form-step is-active" id="step-1">
            <div class="form-step-header">
                <h2>创建您的账户</h2>
            </div>
            <div class="form-step-body">
                <div class="form-group">
                    <label for="email">邮箱:</label>
                    <input type="email" id="email" required>
                </div>
                <div class="form-group">
                    <label for="password">密码:</label>
                    <input type="password" id="password" required>
                </div>
            </div>
        </div>

        <!-- 步骤 2 -->
        <div class="form-step" id="step-2">
            <div class="form-step-header">
                <h2>完善您的个人资料</h2>
            </div>
            <div class="form-step-body">
                <div class="form-group">
                    <label for="username">用户名:</label>
                    <input type="text" id="username">
                </div>
                <div class="form-group">
                    <label for="phone">电话:</label>
                    <input type="tel" id="phone">
                </div>
            </div>
        </div>

        <!-- 步骤 3 -->
        <div class="form-step" id="step-3">
            <div class="form-step-header">
                <h2>确认信息</h2>
            </div>
            <div class="form-step-body">
                <p>请检查您的信息,然后提交。</p>
                <!-- (这里未来可以动态填入前两步的数据) -->
                <div class="summary-block">
                    <strong>邮箱:</strong> <span data-summary="email">...</span>
                </div>
            </div>
        </div>
    </div>
</div>

<!-- 3. 导航按钮区域 -->
<div class="form-navigation">
    <button id="btn-prev" class="btn btn-secondary" disabled>
        上一步
    </button>
    <button id="btn-next" class="btn btn-primary">
        下一步
    </button>
</div>

第二部分:CSS 样式 (皮肤与动画)

CSS 的挑战有两个:

进度条样式:如何处理 .is-active(当前)和 .is-completed(已完成)两种状态。

面板切换动画:我们将使用 transform: translateX() 来实现平滑的“滑动”效果,而不是生硬的 display: none。

/* --- 基础容器样式 --- /
.form-container {
width: 650px;
max-width: 100%;
margin: 40px auto;
border: 1px solid #dfe4ea;
border-radius: 10px;
box-shadow: 0 5px 20px rgba(0,0,0,0.07);
background-color: #fff;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
/
防止子元素溢出圆角 */
overflow: hidden;
}

/* --- 1. 进度条样式 --- */
.progress-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30px 40px;
background-color: #f8f9fa;
}

.progress-step {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
flex-basis: 100px; /* 每个步骤的基础宽度 */
}

.progress-step-icon {
width: 35px;
height: 35px;
border-radius: 50%;
background-color: #dfe4ea;
color: #57606f;
border: 3px solid #dfe4ea;
display: flex;
justify-content: center;
align-items: center;
font-weight: bold;
transition: all 0.3s ease;
}

.progress-step-label {
margin-top: 8px;
font-size: 14px;
font-weight: 500;
color: #57606f;
transition: all 0.3s ease;
}

.progress-line {
flex-grow: 1;
height: 3px;
background-color: #dfe4ea;
margin: 0 10px;
transition: all 0.3s ease;
}

/* 进度条激活状态 */
.progress-step.is-active .progress-step-icon {
background-color: #fff;
border-color: #007bff;
color: #007bff;
}
.progress-step.is-active .progress-step-label {
color: #007bff;
}

/* 进度条完成状态 */
.progress-step.is-completed .progress-step-icon {
background-color: #007bff;
border-color: #007bff;
color: #fff;
}
.progress-step.is-completed .progress-step-label {
color: #2f3542;
}
.progress-step.is-completed + .progress-line {
background-color: #007bff;
}

/* --- 2. 表单步骤样式 (核心动画) --- */

/* “视口”:固定高度,隐藏溢出 /
.form-viewport {
height: 280px; /
必须指定一个固定高度 */
overflow: hidden;
}

/* “步骤容器”:使用 Flex,宽度是 300% (因为它有3个步骤) /
.form-steps-container {
display: flex;
width: 300%; /
100% * 3 个步骤 */
transition: transform 0.4s cubic-bezier(0.77, 0, 0.175, 1);
}

/* 每个步骤面板,占满容器的 1/3 /
.form-step {
flex-basis: 33.333%;
padding: 20px 40px;
box-sizing: border-box; /
保证 padding 不会破坏 33.333% 的宽度 /
/
(在 CSS 中, .is-active 在这里只用于初始定位) */
}

.form-step-header {
margin-bottom: 25px;
}
.form-step-header h2 {
margin: 0;
color: #2f3542;
}

.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 500;
}
.form-group input {
width: 100%;
padding: 10px 12px;
border: 1px solid #dfe4ea;
border-radius: 5px;
box-sizing: border-box;
font-size: 1em;
}

/* --- 3. 导航按钮样式 --- */
.form-navigation {
display: flex;
justify-content: space-between;
padding: 25px 40px;
background-color: #f8f9fa;
border-top: 1px solid #dfe4ea;
}

.btn {
padding: 10px 25px;
font-size: 1em;
font-weight: 500;
border: none;
border-radius: 5px;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-primary {
background-color: #007bff;
color: #fff;
}
.btn-primary:hover {
background-color: #0056b3;
}
.btn-secondary {
background-color: #dfe4ea;
color: #57606f;
}

/* 按钮的禁用状态 */
.btn:disabled {
background-color: #e9ecef;
color: #adb5bd;
cursor: not-allowed;
}

第三部分:JS 交互逻辑 (状态管理)

这是本文最核心的部分。我们将引入一个状态变量 currentStep,所有的 DOM 更新都围绕这个变量展开。

document.addEventListener('DOMContentLoaded', () => {

// --- 1. DOM 元素获取 ---
// 按钮
const btnNext = document.getElementById('btn-next');
const btnPrev = document.getElementById('btn-prev');

// 步骤面板
const allSteps = document.querySelectorAll('.form-step');
const stepsContainer = document.querySelector('.form-steps-container');

// 进度条
const allProgressSteps = document.querySelectorAll('.progress-step');
const allProgressLines = document.querySelectorAll('.progress-line');

// --- 2. 状态管理 ---
let currentStep = 1; // 当前步骤 (从 1 开始)
const totalSteps = allSteps.length; // 总步骤数

// --- 3. 核心功能函数:更新表单状态 ---
// 这是一个“中央控制器”,所有状态变化都由它分发
const updateFormState = () => {
    
    // --- (A) 更新步骤面板的“滑动” ---
    // currentStep 是 1, 偏移 0%
    // currentStep 是 2, 偏移 -33.333% (100 / 3)
    // currentStep 是 3, 偏移 -66.666% (200 / 3)
    const offsetPercentage = -((currentStep - 1) * (100 / totalSteps));
    stepsContainer.style.transform = `translateX(${offsetPercentage}%)`;

    // --- (B) 更新进度条 ---
    allProgressSteps.forEach((step, index) => {
        const stepNum = index + 1;
        
        if (stepNum === currentStep) {
            // 标记为“当前”
            step.classList.add('is-active');
            step.classList.remove('is-completed');
        } else if (stepNum < currentStep) {
            // 标记为“已完成”
            step.classList.add('is-completed');
            step.classList.remove('is-active');
        } else {
            // 标记为“未开始”
            step.classList.remove('is-active');
            step.classList.remove('is-completed');
        }
    });

    // --- (C) 更新导航按钮 ---
    if (currentStep === 1) {
        // 第一步:禁用“上一步”
        btnPrev.disabled = true;
    } else {
        btnPrev.disabled = false;
    }

    if (currentStep === totalSteps) {
        // 最后一步:将“下一步”变为“提交”
        btnNext.textContent = '提交';
    } else {
        btnNext.textContent = '下一步';
    }
};

// --- 4. 事件绑定 ---

// “下一步”按钮点击
btnNext.addEventListener('click', () => {
    if (currentStep < totalSteps) {
        // 增加步骤
        currentStep++;
        // 更新 UI
        updateFormState();
    } else {
        // 已经是最后一步 (“提交”按钮)
        alert('表单已提交!');
        // (在这里可以添加真正的表单提交
        //  例如:document.getElementById("multi-step-form").submit()
        //  或使用 fetch API)
    }
});

// “上一步”按钮点击
btnPrev.addEventListener('click', () => {
    if (currentStep > 1) {
        // 减少步骤
        currentStep--;
        // 更新 UI
        updateFormState();
    }
});

// --- 5. 初始化 ---
// 页面加载时,立即执行一次,以确保 UI 处于正确的第一步状态
updateFormState();

});

总结

恭喜!我们完成了一个复杂的、带状态的组件。

我们学到了:

HTML: 如何为“状态驱动”的 UI 搭建骨架(进度条、视口、面板、按钮)。

CSS:

如何使用 transform: translateX() 结合 overflow: hidden 来实现平滑的滑动动画。

如何为进度条定义 .is-active 和 .is-completed 两种不同的激活状态。

JS (核心):

如何使用一个状态变量 (currentStep) 来驱动所有 UI 变化。

如何编写一个中央更新函数 (updateFormState),将“状态”同步到 DOM,这是解耦的
关键。

在下一篇文章中,我们将继续探索动画,挑战“轮播图 (Carousel)”组件。

posted @ 2025-11-17 15:51  GreenBoos2025  阅读(3)  评论(0)    收藏  举报