综合案例:学员管理后台静态页
本节把前八节 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;
}
}这里只做必要调整:侧栏改到上方,导航横向滚动,标题区纵向排列。响应式体系和复杂断点不属于本课程范围。
逐项验收页面
- 打开附件,确认顶栏、侧栏、内容区边界清楚。
- 将窗口从宽屏逐步缩窄到 760px 以下。
- 检查统计卡片是否根据可用宽度自动调整列数。
- 检查侧栏是否变成顶部横向导航。
- 检查宽表格是否在自身容器内滚动,而不是撑开整个页面。
- 使用 Tab 键检查导航、按钮和表单控件的焦点样式。
- 在开发者工具中取消
.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。
- 宽屏和窄屏下没有明显遮挡或横向溢出。
- 鼠标悬停、键盘焦点和状态颜色清楚可辨。
小结
- 管理后台主结构由嵌套的 Flex 容器完成,统计卡片通过换行适应可用宽度。
- 设计令牌、盒模型、文字层级和状态样式共同形成可维护的组件外观。
- 定位只用于局部覆盖,不参与主结构拼接。
- 长表格放入独立横向滚动容器。
- 最小媒体查询用于调整窄屏排列,不展开完整移动端体系。