【前端从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)”组件。

浙公网安备 33010602011771号