Skip to content

综合案例:学员管理后台静态页 ​

本节把前八节 CSS 组合成完整后台页面。页面包含顶栏、侧栏、统计卡片、搜索区和学员表格:顶栏、侧栏、表单工具条和统计卡片都使用 Flex,并用一个最小媒体查询保证窄屏下仍可阅读。

本案例只使用 HTML 和 CSS:按钮可以显示悬停与焦点状态,但不会真正查询、新增或删除数据。先拆分页面区域,再从外到内完成主框架和内容组件。

拆分页面结构 ​

text
app
├── topbar:品牌、课程说明、用户信息
└── app-body(Flex)
    ├── sidebar:纵向导航
    └── main-content:自动占据剩余空间
        ├── page-header:标题与新增按钮
        ├── stats-cards(Flex):可换行的统计卡片
        ├── search-panel:搜索条件
        └── table-panel:学员表格

主布局和统计卡片都使用 Flex,只有状态点等小装饰使用伪元素,不用绝对定位拼页面。

建立全局基础样式 ​

css
:root {
  --color-primary: #1677ff;
  --color-primary-dark: #0958d9;
  --color-text: #273142;
  --color-heading: #17233d;
  --color-muted: #667085;
  --color-page: #f4f7fb;
  --color-surface: #ffffff;
  --color-border: #e2e8f0;
  --color-border-light: #eef2f7;
  --color-sidebar: #17233d;
  --color-sidebar-text: #dbe7ff;
  --color-sidebar-active: #2457a7;
  --color-control-border: #cbd5e1;
  --color-control-hover: #94a3b8;
  --color-control-text: #344054;
  --color-success: #16803c;
  --color-warning: #ad4e00;
  --color-table-head: #f8fafc;
  --color-table-hover: #f8fbff;
  --space-sm: 8px;
  --space-md: 16px;
  --space-lg: 24px;
  --radius-sm: 8px;
  --radius-md: 12px;
  --focus-ring: rgb(22 119 255 / 22%);

  color: var(--color-text);
  font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
  line-height: 1.5;
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-width: 320px;
  background-color: var(--color-page);
}

a {
  color: inherit;
}

button,
input,
select {
  font: inherit;
}

全局只做必要初始化,不一次性抹掉所有元素的默认样式。

搭建后台主框架 ​

顶栏 ​

html
<header class="topbar">
  <a class="brand" href="#">正心学员管理</a>
  <p class="course-name">FastAPI Web 全栈基础</p>
  <div class="user-summary">
    <span class="user-avatar" aria-hidden="true">正</span>
    <span>正心老师</span>
  </div>
</header>
css
.topbar {
  display: flex;
  min-height: 64px;
  padding: 10px var(--space-lg);
  align-items: center;
  gap: var(--space-lg);
  border-bottom: 1px solid var(--color-border-light);
  background-color: var(--color-surface);
}

.course-name {
  margin: 0;
  color: var(--color-muted);
}

.user-summary {
  display: flex;
  margin-left: auto;
  align-items: center;
  gap: var(--space-sm);
}

margin-left: auto 消耗左侧剩余空间,把用户区推到顶栏右侧。

侧栏与内容区 ​

html
<div class="app-body">
  <aside class="sidebar">
    <nav class="sidebar-nav" aria-label="后台导航">...</nav>
  </aside>
  <main class="main-content">...</main>
</div>
css
.app-body {
  display: flex;
  min-height: calc(100vh - 64px);
}

.sidebar {
  width: 232px;
  flex-shrink: 0;
  padding: var(--space-lg) var(--space-md);
  background-color: var(--color-sidebar);
  color: var(--color-sidebar-text);
  color: #dbe7ff;
}

.sidebar-nav {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.sidebar-link {
  padding: 11px 14px;
  border-radius: var(--radius-sm);
  text-decoration: none;
}

.sidebar-link:hover,
.sidebar-link:focus-visible,
.sidebar-link.is-active {
  background-color: var(--color-sidebar-active);
  color: var(--color-surface);
}

.main-content {
  flex: 1;
  min-width: 0;
  padding: var(--space-lg);
}

侧栏不收缩,主内容占据剩余空间。min-width: 0 避免内部宽表格强行撑开整个布局。

完善主内容区 ​

页面标题与操作 ​

css
.page-header {
  display: flex;
  margin-bottom: var(--space-lg);
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
}

.page-title {
  margin: 0 0 6px;
  color: var(--color-heading);
  font-size: 28px;
}

.button {
  display: inline-flex;
  min-height: 40px;
  padding: 8px var(--space-md);
  align-items: center;
  justify-content: center;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  cursor: pointer;
  text-decoration: none;
}

.button-primary {
  background-color: var(--color-primary);
  color: var(--color-surface);
}

链接和按钮在这个页面中共享视觉类,但 HTML 元素仍根据真实行为选择:跳转使用链接,提交表单使用按钮。

统计卡片 ​

css
.stats-cards {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-md);
  margin-bottom: 20px;
}

.stat-card {
  flex: 1 1 220px;
  min-width: 0;
  padding: var(--space-md);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background-color: var(--color-surface);
}

卡片以 220px 为基础尺寸,空间足够时在同一行伸展;空间不足时换到下一行。gap 保持卡片之间的间距。

搜索区 ​

html
<form class="search-panel" action="/students" method="get">
  <label class="field">
    <span>关键词</span>
    <input name="keyword" type="search" placeholder="姓名或手机号">
  </label>

  <label class="field">
    <span>状态</span>
    <select name="status">
      <option value="">全部</option>
      <option value="active">正常</option>
      <option value="disabled">已停用</option>
    </select>
  </label>

  <button class="button button-primary" type="submit">查询</button>
</form>
css
.search-panel {
  display: flex;
  margin-bottom: 20px;
  padding: var(--space-md);
  align-items: flex-end;
  flex-wrap: wrap;
  gap: var(--space-md);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background-color: var(--color-surface);
}

.field {
  display: flex;
  flex: 1 1 220px;
  flex-direction: column;
  gap: var(--space-sm);
}

.field input,
.field select {
  width: 100%;
  min-height: 40px;
  padding: 8px 12px;
  border: 1px solid var(--color-control-border);
  border-radius: var(--radius-sm);
  background-color: var(--color-surface);
  color: var(--color-text);
}

.field input:hover,
.field select:hover {
  border-color: var(--color-control-hover);
}

表单控件随字段容器伸缩,按钮保持内容宽度。

表格区域 ​

html
<section class="table-panel" aria-labelledby="student-list-title">
  <div class="table-panel-header">
    <h2 id="student-list-title">学员列表</h2>
    <p>共 128 人</p>
  </div>
  <div class="table-wrapper">
    <table class="student-table">...</table>
  </div>
</section>
css
.table-panel {
  overflow: hidden;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background-color: var(--color-surface);
}

.table-panel-header {
  display: flex;
  padding: var(--space-md);
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  border-bottom: 1px solid var(--color-border);
}

.table-wrapper {
  overflow-x: auto;
}

.student-table {
  width: 100%;
  min-width: 760px;
  border-collapse: collapse;
}

.student-table th,
.student-table td {
  padding: 14px 16px;
  border-bottom: 1px solid var(--color-border-light);
  text-align: left;
  white-space: nowrap;
}

.student-table tbody tr:hover {
  background-color: #f8fbff;
}

表格保持可读的最小宽度,窄屏由外层提供横向滚动。

处理窄屏显示 ​

css
@media (max-width: 760px) {
  .topbar {
    padding-inline: 16px;
    flex-wrap: wrap;
  }

  .course-name {
    display: none;
  }

  .app-body {
    flex-direction: column;
  }

  .sidebar {
    width: 100%;
    padding: 12px 16px;
  }

  .sidebar-nav {
    flex-direction: row;
    overflow-x: auto;
  }

  .sidebar-link {
    flex-shrink: 0;
  }

  .main-content {
    padding: 20px 16px;
  }

  .page-header {
    align-items: stretch;
    flex-direction: column;
  }
}

这里只做必要调整:侧栏改到上方,导航横向滚动,标题区纵向排列。响应式体系和复杂断点不属于本课程范围。

逐项验收页面 ​

  1. 打开附件,确认顶栏、侧栏、内容区边界清楚。
  2. 将窗口从宽屏逐步缩窄到 760px 以下。
  3. 检查统计卡片是否根据可用宽度自动调整列数。
  4. 检查侧栏是否变成顶部横向导航。
  5. 检查宽表格是否在自身容器内滚动,而不是撑开整个页面。
  6. 使用 Tab 键检查导航、按钮和表单控件的焦点样式。
  7. 在开发者工具中取消 .main-content 的 min-width: 0,观察长内容可能产生的影响。
学员管理后台静态页源码
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="./styles.css">
</head>
<body>
  <div class="app">
    <header class="topbar">
      <a class="brand" href="#">正心学员管理</a>
      <p class="course-name">FastAPI Web 全栈基础</p>
      <div class="user-summary">
        <span class="user-avatar" aria-hidden="true">正</span>
        <span>正心老师</span>
      </div>
    </header>

    <div class="app-body">
      <aside class="sidebar">
        <nav class="sidebar-nav" aria-label="后台导航">
          <a class="sidebar-link" href="#">数据概览</a>
          <a class="sidebar-link is-active" href="#students" aria-current="page">学员管理</a>
          <a class="sidebar-link" href="#">课程管理</a>
          <a class="sidebar-link" href="#">系统设置</a>
        </nav>
      </aside>

      <main class="main-content" id="students">
        <header class="page-header">
          <div>
            <h1 class="page-title">学员管理</h1>
            <p class="page-description">查询学员资料并查看当前学习状态。</p>
          </div>
          <a class="button button-primary" href="#">新增学员</a>
        </header>

        <section class="stats-cards" aria-label="学员统计">
          <article class="stat-card">
            <p class="stat-label">学员总数</p>
            <p class="stat-value">128</p>
            <p class="stat-note is-positive">较上周增加 8 人</p>
          </article>
          <article class="stat-card">
            <p class="stat-label">正常学习</p>
            <p class="stat-value">112</p>
            <p class="stat-note">占学员总数 87.5%</p>
          </article>
          <article class="stat-card">
            <p class="stat-label">待审核</p>
            <p class="stat-value">9</p>
            <p class="stat-note is-warning">请及时处理</p>
          </article>
          <article class="stat-card">
            <p class="stat-label">已停用</p>
            <p class="stat-value">7</p>
            <p class="stat-note">保留历史学习记录</p>
          </article>
        </section>

        <form class="search-panel" action="/students" method="get">
          <label class="field">
            <span>关键词</span>
            <input name="keyword" type="search" placeholder="姓名或手机号">
          </label>
          <label class="field">
            <span>班级</span>
            <select name="class_code">
              <option value="">全部班级</option>
              <option value="web-01">FastAPI Web 1 班</option>
              <option value="python-02">Python 基础 2 班</option>
            </select>
          </label>
          <label class="field">
            <span>状态</span>
            <select name="status">
              <option value="">全部状态</option>
              <option value="active">正常</option>
              <option value="pending">待审核</option>
              <option value="disabled">已停用</option>
            </select>
          </label>
          <button class="button button-primary" type="submit">查询</button>
          <button class="button button-secondary" type="reset">重置</button>
        </form>

        <section class="table-panel" aria-labelledby="student-list-title">
          <header class="table-panel-header">
            <h2 id="student-list-title">学员列表</h2>
            <p>共 128 人</p>
          </header>

          <div class="table-wrapper">
            <table class="student-table">
              <thead>
                <tr>
                  <th scope="col">姓名</th>
                  <th scope="col">班级</th>
                  <th scope="col">手机号</th>
                  <th scope="col">报名日期</th>
                  <th scope="col">状态</th>
                  <th scope="col">操作</th>
                </tr>
              </thead>
              <tbody>
                <tr>
                  <th scope="row">张三</th>
                  <td>FastAPI Web 1 班</td>
                  <td>13800000001</td>
                  <td>2026-07-12</td>
                  <td><span class="status status-active">正常</span></td>
                  <td class="table-actions"><a href="#">查看</a><a href="#">编辑</a></td>
                </tr>
                <tr>
                  <th scope="row">李四</th>
                  <td>Python 基础 2 班</td>
                  <td>13800000002</td>
                  <td>2026-07-14</td>
                  <td><span class="status status-pending">待审核</span></td>
                  <td class="table-actions"><a href="#">查看</a><a href="#">编辑</a></td>
                </tr>
                <tr>
                  <th scope="row">王小明</th>
                  <td>FastAPI Web 1 班</td>
                  <td>13800000003</td>
                  <td>2026-06-28</td>
                  <td><span class="status status-disabled">已停用</span></td>
                  <td class="table-actions"><a href="#">查看</a><a href="#">编辑</a></td>
                </tr>
                <tr>
                  <th scope="row">赵敏</th>
                  <td>FastAPI Web 1 班</td>
                  <td>13800000004</td>
                  <td>2026-07-15</td>
                  <td><span class="status status-active">正常</span></td>
                  <td class="table-actions"><a href="#">查看</a><a href="#">编辑</a></td>
                </tr>
              </tbody>
            </table>
          </div>
        </section>
      </main>
    </div>
  </div>
</body>
</html>
css
:root {
  --color-primary: #1677ff;
  --color-primary-dark: #0958d9;
  --color-text: #273142;
  --color-heading: #17233d;
  --color-muted: #667085;
  --color-page: #f4f7fb;
  --color-surface: #ffffff;
  --color-border: #e2e8f0;
  --color-border-light: #eef2f7;
  --color-sidebar: #17233d;
  --color-sidebar-text: #dbe7ff;
  --color-sidebar-active: #2457a7;
  --color-control-border: #cbd5e1;
  --color-control-hover: #94a3b8;
  --color-control-text: #344054;
  --color-success: #16803c;
  --color-warning: #ad4e00;
  --color-table-head: #f8fafc;
  --color-table-hover: #f8fbff;
  --space-sm: 8px;
  --space-md: 16px;
  --space-lg: 24px;
  --radius-sm: 8px;
  --radius-md: 12px;
  --focus-ring: rgb(22 119 255 / 22%);

  color: var(--color-text);
  font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
  line-height: 1.5;
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-width: 320px;
  background-color: var(--color-page);
}

a {
  color: inherit;
}

button,
input,
select {
  font: inherit;
}

.app {
  min-height: 100vh;
}

.topbar {
  display: flex;
  min-height: 64px;
  padding: 10px var(--space-lg);
  align-items: center;
  gap: var(--space-lg);
  border-bottom: 1px solid var(--color-border-light);
  background-color: var(--color-surface);
}

.brand {
  color: var(--color-heading);
  font-size: 19px;
  font-weight: 700;
  text-decoration: none;
}

.course-name {
  margin: 0;
  color: var(--color-muted);
}

.user-summary {
  display: flex;
  margin-left: auto;
  align-items: center;
  gap: var(--space-sm);
  white-space: nowrap;
}

.user-avatar {
  display: inline-flex;
  width: 36px;
  height: 36px;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background-color: #e6f4ff;
  color: var(--color-primary-dark);
  font-weight: 700;
}

.app-body {
  display: flex;
  min-height: calc(100vh - 64px);
}

.sidebar {
  width: 232px;
  flex-shrink: 0;
  padding: var(--space-lg) var(--space-md);
  background-color: var(--color-sidebar);
  color: var(--color-sidebar-text);
  color: #dbe7ff;
}

.sidebar-nav {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.sidebar-link {
  padding: 11px 14px;
  border-radius: var(--radius-sm);
  text-decoration: none;
}

.sidebar-link:hover,
.sidebar-link:focus-visible,
.sidebar-link.is-active {
  background-color: var(--color-sidebar-active);
  color: var(--color-surface);
  outline: none;
}

.sidebar-link:focus-visible {
  box-shadow: 0 0 0 3px rgb(255 255 255 / 35%);
}

.main-content {
  flex: 1;
  min-width: 0;
  padding: var(--space-lg);
}

.page-header {
  display: flex;
  margin-bottom: var(--space-lg);
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
}

.page-title {
  margin: 0 0 6px;
  color: var(--color-heading);
  font-size: 28px;
  line-height: 1.35;
}

.page-description {
  margin: 0;
  color: var(--color-muted);
}

.button {
  display: inline-flex;
  min-height: 40px;
  padding: 8px var(--space-md);
  align-items: center;
  justify-content: center;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
}

.button:focus-visible,
.field input:focus,
.field select:focus,
.table-actions a:focus-visible {
  outline: 3px solid var(--focus-ring);
  outline-offset: 2px;
}

.button-primary {
  background-color: var(--color-primary);
  color: var(--color-surface);
}

.button-primary:hover {
  background-color: var(--color-primary-dark);
}

.button-secondary {
  border-color: var(--color-control-border);
  background-color: var(--color-surface);
  color: var(--color-control-text);
}

.button-secondary:hover {
  background-color: #f8fafc;
}

.stats-cards {
  display: flex;
  flex-wrap: wrap;
  margin-bottom: 20px;
  gap: var(--space-md);
}

.stat-card {
  flex: 1 1 220px;
  min-width: 0;
  padding: var(--space-md);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background-color: var(--color-surface);
}

.stat-label,
.stat-note {
  margin: 0;
  color: var(--color-muted);
}

.stat-value {
  margin: 8px 0 6px;
  color: var(--color-heading);
  font-size: 30px;
  font-weight: 700;
}

.stat-note {
  font-size: 13px;
}

.stat-note.is-positive {
  color: var(--color-success);
}

.stat-note.is-warning {
  color: var(--color-warning);
}

.search-panel {
  display: flex;
  margin-bottom: 20px;
  padding: var(--space-md);
  align-items: flex-end;
  flex-wrap: wrap;
  gap: var(--space-md);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background-color: var(--color-surface);
}

.field {
  display: flex;
  flex: 1 1 190px;
  flex-direction: column;
  gap: var(--space-sm);
  color: var(--color-control-text);
  font-size: 14px;
  font-weight: 600;
}

.field input,
.field select {
  width: 100%;
  min-height: 40px;
  padding: 8px 12px;
  border: 1px solid var(--color-control-border);
  border-radius: var(--radius-sm);
  background-color: var(--color-surface);
  color: var(--color-text);
  font-weight: 400;
}

.field input:hover,
.field select:hover {
  border-color: var(--color-control-hover);
}

.table-panel {
  overflow: hidden;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background-color: var(--color-surface);
}

.table-panel-header {
  display: flex;
  padding: var(--space-md);
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  border-bottom: 1px solid var(--color-border);
}

.table-panel-header h2,
.table-panel-header p {
  margin: 0;
}

.table-panel-header h2 {
  color: var(--color-heading);
  font-size: 19px;
}

.table-panel-header p {
  color: var(--color-muted);
}

.table-wrapper {
  overflow-x: auto;
}

.student-table {
  width: 100%;
  min-width: 760px;
  border-collapse: collapse;
}

.student-table th,
.student-table td {
  padding: 14px 16px;
  border-bottom: 1px solid var(--color-border-light);
  text-align: left;
  white-space: nowrap;
}

.student-table thead th {
  background-color: var(--color-table-head);
  color: #475467;
  font-size: 13px;
}

.student-table tbody th {
  color: var(--color-heading);
  font-weight: 600;
}

.student-table tbody tr:hover {
  background-color: var(--color-table-hover);
}

.student-table tbody tr:last-child th,
.student-table tbody tr:last-child td {
  border-bottom: 0;
}

.status {
  display: inline-block;
  padding: 4px 9px;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 600;
}

.status-active {
  background-color: #ecfdf3;
  color: #16803c;
}

.status-pending {
  background-color: #fff7e6;
  color: #ad4e00;
}

.status-disabled {
  background-color: #fff1f0;
  color: #b42318;
}

.table-actions {
  display: flex;
  gap: 12px;
}

.table-actions a {
  color: #0958d9;
  text-decoration: none;
}

.table-actions a:hover {
  text-decoration: underline;
}

@media (max-width: 760px) {
  .topbar {
    padding-inline: 16px;
    flex-wrap: wrap;
    gap: 12px;
  }

  .course-name {
    display: none;
  }

  .app-body {
    flex-direction: column;
  }

  .sidebar {
    width: 100%;
    padding: 12px 16px;
  }

  .sidebar-nav {
    flex-direction: row;
    overflow-x: auto;
  }

  .sidebar-link {
    flex-shrink: 0;
  }

  .main-content {
    padding: 20px 16px;
  }

  .page-header {
    align-items: stretch;
    flex-direction: column;
  }

  .page-header .button {
    align-self: flex-start;
  }

  .search-panel .button {
    flex: 1 1 120px;
  }
}

:::

AI 协作任务:定位布局问题 ​

先选择一个可观察的问题,例如“窄屏下搜索区拥挤”或“键盘焦点不明显”,再把问题和相关 HTML、CSS 一起提供给 AI:

text
目标:修复学员管理静态页在 768px 宽度下的布局问题。
上下文:页面已有顶栏、侧栏、统计卡片、搜索区和表格。
约束:只能修改 HTML 和 CSS;不添加 JavaScript、框架或 UI 组件库;主布局和统计卡片继续使用 Flex。
验收:页面无横向溢出,内容不遮挡,Tab 键焦点可见,宽屏布局不退化。
请给出最小修改,并说明每条 CSS 解决什么问题。

不要因为 AI 说“已修复”就结束。分别在宽屏和窄屏检查页面,再按 Tab 键观察链接、输入框和按钮的焦点。若修改引入新的溢出或覆盖,应把具体视口宽度和现象反馈给 AI,继续小步调整。

案例验收 ​

完成本节案例后,对照下面的标准检查这个 HTML/CSS 管理后台。

验收:

  • 顶栏、侧栏、工具条和统计卡片使用 Flex。
  • 宽屏和窄屏下没有明显遮挡或横向溢出。
  • 鼠标悬停、键盘焦点和状态颜色清楚可辨。

小结 ​

  1. 管理后台主结构由嵌套的 Flex 容器完成,统计卡片通过换行适应可用宽度。
  2. 设计令牌、盒模型、文字层级和状态样式共同形成可维护的组件外观。
  3. 定位只用于局部覆盖,不参与主结构拼接。
  4. 长表格放入独立横向滚动容器。
  5. 最小媒体查询用于调整窄屏排列,不展开完整移动端体系。