【前端从0到1实战】第7篇:构建“多步骤表单向导” (Multi-Step Form)
【前端从0到1实战】第7篇:构建“多步骤表单向导” (Multi-Step Form)
欢迎来到本系列的第三篇。我们即将从“内容展示”组件(如 Tab 和手风琴)毕业,进入一个更高级、更具挑战的领域:“复杂交互与状态管理”。
“多步骤表单”是解决复杂信息收集(如注册流程、结账页面、问卷调查)的最佳方案。它通过将一个庞大的表单分解为多个小步骤,极大地提升了用户体验,减少了用户的心理压力。
本篇我们将手写一个功能完整的表单向导,它将包含:
一个动态的“进度条”。
平滑的“步骤切换”动画。
“上一步”/“下一步”/“提交”的逻辑切换。
第一部分:HTML 结构搭建 (骨架)
一个多步骤表单的结构比之前的组件都要复杂。我们需要:
进度条 (.progress-bar):用于显示当前进度。
表单步骤容器 (.form-steps-container):一个“视口”,它将隐藏所有非当前步骤的面板。
多个步骤面板 (.form-step):包含表单字段的实际内容。
导航按钮 (.form-navigation):用于前进和后退。
第二部分: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号