运用 HTML, CSS 和 JavaScript 构建一个响应式管理仪表板
管理仪表板 (Admin Dashboard) 是现代 Web 应用程序中一个常见且至关重要的组成部分。它提供了一个集中化的界面,用于监控、管理和展示关键数据和操作,例如用户统计、销售报告、系统状态或内容管理。一个优秀的仪表板不仅需要功能强大,更需要具备出色的用户体验,其中响应式设计是关键。这意味着无论用户是在桌面电脑、平板电脑还是手机上访问,仪表板的布局和功能都能优雅地适应不同屏幕尺寸。
本教程将引导您一步步实现这个功能。我们将从 HTML 结构开始,设计一个包含侧边导航栏、顶部标题栏和主内容区域的典型仪表板布局;然后用 CSS 美化应用的界面,使其具有专业、现代的外观,并通过 CSS Grid 和 Flexbox 实现强大的响应式布局;最后,我们将使用 JavaScript 赋予它“智能”,使其能够处理用户交互,例如在小屏幕设备上切换侧边导航栏的显示/隐藏,并展示如何通过 JavaScript 动态调整 CSS 变量。这个项目是您巩固 HTML、CSS 和 JavaScript 基础知识,以及深入理解高级 CSS 布局(Grid, Flexbox)和响应式设计哲学的绝佳实践。
前置知识
为了更好地理解本指南,建议您具备以下基础知识:
- HTML 基础: 了解语义化标签 (
<header>,<aside>,<main>,<div>,<nav>,<ul>,<li>,<button>),以及如何构建页面结构和使用id,class属性。 - CSS 基础: 了解选择器、属性、值,盒模型、文本样式、颜色、背景、边框、阴影等。对 Flexbox 和 CSS Grid 有基本了解将非常有帮助。了解
@media查询进行响应式设计。 - JavaScript 基础: 了解变量、函数、基本的条件语句,以及如何进行 DOM 操作(
getElementById,querySelector,classList.toggle)和事件处理(addEventListener)。
目录
- 项目概览与目标
- HTML 结构:构建应用的骨架
- 2.1 创建
index.html文件 - 2.2 代码解释
- 2.1 创建
- CSS 样式:美化应用界面与实现响应式布局
- 3.1 创建
style.css文件 - 3.2 代码解释
- 3.3 CSS 趣闻:
CSS Grid与Flexbox:双剑合璧构建复杂布局
- 3.1 创建
- JavaScript 逻辑:赋予应用交互性
- 4.1 创建
script.js文件 - 4.2 代码解释
- 4.3 JS 趣闻:JavaScript 与
CSS Custom Properties(CSS 变量) 的动态交互
- 4.1 创建
- 将所有文件连接起来
- 最终代码展示
- 拓展与改进
- 总结
- 附录:常见问题
1. 项目概览与目标
我们的目标是创建一个响应式管理仪表板,它具备以下主要特性:
- 侧边导航栏 (Sidebar): 包含 Logo 和导航链接。在桌面视图下常驻,在小屏幕下可折叠/展开。
- 顶部标题栏 (Header): 包含仪表板名称、菜单切换按钮(在小屏幕下显示)、用户头像/信息等。
- 主内容区域 (Main Content): 包含各种数据卡片 (Cards) 和一个数据表格。
- 响应式布局:
- 在桌面视图下,侧边栏和主内容区域并排显示。
- 在平板/手机视图下,侧边栏会隐藏,通过点击菜单按钮可以从左侧滑出。主内容区域自动调整宽度,卡片堆叠显示,表格适应性调整。
- 交互性: 点击菜单按钮可以切换侧边栏的显示状态。
预期效果图(文本描述):
桌面视图 (大屏幕):
- 整个页面是一个两栏布局。
- 左侧: 一个固定宽度的深色侧边栏。顶部是 Logo,下方是一系列导航链接(例如:仪表板、用户、设置、报告),链接有图标和文本,并有悬停效果。
- 右侧: 占据剩余宽度的主内容区域。
- 顶部: 一个浅色标题栏,显示“仪表板”标题和右侧的用户头像/名称。
- 下方: 主内容区,包含:
- 一行(或多行)多个数据卡片,水平排列。每个卡片显示一个关键指标(例如:总用户、销售额),有标题、数值和简单的图标,带有阴影效果。
- 一个数据表格,显示更详细的列表信息,具有表头和多行数据,表格可能带有斑马纹或悬停效果。
移动视图 (小屏幕):
- 整个页面是单栏布局。
- 顶部: 一个浅色标题栏,显示“仪表板”标题,左侧有一个“三道杠”菜单按钮,右侧是用户头像/名称。
- 侧边栏: 初始隐藏。点击菜单按钮后,侧边栏从左侧滑出,覆盖一部分主内容。侧边栏与桌面版类似,但可能更窄,且通常会带有一个关闭按钮。
- 主内容区:
- 数据卡片垂直堆叠显示,占据整个宽度。
- 数据表格也适应小屏幕,可能只显示关键列,或者通过
data-label技巧进行优化。
2. HTML 结构:构建应用的骨架
首先,我们需要创建 HTML 文件来定义仪表板的基本结构。
2.1 创建 index.html 文件
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>响应式管理仪表板</title>
<!-- 引入自定义 CSS 文件 -->
<link rel="stylesheet" href="style.css">
<!-- 引入 Font Awesome 图标库 (CDN) -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
</head>
<body>
<div class="dashboard-container">
<!-- 侧边导航栏 -->
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
<a href="#" class="logo">AdminPro</a>
<button class="menu-toggle close-btn" id="sidebarCloseBtn">
<i class="fas fa-times"></i>
</button>
</div>
<nav class="sidebar-nav">
<ul>
<li><a href="#" class="active"><i class="fas fa-th-large"></i> <span>仪表板</span></a></li>
<li><a href="#"><i class="fas fa-users"></i> <span>用户管理</span></a></li>
<li><a href="#"><i class="fas fa-cog"></i> <span>设置</span></a></li>
<li><a href="#"><i class="fas fa-chart-line"></i> <span>报告</span></a></li>
<li><a href="#"><i class="fas fa-box"></i> <span>产品</span></a></li>
<li><a href="#"><i class="fas fa-sign-out-alt"></i> <span>登出</span></a></li>
</ul>
</nav>
</aside>
<!-- 主内容区域 -->
<main class="main-content" id="mainContent">
<!-- 顶部标题栏 -->
<header class="main-header">
<button class="menu-toggle open-btn" id="menuToggleBtn">
<i class="fas fa-bars"></i>
</button>
<h2 class="page-title">仪表板总览</h2>
<div class="user-info">
<img src="https://via.placeholder.com/40" alt="User Avatar" class="user-avatar">
<span>张三</span>
</div>
</header>
<!-- 仪表板卡片 -->
<section class="dashboard-cards">
<div class="card">
<div class="card-icon"><i class="fas fa-user-plus"></i></div>
<div class="card-content">
<h3>新用户</h3>
<p class="card-value">1,250</p>
<span class="card-trend up">↑ 12%</span>
</div>
</div>
<div class="card">
<div class="card-icon"><i class="fas fa-dollar-sign"></i></div>
<div class="card-content">
<h3>总销售额</h3>
<p class="card-value">¥85,300</p>
<span class="card-trend up">↑ 8%</span>
</div>
</div>
<div class="card">
<div class="card-icon"><i class="fas fa-chart-bar"></i></div>
<div class="card-content">
<h3>流量</h3>
<p class="card-value">45,789</p>
<span class="card-trend down">↓ 3%</span>
</div>
</div>
<div class="card">
<div class="card-icon"><i class="fas fa-tasks"></i></div>
<div class="card-content">
<h3>待处理任务</h3>
<p class="card-value">15</p>
<span class="card-trend neutral">保持稳定</span>
</div>
</div>
</section>
<!-- 数据表格 -->
<section class="data-table-section">
<h2>最新订单</h2>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>订单ID</th>
<th>客户</th>
<th>商品</th>
<th>数量</th>
<th>总价</th>
<th>状态</th>
</tr>
</thead>
<tbody>
<tr>
<td data-label="订单ID">#001234</td>
<td data-label="客户">李四</td>
<td data-label="商品">笔记本电脑</td>
<td data-label="数量">1</td>
<td data-label="总价">¥8,999</td>
<td data-label="状态" class="status-pending">待处理</td>
</tr>
<tr>
<td data-label="订单ID">#001235</td>
<td data-label="客户">王五</td>
<td data-label="商品">智能手机</td>
<td data-label="数量">2</td>
<td data-label="总价">¥7,998</td>
<td data-label="状态" class="status-completed">已完成</td>
</tr>
<tr>
<td data-label="订单ID">#001236</td>
<td data-label="客户">赵六</td>
<td data-label="商品">无线耳机</td>
<td data-label="数量">3</td>
<td data-label="总价">¥1,500</td>
<td data-label="状态" class="status-cancelled">已取消</td>
</tr>
<tr>
<td data-label="订单ID">#001237</td>
<td data-label="客户">钱七</td>
<td data-label="商品">智能手表</td>
<td data-label="数量">1</td>
<td data-label="总价">¥2,499</td>
<td data-label="状态" class="status-pending">待处理</td>
</tr>
<tr>
<td data-label="订单ID">#001238</td>
<td data-label="客户">孙八</td>
<td data-label="商品">便携音箱</td>
<td data-label="数量">1</td>
<td data-label="总价">¥699</td>
<td data-label="状态" class="status-completed">已完成</td>
</tr>
</tbody>
</table>
</div>
</section>
</main>
</div>
<!-- 引入 JavaScript 文件,defer 属性确保 HTML 解析完成后再执行 -->
<script src="script.js" defer></script>
</body>
</html>
2.2 代码解释
<!DOCTYPE html>,<html>,<head>,<body>: 标准的 HTML5 结构。<link rel="stylesheet" href="style.css">: 关联自定义 CSS 样式文件。<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">: 引入 Font Awesome 图标库,用于显示小图标,如菜单图标、导航链接图标等。<div class="dashboard-container">: 整体容器。 这是整个仪表板的最高层容器,我们将使用 CSS Grid 在这里定义页面的主布局(侧边栏 + 主内容)。- 侧边导航栏 (
<aside class="sidebar" id="sidebar">):sidebar-header: 包含 Logo 和一个在移动设备上显示的“关闭”按钮 (sidebarCloseBtn)。sidebar-nav: 导航链接列表 (<ul>,<li>,<a>和 Font Awesome 图标<i>)。active类用于高亮当前选中的链接。
- 主内容区域 (
<main class="main-content" id="mainContent">):main-header: 顶部标题栏。menu-toggle open-btn: 在移动设备上显示的“打开”菜单按钮 (menuToggleBtn)。page-title: 页面标题。user-info: 显示用户头像和名称。
dashboard-cards: 仪表板卡片区域。.card: 每个卡片都有图标、标题、数值和趋势指示。.card-trend.up/down/neutral用于显示趋势方向和颜色。
data-table-section: 数据表格区域。table-responsive: 一个包装器div,用于在小屏幕上处理表格的响应式行为。data-table: 实际的数据表格,包含<thead>(表头) 和<tbody>(表体)。data-label="...": 重要! 为表格的每个<td>元素添加了data-label属性。这在移动设备上非常有用,当表格变为堆叠布局时,CSS 可以利用::before伪元素显示这些标签,让用户知道每个数据代表什么。status-pending/completed/cancelled: 用于给状态文本添加不同颜色的类。
<script src="script.js" defer></script>: 关联 JavaScript 文件。defer属性确保 HTML 解析完成后再执行脚本。
3. CSS 样式:美化应用界面与实现响应式布局
现在,我们来创建 style.css 文件,为仪表板添加美观的视觉效果,并通过 CSS Grid 和 @media 查询实现响应式布局。
3.1 创建 style.css 文件
/* style.css */
/* 定义 CSS 变量 (Custom Properties) */
:root {
--sidebar-width: 250px;
--sidebar-bg: #2c3e50; /* 深蓝色 */
--header-height: 70px;
--primary-color: #3498db; /* 浅蓝色 */
--text-color: #333;
--light-text-color: #f0f2f5;
--card-bg: #ffffff;
--card-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
--border-color: #eee;
--transition-speed: 0.3s ease;
}
/* 全局样式和重置 */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
color: var(--text-color);
background-color: var(--light-text-color);
line-height: 1.6;
}
a {
text-decoration: none;
color: var(--primary-color);
}
ul {
list-style: none;
}
/* 仪表板容器 - 使用 CSS Grid 进行主布局 */
.dashboard-container {
display: grid;
/* 定义网格区域:sidebar 和 main */
grid-template-columns: var(--sidebar-width) 1fr; /* 侧边栏固定宽度,主内容区占满剩余 */
grid-template-rows: 1fr; /* 单行 */
grid-template-areas: "sidebar main";
min-height: 100vh;
}
/* 侧边导航栏 */
.sidebar {
grid-area: sidebar;
background-color: var(--sidebar-bg);
color: var(--light-text-color);
padding: 20px 0;
display: flex;
flex-direction: column;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
transition: transform var(--transition-speed);
}
.sidebar-header {
text-align: center;
margin-bottom: 30px;
padding: 0 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.sidebar-header .logo {
color: white;
font-size: 1.8em;
font-weight: 700;
}
/* 移动端侧边栏的关闭按钮 (默认隐藏) */
.sidebar-header .close-btn {
display: none;
background: none;
border: none;
color: white;
font-size: 1.5em;
cursor: pointer;
}
.sidebar-nav ul li a {
display: flex;
align-items: center;
padding: 15px 20px;
color: var(--light-text-color);
font-size: 1.05em;
transition: background-color var(--transition-speed), color var(--transition-speed);
}
.sidebar-nav ul li a:hover,
.sidebar-nav ul li a.active {
background-color: var(--primary-color);
color: white;
}
.sidebar-nav ul li a i {
margin-right: 15px;
width: 20px; /* 确保图标宽度一致 */
text-align: center;
}
/* 主内容区域 */
.main-content {
grid-area: main;
display: flex;
flex-direction: column;
background-color: #f8f9fa;
}
/* 顶部标题栏 */
.main-header {
background-color: var(--card-bg);
height: var(--header-height);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 25px;
box-shadow: var(--card-shadow);
z-index: 10;
}
/* 移动端菜单切换按钮 (默认隐藏) */
.main-header .open-btn {
display: none;
background: none;
border: none;
color: var(--text-color);
font-size: 1.5em;
cursor: pointer;
}
.page-title {
font-size: 1.8em;
color: var(--sidebar-bg);
}
.user-info {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
color: var(--text-color);
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
border: 2px solid var(--primary-color);
}
/* 仪表板卡片区域 */
.dashboard-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); /* 响应式卡片布局 */
gap: 25px;
padding: 25px;
flex-wrap: wrap;
}
.card {
background-color: var(--card-bg);
padding: 25px;
border-radius: 10px;
box-shadow: var(--card-shadow);
display: flex;
align-items: center;
gap: 20px;
transition: transform 0.2s ease;
}
.card:hover {
transform: translateY(-5px);
}
.card-icon {
font-size: 2.5em;
color: var(--primary-color);
}
.card-content h3 {
font-size: 1.1em;
color: #666;
margin-bottom: 5px;
}
.card-content .card-value {
font-size: 2em;
font-weight: 700;
color: var(--sidebar-bg);
margin-bottom: 5px;
}
.card-content .card-trend {
font-size: 0.9em;
font-weight: 600;
}
.card-trend.up { color: #28a745; } /* 绿色 */
.card-trend.down { color: #dc3545; } /* 红色 */
.card-trend.neutral { color: #ffc107; } /* 黄色 */
/* 数据表格区域 */
.data-table-section {
padding: 0 25px 25px;
flex-grow: 1; /* 占据剩余垂直空间 */
}
.data-table-section h2 {
font-size: 1.8em;
color: var(--sidebar-bg);
margin-bottom: 20px;
text-align: left;
}
.table-responsive {
overflow-x: auto; /* 允许表格在小屏幕上横向滚动 */
background-color: var(--card-bg);
border-radius: 10px;
box-shadow: var(--card-shadow);
}
.data-table {
width: 100%;
border-collapse: collapse;
min-width: 600px; /* 确保表格不会太窄 */
}
.data-table th,
.data-table td {
padding: 15px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
.data-table thead th {
background-color: var(--primary-color);
color: white;
font-weight: 600;
text-transform: uppercase;
font-size: 0.9em;
}
.data-table tbody tr:hover {
background-color: #f0f8ff; /* 悬停时浅蓝色 */
}
.data-table tbody tr:nth-child(even) {
background-color: #fdfdfd; /* 斑马纹 */
}
.data-table tbody tr:last-child td {
border-bottom: none;
}
/* 表格状态徽章 */
.status-pending { color: #ffc107; font-weight: 600; } /* 黄色 */
.status-completed { color: #28a745; font-weight: 600; } /* 绿色 */
.status-cancelled { color: #dc3545; font-weight: 600; } /* 红色 */
/* 响应式设计 */
/* 小于 1024px 时的调整 (平板) */
@media (max-width: 1024px) {
.dashboard-container {
grid-template-columns: 1fr; /* 单列布局 */
grid-template-areas: "main"; /* 侧边栏不再是网格区域 */
}
.sidebar {
position: fixed; /* 固定定位 */
top: 0;
left: 0;
width: var(--sidebar-width);
height: 100%;
z-index: 1000; /* 确保在最上层 */
transform: translateX(-100%); /* 默认隐藏到左侧 */
box-shadow: 4px 0 15px rgba(0, 0, 0, 0.2);
}
.sidebar.active {
transform: translateX(0); /* 显示侧边栏 */
}
.sidebar-header .close-btn {
display: block; /* 显示关闭按钮 */
}
.main-header .open-btn {
display: block; /* 显示打开菜单按钮 */
}
.main-header .page-title {
font-size: 1.5em; /* 缩小标题 */
}
.dashboard-cards {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); /* 卡片适应更窄的宽度 */
padding: 20px;
gap: 20px;
}
.card {
padding: 20px;
flex-direction: column; /* 卡片内容垂直堆叠 */
align-items: flex-start;
text-align: left;
}
.card-icon { margin-bottom: 10px; }
}
/* 小于 768px 时的调整 (手机) */
@media (max-width: 768px) {
.main-header {
padding: 0 15px;
}
.main-header .page-title {
font-size: 1.2em; /* 进一步缩小标题 */
}
.user-info span {
display: none; /* 隐藏用户名,只显示头像 */
}
.dashboard-cards {
padding: 15px;
gap: 15px;
grid-template-columns: 1fr; /* 单列卡片布局 */
}
.card-content .card-value {
font-size: 1.8em;
}
.data-table-section {
padding: 0 15px 15px;
}
.data-table-section h2 {
font-size: 1.5em;
}
/* 响应式表格:堆叠行 */
.data-table thead {
display: none; /* 隐藏表头 */
}
.data-table,
.data-table tbody,
.data-table tr,
.data-table td {
display: block; /* 使表格元素块级显示 */
width: 100%;
}
.data-table tr {
margin-bottom: 15px;
border: 1px solid var(--border-color);
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
}
.data-table td {
text-align: right;
padding-left: 50%; /* 为标签留出空间 */
position: relative;
border-bottom: 1px solid #eee;
}
.data-table td:last-child {
border-bottom: none;
}
.data-table td::before {
content: attr(data-label); /* 使用 data-label 显示列名 */
position: absolute;
left: 15px;
width: calc(50% - 30px);
padding-right: 10px;
white-space: nowrap;
text-align: left;
font-weight: bold;
color: #555;
}
}
3.2 代码解释
--CSS 变量 (Custom Properties): 在:root中定义了一系列 CSS 变量,用于存储颜色、尺寸等常用值。这使得样式管理更加灵活和模块化,便于主题切换和统一修改。body和a,ul样式: 设置基础字体、颜色和链接样式。.dashboard-container(CSS Grid 核心):display: grid;: 启用 Grid 布局。grid-template-columns: var(--sidebar-width) 1fr;: 定义两列。第一列的宽度是var(--sidebar-width)(250px),第二列 (main) 占据剩余所有空间 (1fr)。grid-template-areas: "sidebar main";: 定义网格区域的命名,使布局更具可读性。
.sidebar样式:grid-area: sidebar;: 将此元素分配到名为sidebar的网格区域。- 设置背景色、内边距、字体颜色和阴影。
- 移动端隐藏/显示:
transform: translateX(-100%);和transform: translateX(0);配合transition用于实现侧边栏滑入滑出的动画。position: fixed和z-index: 1000确保在小屏幕下侧边栏能够覆盖其他内容。
.main-content样式:grid-area: main;: 将此元素分配到名为main的网格区域。display: flex; flex-direction: column;: 使内部元素(header, cards, table)垂直堆叠。
.main-header样式:- 使用 Flexbox (
display: flex; justify-content: space-between; align-items: center;) 将标题、菜单按钮和用户信息水平排列并两端对齐。 open-btn(打开菜单) 按钮默认隐藏,在小屏幕下才显示。
- 使用 Flexbox (
.dashboard-cards样式 (CSS Grid for cards):display: grid;: 启用 Grid 布局。grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));: 核心响应式卡片布局! 允许 Grid 自动创建尽可能多的列,每列的最小宽度是 220px,最大宽度是1fr(平均分配剩余空间)。这使得卡片在不同屏幕尺寸下自动调整数量和大小。gap: 卡片之间的间距。
.card样式: 美化单个卡片的外观,包括背景、内边距、圆角、阴影和图标/内容的布局(使用 Flexbox)。.data-table-section和.data-table样式:- 设置表格容器和表格本身的基础样式,包括背景、阴影、边框合并、内边距等。
overflow-x: auto;在table-responsive上,确保当表格内容过宽时,用户可以横向滚动查看,而不是破坏布局。min-width: 600px;确保表格在桌面端有足够的宽度。
- 表格状态类 (
.status-pending, etc.): 为表格中的不同状态添加不同的颜色,增强可读性。 @media (max-width: 1024px)(平板布局):grid-template-columns: 1fr; grid-template-areas: "main";: 仪表板容器变为单列布局,侧边栏不再作为网格区域的一部分。.sidebar:position: fixed; transform: translateX(-100%);默认隐藏侧边栏,使其通过动画滑出。.sidebar.active: 当 JavaScript 添加active类时,侧边栏显示。close-btn和open-btn显示。- 卡片布局调整:
grid-template-columns再次调整,卡片内容 (.card) 变为垂直堆叠 (flex-direction: column;)。
@media (max-width: 768px)(手机布局):- 进一步缩小标题,隐藏用户名。
- 卡片布局变为单列 (
grid-template-columns: 1fr;)。 - 响应式表格 (
.data-table) 核心技巧:thead { display: none; }: 隐藏表格头部,节省空间。table,tbody,tr,td { display: block; width: 100%; }: 将表格的所有元素强制变为块级元素,使每行数据垂直堆叠。td::before { content: attr(data-label); ... }: 利用data-label属性和::before伪元素,在每个数据单元格前显示其对应的列名,即使表头隐藏了,用户也能理解数据含义。
4. JavaScript 逻辑:赋予应用交互性
最后,我们来创建 script.js 文件,实现侧边栏的切换功能。
4.1 创建 script.js 文件
// script.js
// 1. 获取所有需要操作的 DOM 元素
const sidebar = document.getElementById('sidebar');
const menuToggleBtn = document.getElementById('menuToggleBtn'); // 主内容区打开侧边栏的按钮
const sidebarCloseBtn = document.getElementById('sidebarCloseBtn'); // 侧边栏内部的关闭按钮
const mainContent = document.getElementById('mainContent');
// 2. 函数:切换侧边栏的显示/隐藏状态
function toggleSidebar() {
sidebar.classList.toggle('active'); // 切换 'active' 类
// 在桌面视图下,我们不需要 mainContent 调整,只在移动端需要
// 对于小屏幕,侧边栏是浮动的,不影响 mainContent 布局
// 对于大屏幕,侧边栏是网格的一部分,也不需要 JS 调整
// 因此这里不需要对 mainContent 做额外的 CSS 属性修改
}
// 3. 函数:关闭侧边栏 (主要用于移动端)
function closeSidebar() {
sidebar.classList.remove('active'); // 移除 'active' 类
}
// 4. 添加事件监听器
// 点击主内容区顶部的菜单按钮,切换侧边栏
menuToggleBtn.addEventListener('click', toggleSidebar);
// 点击侧边栏内部的关闭按钮,关闭侧边栏
sidebarCloseBtn.addEventListener('click', closeSidebar);
// (可选) 点击主内容区域任意处,如果侧边栏打开,则关闭它 (仅在移动端有效)
mainContent.addEventListener('click', (event) => {
// 检查侧边栏是否处于活动状态,并且点击不是来自菜单按钮本身
if (sidebar.classList.contains('active') && event.target !== menuToggleBtn && !menuToggleBtn.contains(event.target)) {
// 进一步判断是否点击在侧边栏外部,且当前视口宽度小于1024px (移动端)
// 避免在桌面端误触
if (window.innerWidth <= 1024) {
closeSidebar();
}
}
});
// 5. 初始状态处理 (当窗口大小改变时,确保侧边栏状态正确)
function handleResize() {
if (window.innerWidth > 1024) {
// 如果是桌面视图,确保侧边栏是显示的 (移除 active 类,因为桌面版是网格布局,不需要 active 类来显示)
sidebar.classList.remove('active');
} else {
// 如果是移动视图,确保侧边栏是隐藏的
sidebar.classList.remove('active');
}
}
window.addEventListener('resize', handleResize); // 监听窗口大小变化
window.addEventListener('load', handleResize); // 页面加载时也执行一次
4.2 代码解释
- 获取 DOM 元素: 获取侧边栏 (
sidebar)、主内容区打开按钮 (menuToggleBtn) 和侧边栏关闭按钮 (sidebarCloseBtn) 的引用。 toggleSidebar()函数:sidebar.classList.toggle('active');: 这是核心逻辑。classList.toggle()方法会检查元素是否包含指定的类名 (active)。如果包含,则移除它;如果不包含,则添加它。这使得每次调用此函数都能切换侧边栏的显示状态,通过 CSS 中定义的transform动画实现平滑过渡。
closeSidebar()函数: 明确移除active类,用于侧边栏内部的关闭按钮。- 事件监听器:
menuToggleBtn.addEventListener('click', toggleSidebar);: 当点击主内容区顶部 (移动端) 的菜单图标时,调用toggleSidebar。sidebarCloseBtn.addEventListener('click', closeSidebar);: 当点击侧边栏内部 (移动端) 的关闭图标时,调用closeSidebar。- (可选)
mainContent.addEventListener('click', ...): 这是一个用户体验优化。当侧边栏在移动端打开时,如果用户点击了侧边栏以外的主内容区域,就会关闭侧边栏。这里增加了event.target !== menuToggleBtn && !menuToggleBtn.contains(event.target)的判断,以避免点击菜单按钮本身时触发关闭。并且只在window.innerWidth <= 1024(即移动/平板视图) 时才生效,防止在桌面端意外关闭。
handleResize()和window.addEventListener('resize', handleResize):- 这个函数用于在浏览器窗口大小发生变化时,确保侧边栏的显示状态与当前屏幕尺寸匹配。
- 当
window.innerWidth > 1024px(桌面视图) 时,侧边栏应该始终是可见的,并且不应该有active类(因为桌面端是通过 Grid 布局直接显示的)。所以我们移除active类。 - 当
window.innerWidth <= 1024px(移动/平板视图) 时,侧边栏默认应该是隐藏的,所以也移除active类,确保它在切换到移动视图时是关闭状态。 window.addEventListener('load', handleResize);: 确保页面加载时也执行一次handleResize,以设置正确的初始状态。
4.3 JS 趣闻:JavaScript 与 CSS Custom Properties (CSS 变量) 的动态交互
CSS Custom Properties的强大之处:- 趣闻: CSS 变量(
--my-variable: value;)不仅让 CSS 样式更易于维护和主题化,它们还是一个强大的桥梁,连接了 CSS 和 JavaScript。JavaScript 可以直接读取和修改这些 CSS 变量的值。
- 趣闻: CSS 变量(
- 如何用 JavaScript 交互:
- 读取变量:
const rootStyles = getComputedStyle(document.documentElement); // 获取 :root 元素的计算样式 const sidebarWidth = rootStyles.getPropertyValue('--sidebar-width'); // 读取 --sidebar-width console.log(sidebarWidth); // 输出 "250px" - 设置变量:
document.documentElement.style.setProperty('--sidebar-width', '300px'); // 将侧边栏宽度设为 300px // 或者针对特定元素: // sidebar.style.setProperty('--sidebar-bg', '#ff0000'); // 改变侧边栏背景色
- 读取变量:
- 在仪表板中的应用场景:
- 动态主题切换: 用户可以选择“亮色模式”或“暗色模式”。JavaScript 可以通过修改
--primary-color,--sidebar-bg,--text-color等变量来实时切换整个仪表板的主题,而无需修改大量的 CSS 规则。 - 用户偏好设置: 允许用户自定义侧边栏宽度。JavaScript 读取用户设置,然后通过
setProperty()更新--sidebar-width变量。 - 响应式调试: 在开发时,JavaScript 可以根据某些条件动态调整布局相关的 CSS 变量,便于测试不同状态下的表现。
- 动态主题切换: 用户可以选择“亮色模式”或“暗色模式”。JavaScript 可以通过修改
- 本教程的简化: 虽然本教程主要通过
classList.toggle()切换侧边栏的active类来实现响应式,但了解 JavaScript 与 CSS 变量的交互,能为仪表板的进一步定制和高级动态功能(如用户自定义主题、动态调整布局参数等)打开大门。
5. 将所有文件连接起来
确保您的项目文件夹结构如下:
your-admin-dashboard-project/
├── index.html
├── style.css
└── script.js
然后,用浏览器打开 index.html 文件(可以直接双击,或在 VS Code 中使用 “Open with Live Server” 插件),您应该能看到一个功能完善且响应式的管理仪表板应用!尝试调整浏览器窗口大小,观察布局和侧边栏行为的变化。
6. 最终代码展示
index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>响应式管理仪表板</title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
</head>
<body>
<div class="dashboard-container">
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
<a href="#" class="logo">AdminPro</a>
<button class="menu-toggle close-btn" id="sidebarCloseBtn">
<i class="fas fa-times"></i>
</button>
</div>
<nav class="sidebar-nav">
<ul>
<li><a href="#" class="active"><i class="fas fa-th-large"></i> <span>仪表板</span></a></li>
<li><a href="#"><i class="fas fa-users"></i> <span>用户管理</span></a></li>
<li><a href="#"><i class="fas fa-cog"></i> <span>设置</span></a></li>
<li><a href="#"><i class="fas fa-chart-line"></i> <span>报告</span></a></li>
<li><a href="#"><i class="fas fa-box"></i> <span>产品</span></a></li>
<li><a href="#"><i class="fas fa-sign-out-alt"></i> <span>登出</span></a></li>
</ul>
</nav>
</aside>
<main class="main-content" id="mainContent">
<header class="main-header">
<button class="menu-toggle open-btn" id="menuToggleBtn">
<i class="fas fa-bars"></i>
</button>
<h2 class="page-title">仪表板总览</h2>
<div class="user-info">
<img src="https://via.placeholder.com/40" alt="User Avatar" class="user-avatar">
<span>张三</span>
</div>
</header>
<section class="dashboard-cards">
<div class="card">
<div class="card-icon"><i class="fas fa-user-plus"></i></div>
<div class="card-content">
<h3>新用户</h3>
<p class="card-value">1,250</p>
<span class="card-trend up">↑ 12%</span>
</div>
</div>
<div class="card">
<div class="card-icon"><i class="fas fa-dollar-sign"></i></div>
<div class="card-content">
<h3>总销售额</h3>
<p class="card-value">¥85,300</p>
<span class="card-trend up">↑ 8%</span>
</div>
</div>
<div class="card">
<div class="card-icon"><i class="fas fa-chart-bar"></i></div>
<div class="card-content">
<h3>流量</h3>
<p class="card-value">45,789</p>
<span class="card-trend down">↓ 3%</span>
</div>
</div>
<div class="card">
<div class="card-icon"><i class="fas fa-tasks"></i></div>
<div class="card-content">
<h3>待处理任务</h3>
<p class="card-value">15</p>
<span class="card-trend neutral">保持稳定</span>
</div>
</div>
</section>
<section class="data-table-section">
<h2>最新订单</h2>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>订单ID</th>
<th>客户</th>
<th>商品</th>
<th>数量</th>
<th>总价</th>
<th>状态</th>
</tr>
</thead>
<tbody>
<tr>
<td data-label="订单ID">#001234</td>
<td data-label="客户">李四</td>
<td data-label="商品">笔记本电脑</td>
<td data-label="数量">1</td>
<td data-label="总价">¥8,999</td>
<td data-label="状态" class="status-pending">待处理</td>
</tr>
<tr>
<td data-label="订单ID">#001235</td>
<td data-label="客户">王五</td>
<td data-label="商品">智能手机</td>
<td data-label="数量">2</td>
<td data-label="总价">¥7,998</td>
<td data-label="状态" class="status-completed">已完成</td>
</tr>
<tr>
<td data-label="订单ID">#001236</td>
<td data-label="客户">赵六</td>
<td data-label="商品">无线耳机</td>
<td data-label="数量">3</td>
<td data-label="总价">¥1,500</td>
<td data-label="状态" class="status-cancelled">已取消</td>
</tr>
<tr>
<td data-label="订单ID">#001237</td>
<td data-label="客户">钱七</td>
<td data-label="商品">智能手表</td>
<td data-label="数量">1</td>
<td data-label="总价">¥2,499</td>
<td data-label="状态" class="status-pending">待处理</td>
</tr>
<tr>
<td data-label="订单ID">#001238</td>
<td data-label="客户">孙八</td>
<td data-label="商品">便携音箱</td>
<td data-label="数量">1</td>
<td data-label="总价">¥699</td>
<td data-label="状态" class="status-completed">已完成</td>
</tr>
</tbody>
</table>
</div>
</section>
</main>
</div>
<script src="script.js" defer></script>
</body>
</html>
style.css
:root {
--sidebar-width: 250px;
--sidebar-bg: #2c3e50;
--header-height: 70px;
--primary-color: #3498db;
--text-color: #333;
--light-text-color: #f0f2f5;
--card-bg: #ffffff;
--card-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
--border-color: #eee;
--transition-speed: 0.3s ease;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
color: var(--text-color);
background-color: var(--light-text-color);
line-height: 1.6;
}
a {
text-decoration: none;
color: var(--primary-color);
}
ul {
list-style: none;
}
.dashboard-container {
display: grid;
grid-template-columns: var(--sidebar-width) 1fr;
grid-template-rows: 1fr;
grid-template-areas: "sidebar main";
min-height: 100vh;
}
.sidebar {
grid-area: sidebar;
background-color: var(--sidebar-bg);
color: var(--light-text-color);
padding: 20px 0;
display: flex;
flex-direction: column;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
transition: transform var(--transition-speed);
}
.sidebar-header {
text-align: center;
margin-bottom: 30px;
padding: 0 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.sidebar-header .logo {
color: white;
font-size: 1.8em;
font-weight: 700;
}
.sidebar-header .close-btn {
display: none;
background: none;
border: none;
color: white;
font-size: 1.5em;
cursor: pointer;
}
.sidebar-nav ul li a {
display: flex;
align-items: center;
padding: 15px 20px;
color: var(--light-text-color);
font-size: 1.05em;
transition: background-color var(--transition-speed), color var(--transition-speed);
}
.sidebar-nav ul li a:hover,
.sidebar-nav ul li a.active {
background-color: var(--primary-color);
color: white;
}
.sidebar-nav ul li a i {
margin-right: 15px;
width: 20px;
text-align: center;
}
.main-content {
grid-area: main;
display: flex;
flex-direction: column;
background-color: #f8f9fa;
}
.main-header {
background-color: var(--card-bg);
height: var(--header-height);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 25px;
box-shadow: var(--card-shadow);
z-index: 10;
}
.main-header .open-btn {
display: none;
background: none;
border: none;
color: var(--text-color);
font-size: 1.5em;
cursor: pointer;
}
.page-title {
font-size: 1.8em;
color: var(--sidebar-bg);
}
.user-info {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
color: var(--text-color);
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
border: 2px solid var(--primary-color);
}
.dashboard-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 25px;
padding: 25px;
flex-wrap: wrap;
}
.card {
background-color: var(--card-bg);
padding: 25px;
border-radius: 10px;
box-shadow: var(--card-shadow);
display: flex;
align-items: center;
gap: 20px;
transition: transform 0.2s ease;
}
.card:hover {
transform: translateY(-5px);
}
.card-icon {
font-size: 2.5em;
color: var(--primary-color);
}
.card-content h3 {
font-size: 1.1em;
color: #666;
margin-bottom: 5px;
}
.card-content .card-value {
font-size: 2em;
font-weight: 700;
color: var(--sidebar-bg);
margin-bottom: 5px;
}
.card-content .card-trend {
font-size: 0.9em;
font-weight: 600;
}
.card-trend.up { color: #28a745; }
.card-trend.down { color: #dc3545; }
.card-trend.neutral { color: #ffc107; }
.data-table-section {
padding: 0 25px 25px;
flex-grow: 1;
}
.data-table-section h2 {
font-size: 1.8em;
color: var(--sidebar-bg);
margin-bottom: 20px;
text-align: left;
}
.table-responsive {
overflow-x: auto;
background-color: var(--card-bg);
border-radius: 10px;
box-shadow: var(--card-shadow);
}
.data-table {
width: 100%;
border-collapse: collapse;
min-width: 600px;
}
.data-table th,
.data-table td {
padding: 15px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
.data-table thead th {
background-color: var(--primary-color);
color: white;
font-weight: 600;
text-transform: uppercase;
font-size: 0.9em;
}
.data-table tbody tr:hover {
background-color: #f0f8ff;
}
.data-table tbody tr:nth-child(even) {
background-color: #fdfdfd;
}
.data-table tbody tr:last-child td {
border-bottom: none;
}
.status-pending { color: #ffc107; font-weight: 600; }
.status-completed { color: #28a745; font-weight: 600; }
.status-cancelled { color: #dc3545; font-weight: 600; }
@media (max-width: 1024px) {
.dashboard-container {
grid-template-columns: 1fr;
grid-template-areas: "main";
}
.sidebar {
position: fixed;
top: 0;
left: 0;
width: var(--sidebar-width);
height: 100%;
z-index: 1000;
transform: translateX(-100%);
box-shadow: 4px 0 15px rgba(0, 0, 0, 0.2);
}
.sidebar.active {
transform: translateX(0);
}
.sidebar-header .close-btn {
display: block;
}
.main-header .open-btn {
display: block;
}
.main-header .page-title {
font-size: 1.5em;
}
.dashboard-cards {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
padding: 20px;
gap: 20px;
}
.card {
padding: 20px;
flex-direction: column;
align-items: flex-start;
text-align: left;
}
.card-icon { margin-bottom: 10px; }
}
@media (max-width: 768px) {
.main-header {
padding: 0 15px;
}
.main-header .page-title {
font-size: 1.2em;
}
.user-info span {
display: none;
}
.dashboard-cards {
padding: 15px;
gap: 15px;
grid-template-columns: 1fr;
}
.card-content .card-value {
font-size: 1.8em;
}
.data-table-section {
padding: 0 15px 15px;
}
.data-table-section h2 {
font-size: 1.5em;
}
.data-table thead {
display: none;
}
.data-table,
.data-table tbody,
.data-table tr,
.data-table td {
display: block;
width: 100%;
}
.data-table tr {
margin-bottom: 15px;
border: 1px solid var(--border-color);
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
}
.data-table td {
text-align: right;
padding-left: 50%;
position: relative;
border-bottom: 1px solid #eee;
}
.data-table td:last-child {
border-bottom: none;
}
.data-table td::before {
content: attr(data-label);
position: absolute;
left: 15px;
width: calc(50% - 30px);
padding-right: 10px;
white-space: nowrap;
text-align: left;
font-weight: bold;
color: #555;
}
}
script.js
const sidebar = document.getElementById('sidebar');
const menuToggleBtn = document.getElementById('menuToggleBtn');
const sidebarCloseBtn = document.getElementById('sidebarCloseBtn');
const mainContent = document.getElementById('mainContent');
function toggleSidebar() {
sidebar.classList.toggle('active');
}
function closeSidebar() {
sidebar.classList.remove('active');
}
menuToggleBtn.addEventListener('click', toggleSidebar);
sidebarCloseBtn.addEventListener('click', closeSidebar);
mainContent.addEventListener('click', (event) => {
if (sidebar.classList.contains('active') && event.target !== menuToggleBtn && !menuToggleBtn.contains(event.target)) {
if (window.innerWidth <= 1024) { // Only close if on mobile/tablet view
closeSidebar();
}
}
});
function handleResize() {
if (window.innerWidth > 1024) {
// On desktop view, ensure sidebar is not in active (mobile-only) state
sidebar.classList.remove('active');
} else {
// On mobile/tablet view, ensure sidebar is hidden by default (remove active class)
// This prevents the sidebar from being stuck open if resized from desktop while active
sidebar.classList.remove('active');
}
}
window.addEventListener('resize', handleResize);
window.addEventListener('load', handleResize);
7. 拓展与改进
- 数据可视化: 集成图表库(如 Chart.js, D3.js)来动态展示数据,使仪表板更具信息量和吸引力。
- 深色模式 (Dark Mode): 添加一个按钮来切换深色模式。利用 CSS 变量(如 JS 趣闻中提到的),JavaScript 可以轻松地改变全局颜色变量的值,从而实现主题切换。
- 用户认证与权限: 引入用户登录/注册功能,并根据用户角色显示不同的内容或权限。
- 搜索与筛选: 为表格数据添加搜索框和筛选器,方便用户查找特定信息。
- 动态数据加载: 使用
fetchAPI 从后端服务器加载实时数据,而不是使用静态 HTML 内容。 - 通知中心: 添加一个通知区域,显示系统消息、警报或用户互动。
- 模态框 (Modal): 当需要用户输入或确认时,使用模态框提供一个独立的弹出界面。
- 可定制组件: 允许用户拖放和调整仪表板上的卡片布局,保存他们的偏好。
- 国际化 (i18n): 支持多种语言,让不同地区的用户可以使用他们熟悉的语言。
- 更复杂的响应式表格: 除了堆叠行,还可以考虑表格列的动态显示/隐藏,或使用更高级的响应式表格库。
8. 总结
恭喜您!您已经成功地使用 HTML、CSS 和 JavaScript 构建了一个专业且响应式的管理仪表板。在这个过程中,您:
- 学会了如何构建复杂的 HTML 结构,包括侧边导航栏、顶部标题栏、数据卡片和数据表格。
- 掌握了如何利用 CSS 变量进行样式管理,使其更易于维护和主题化。
- 深入理解了 CSS Grid 如何用于构建整体页面布局 (sidebar + main content),以及如何使用
grid-template-areas和grid-template-columns。 - 掌握了 Flexbox 如何用于卡片内部布局和标题栏内容对齐。
- 学习了如何使用
@media查询实现仪表板的响应式设计,使其在桌面、平板和手机上都能提供出色的用户体验,包括侧边栏的切换和表格的适应性调整。 - 掌握了 JavaScript 如何通过 DOM 操作 (
classList.toggle()) 实现侧边栏的动态显示/隐藏交互。 - 理解了 JavaScript 如何监听窗口
resize事件来调整布局状态,以及event.target.closest()在事件委托中的应用。
这个项目是您 Web 开发旅程中的一个重要里程碑,它涵盖了现代 Web 开发中 UI 布局、响应式设计和交互性实现的许多核心概念。继续练习、探索和构建,您将很快成为一名熟练的开发者!
9. 附录:常见问题
Q: 为什么在 dashboard-container 中选择使用 CSS Grid 而不是 Flexbox 来做主布局?
A: 对于这种两维布局(同时控制行和列),CSS Grid 通常比 Flexbox 更强大和直观。
- Flexbox 主要用于一维布局(行或列),擅长在一条轴线上对齐和分布项目。
- CSS Grid 专为二维布局设计,可以同时定义行和列,并通过
grid-template-areas明确命名和放置元素,这使得复杂的整体页面布局(如仪表板的侧边栏、顶部栏、主内容区)变得非常容易管理和理解。尤其在实现不同屏幕尺寸下的布局切换时,Grid 只需要修改grid-template-columns和grid-template-areas就能快速重排,而 Flexbox 可能需要更多嵌套和更复杂的媒体查询。
在本例中,dashboard-container是一个完美的 Grid 布局用例,因为它有明确的“侧边栏”和“主内容”两个区域。
Q: 为什么侧边栏在移动端使用 position: fixed 和 transform 来隐藏和显示,而不是简单的 display: none / block?
A:
display: none / block的缺点: 使用display: none会导致元素立即从布局中移除,没有动画效果,切换会显得生硬。position: fixed和transform的优点:- 动画效果:
transform: translateX(-100%)可以将侧边栏完全移出屏幕左侧,使其隐藏。当添加active类将transform改为translateX(0)时,结合transition属性,可以实现平滑的滑入滑出动画效果,提升用户体验。 - 不影响布局:
position: fixed使侧边栏脱离文档流,不占用页面空间,可以避免在侧边栏显示/隐藏时引起主内容区的抖动。 - 覆盖效果:
z-index: 1000确保侧边栏在滑出时能覆盖在主内容区域之上,这是移动端常见的设计模式。
- 动画效果:
Q: minmax(220px, 1fr) 在 grid-template-columns 中有什么作用?
A: minmax() 是 CSS Grid 中的一个函数,用于定义网格轨道(行或列)的最小和最大尺寸。
minmax(220px, 1fr)表示:- 最小尺寸: 网格轨道(这里是卡片列)的最小宽度为
220px。这意味着无论屏幕多窄,卡片都不会小于这个宽度,以保证内容的可读性。 - 最大尺寸: 网格轨道在有空间时可以扩展到
1fr。1fr表示“一个分数单位”,即占据所有可用空间中的一个等份。如果有多列,它们会平均分配剩余空间。
- 最小尺寸: 网格轨道(这里是卡片列)的最小宽度为
- 结合
repeat(auto-fit, minmax(220px, 1fr)):auto-fit关键字告诉 Grid 布局算法在容器内自动填充尽可能多的列。- 这意味着,Grid 会根据容器的宽度,自动计算可以容纳多少个宽度至少为 220px 的卡片列。如果容器足够宽,它可能会显示 3 个、4 个甚至更多卡片;如果容器变窄,它会自动减少列数,甚至变成单列,所有这些都是自动完成的,而无需手动编写多个媒体查询来改变
grid-template-columns。这是实现响应式卡片布局的强大且简洁的方法。
浙公网安备 33010602011771号