常见模式 & 方案
针对每个管理仪表盘中都会出现的模式,提供可直接复制粘贴的解决方案。每个方案都假定您已经按照入门指南操作,并拥有一个可用的 AdminLTE 页面作为基础。
带图标、徽章和标题的多级侧边栏
演示仪表盘使用的就是这个模式。使用
.nav-header 对区域进行分组,使用嵌套的
.nav-treeview 列表表示项目层级深度,使用
.navbar-badge 显示计数。
<aside class="app-sidebar bg-body-secondary shadow" data-bs-theme="dark">
<div class="sidebar-brand">
<a href="/" class="brand-link">
<span class="brand-text fw-light">我的仪表盘</span>
</a>
</div>
<div class="sidebar-wrapper">
<nav class="mt-2">
<ul class="nav sidebar-menu flex-column" data-lte-toggle="treeview" role="menu">
<li class="nav-header">首页</li>
<li class="nav-item">
<a href="/" class="nav-link active">
<i class="nav-icon bi bi-speedometer"></i>
<p>仪表盘</p>
</a>
</li>
<li class="nav-item">
<a href="#" class="nav-link">
<i class="nav-icon bi bi-folder"></i>
<p>
项目
<span class="badge text-bg-info ms-2">12</span>
<i class="nav-arrow bi bi-chevron-right"></i>
</p>
</a>
<ul class="nav nav-treeview">
<li class="nav-item">
<a href="#" class="nav-link"><i class="nav-icon bi bi-circle"></i><p>活跃</p></a>
</li>
<li class="nav-item">
<a href="#" class="nav-link"><i class="nav-icon bi bi-circle"></i><p>归档</p></a>
</li>
</ul>
</li>
<li class="nav-header">报告</li>
<li class="nav-item">
<a href="#" class="nav-link">
<i class="nav-icon bi bi-bar-chart"></i>
<p>销售</p>
<span class="badge text-bg-danger ms-auto">3</span>
</a>
</li>
</ul>
</nav>
</div>
</aside>
要将当前页面标记为激活状态,请在服务端将 .active 类渲染到对应的
.nav-link 上。对于 SPA 框架,在路由变更时添加它。
从 JavaScript 高亮当前侧边栏项
如果您是一次性渲染侧边栏,并希望客户端路由高亮:
function setActiveSidebarItem(href) {
document.querySelectorAll(".sidebar-menu .nav-link").forEach((el) => {
el.classList.toggle("active", el.getAttribute("href") === href)
})
}
// 路线变更通知
setActiveSidebarItem(window.location.pathname)
响应式数据表格
将 <table> 包裹在
.table-responsive
中,使其在窄视口下获得水平滚动条,而不是溢出卡片。
<div class="card">
<div class="card-header">
<h3 class="card-title">最近订单</h3>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-striped align-middle mb-0">
<thead>
<tr>
<th>#</th>
<th>客户</th>
<th>合计</th>
<th>状态</th>
<th class="text-end">操作</th>
</tr>
</thead>
<tbody>
<!-- rows -->
</tbody>
</table>
</div>
</div>
</div>
如需交互式排序/筛选/分页,请使用
Tabulator——演示中的
tables/data.html 有完整示例。
顶部栏中的用户下拉菜单
<li class="nav-item dropdown user-menu">
<a href="#" class="nav-link dropdown-toggle" data-bs-toggle="dropdown">
<span class="d-none d-md-inline me-1">Jane Doe</span>
<i class="bi bi-person-circle fs-5"></i>
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a href="/profile" class="dropdown-item">
<i class="bi bi-person me-2"></i>资料
</a>
</li>
<li>
<a href="/settings" class="dropdown-item">
<i class="bi bi-gear me-2"></i>设置
</a>
</li>
<li><hr class="dropdown-divider"></li>
<li>
<a href="/logout" class="dropdown-item text-danger">
<i class="bi bi-box-arrow-right me-2"></i>退出
</a>
</li>
</ul>
</li>
将此代码放在 .app-header 中的
<ul class="navbar-nav ms-auto"> 内部。
一个与真实语言环境联动的语言切换器
演示版的顶栏自带一个语言下拉菜单,但光有这点标记其实什么都变不了——切换语言区域是应用自己的活儿,而且做砸了还不如不做。下面这套模式涵盖了语言切换器真正必须做对的那些事。
<li class="nav-item dropdown">
<a class="nav-link" href="#" id="language-menu" data-bs-toggle="dropdown"
aria-expanded="false" aria-label="Change language, current language English">
<i class="bi bi-translate" aria-hidden="true"></i>
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="language-menu">
<li>
<a class="dropdown-item active" href="/en/dashboard" hreflang="en" aria-current="true">
English <i class="bi bi-check-lg ms-2" aria-hidden="true"></i>
</a>
</li>
<li><a class="dropdown-item" href="/es/dashboard" hreflang="es">Español</a></li>
<li><a class="dropdown-item" href="/ar/dashboard" hreflang="ar">العربية</a></li>
</ul>
</li>
四件事决定了语言切换器是真能用还是纯摆设:
-
每一项都是一个真正的链接,指向同一页面在另一种语言区域下的版本,而不是一个带点击事件的
#。链接可以被收藏、被爬虫抓取、能新标签页打开,而且在你的 JavaScript 加载之前就能用。再加上hreflang,这样辅助技术能播报语言变化,搜索引擎也能把对应的 URL 配对起来。 -
语言区域应该放在 URL 里(比如
/es/dashboard),或者放在服务器能读取的 cookie 里——而不是放在localStorage里。服务端渲染的 HTML 必须在渲染之前就知道语言区域是什么,而localStorage要等页面已经以错误语言发出去了之后才能读到。 -
标记当前语言,同时用上
.active和aria-current="true",并在切换按钮的aria-label里写明当前语言。光有一个国旗图标并不能代表一门语言:好几种语言共用一个国旗,也有好几个国家共用一门语言。 -
切换到 RTL(从右到左)语言区域时,需要配套的 RTL 样式表,光加个
dir属性是不够的。要同时提供adminlte.rtl.css并设置<html lang="ar" dir="rtl">——详见 RTL 支持。
如果你的语言区域放在 URL 里,那么由服务器来挑样式表,切换器本身就可以保持纯标记:
<!-- 按请求、基于当前激活的语言区域实时渲染 -->
<html lang="ar" dir="rtl">
<link rel="stylesheet" href="/css/adminlte.rtl.css">
对于客户端渲染的应用,要在路由层面做同样的事——切换语言区域、重新渲染、替换样式表的
href——而不是就地去翻译文本节点。
KPI 卡片(small-box)
大多数仪表盘顶部的彩色统计框:
<div class="row g-3">
<div class="col-lg-3 col-6">
<div class="small-box text-bg-primary">
<div class="inner">
<h3>150</h3>
<p>新订单</p>
</div>
<i class="bi bi-cart small-box-icon" aria-hidden="true"></i>
<a href="#" class="small-box-footer link-light">
更多信息 <i class="bi bi-arrow-right"></i>
</a>
</div>
</div>
<!-- 重复 3 次,使用不同的数值和颜色 -->
</div>
将 text-bg-primary 替换为
text-bg-success、text-bg-warning、text-bg-danger
等。
面包屑 + 页面标题行
这是每个演示页面都使用的标准 .app-content-header 模式:
<div class="app-content-header">
<div class="container-fluid">
<div class="row">
<div class="col-sm-6">
<h3 class="mb-0">页面标题</h3>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-end">
<li class="breadcrumb-item"><a href="/">首页</a></li>
<li class="breadcrumb-item"><a href="/section">章节</a></li>
<li class="breadcrumb-item active" aria-current="page">当前页</li>
</ol>
</div>
</div>
</div>
</div>
时间线
用于动态消息、审计日志或路线图视图:
<div class="timeline">
<div>
<i class="bi bi-envelope bg-info" aria-hidden="true"></i>
<div class="timeline-item">
<span class="time"><i class="bi bi-clock"></i> 12:05</span>
<h3 class="timeline-header">
<a href="#">Olivia</a> 给你发送一条消息
</h3>
<div class="timeline-body">
嘿,我们明天10点能看一下第四季度的数字吗?
</div>
<div class="timeline-footer">
<a class="btn btn-primary btn-sm">查看更多</a>
</div>
</div>
</div>
<!-- repeat -->
</div>
时间线组件位于 _timeline.scss 中,并有多种变体。完整集合请参见
/UI/timeline.html。
聊天小部件
用于实时客户支持、团队聊天等的聊天风格卡片。包括联系人切换和消息气泡。
<div class="card direct-chat direct-chat-primary">
<div class="card-header">
<h3 class="card-title">聊天</h3>
<div class="card-tools">
<span class="badge text-bg-primary">3 new</span>
<button type="button" class="btn btn-tool" data-lte-toggle="chat-pane" title="Contacts">
<i class="bi bi-people-fill"></i>
</button>
</div>
</div>
<div class="card-body">
<div class="direct-chat-messages">
<div class="direct-chat-msg">
<div class="direct-chat-infos clearfix">
<span class="direct-chat-name float-start">Olivia</span>
<span class="direct-chat-timestamp float-end">12:05</span>
</div>
<img class="direct-chat-img" src="/avatars/olivia.jpg" alt="" />
<div class="direct-chat-text">嘿,准备好了吗?</div>
</div>
<div class="direct-chat-msg right">
<div class="direct-chat-infos clearfix">
<span class="direct-chat-name float-end">You</span>
<span class="direct-chat-timestamp float-start">12:06</span>
</div>
<img class="direct-chat-img" src="/avatars/me.jpg" alt="" />
<div class="direct-chat-text">是的,我准备好了!</div>
</div>
</div>
<div class="direct-chat-contacts">
<!-- 聊天面板切换时显示的联系人列表 -->
</div>
</div>
<div class="card-footer">
<form>
<div class="input-group">
<input type="text" class="form-control" placeholder="Message…" />
<button type="submit" class="btn btn-primary">
<i class="bi bi-send"></i>
</button>
</div>
</form>
</div>
</div>
如需完整的聊天页面应用,请参见演示中的 /pages/chat.html。
响应布局/侧边栏变更
监听插件事件,好跟你自己的代码配合 —— 比如侧边栏收起时让某个组件重新适配(这样它就会重新测量容器的宽度):
let table = /* your Tabulator instance */
document.addEventListener("collapse.lte.push-menu", () => {
// wait for the transition to finish (~300ms by default)
setTimeout(() => table.redraw(), 350)
})
document.addEventListener("open.lte.push-menu", () => {
setTimeout(() => table.redraw(), 350)
})
同样的套路也适用于
FullCalendar(calendar.updateSize())以及任何其他在初始化时读取容器宽度的库。Chart.js
的图表在这里什么都不用做:它们用
ResizeObserver 监视自己的容器,会自动重绘。
通过 fetch 将内容加载到卡片中
<div class="card" id="users-card">
<div class="card-header">
<h3 class="card-title">Users</h3>
<div class="card-tools">
<span class="spinner-border spinner-border-sm text-secondary d-none" id="users-spinner" role="status"></span>
</div>
</div>
<div class="card-body p-0">
<div id="users-body" class="text-secondary p-3">加载中…</div>
</div>
</div>
<script>
const spinner = document.getElementById("users-spinner")
const body = document.getElementById("users-body")
spinner.classList.remove("d-none")
fetch("/api/users")
.then((r) => r.text())
.then((html) => { body.innerHTML = html })
.catch(() => { body.textContent = "Failed to load." })
.finally(() => spinner.classList.add("d-none"))
</script>
提交前确认破坏性操作
Bootstrap 模态框 + 一个简单的确认模式:
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#confirmDelete">
删除账户
</button>
<div class="modal fade" id="confirmDelete" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">删除账户?</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
这将永久删除您的账户及所有相关数据,且无法恢复。
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<form action="/account/delete" method="POST" class="d-inline">
<button type="submit" class="btn btn-danger">是的,删除它!</button>
</form>
</div>
</div>
</div>
</div>
颜色模式、无障碍访问以及捆绑的 JS 会自动处理模态框内的 ESC 键关闭、焦点陷阱和键盘导航。
会跟随深色模式的图表
图表库是基于画布绘制的,因此它们对 data-bs-theme 或其他 CSS
一无所知:Chart.js
会使用其自身固定的默认颜色来绘制坐标轴标签、网格线和数据工具提示,这些颜色适合浅色页面,但在深色卡片上几乎完全不可见。你需要手动为它提供颜色,并在模式更改时再次传递这些颜色。
绘制图表的演示页面都带了一个小预设(src/html/components/_chartjs.astro,在每个页面上内联在
Chart.js 之后)。它把 AdminLTE 的 CSS 变量读进 Chart.defaults ——
字体族、文字颜色、网格线、跟界面其余部分风格一致的提示框和图例,以及在
<html dir="rtl"> 时的 rtl 设置 ——
并监听
<html>,这样页面上的每张图表都会在模式变化时重新刷样式,不管变化是来自切换器、auto
模式下的系统偏好,还是你自己的代码。它的核心部分:
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/chart.umd.min.js"></script>
<script>
const root = document.documentElement
const cssVar = (name) => getComputedStyle(root).getPropertyValue(name).trim()
const applyTheme = () => {
const grid = `rgba(${cssVar("--bs-emphasis-color-rgb")}, 0.08)`
Chart.defaults.font.family = cssVar("--bs-body-font-family")
Chart.defaults.color = cssVar("--bs-secondary-color") // 标记与标题
Chart.defaults.borderColor = grid // 网格线和坐标轴线
Chart.defaults.plugins.legend.labels.color = cssVar("--bs-body-color")
Object.assign(Chart.defaults.plugins.tooltip, {
backgroundColor: cssVar("--bs-body-bg"),
borderColor: cssVar("--bs-border-color"),
borderWidth: 1,
titleColor: cssVar("--bs-emphasis-color"),
bodyColor: cssVar("--bs-body-color")
})
Chart.defaults.elements.arc.borderColor = cssVar("--bs-body-bg") // 甜甜圈切片间隙
}
applyTheme()
new MutationObserver(() => {
applyTheme()
for (const chart of Object.values(Chart.instances)) chart.update("none")
}).observe(root, { attributes: true, attributeFilter: ["data-bs-theme"] })
</script>
在这个脚本之后创建的图表会默认采用这些颜色,而
chart.update()
会让已有的图表重新读一遍。你在数据集上自己设的颜色(borderColor: "#0d6efd")归你自己所有,会保持原样;而需要跟随模式变化的颜色可以写成一个函数,Chart.js
会在每次更新时重新调用它 ——
backgroundColor: () => cssVar("--bs-secondary-bg")。
如果你更想用 ColorMode 事件,而不是监听属性变化,可以用
document.addEventListener("changed.lte.color-mode", e =>
…),它会收到 e.detail.resolved(值为
"light" 或
"dark");而上面那个监听器还额外覆盖了 auto
模式下的系统偏好变化,以及任何直接设置这个属性的代码。同样的思路也适用于其他图表库:找到它们从哪里取文字、网格和提示框的颜色,把
CSS 变量喂给它们,然后在变化时重新应用一遍。
将页面作为文档打印
打印页面是一种文档,而非应用程序的截图,因此 AdminLTE 会将页面的外壳(页眉、侧边栏菜单和页脚)省略在纸上,而让内容占据整张纸的宽度。打印时的样式是为纸质阅读而设计的:外部链接的目标文本会被打印在后面,按钮保持轮廓以确保仍能识别为按钮,所有内容都采用黑白配色,避免背景遮盖墨迹。
对于那些要打印成文档的页面 ——
比如发给客户的发票、报价单、工单或装箱单 ——
最后那两样就是错误的输出:没人希望打印出来的发票上带着
在线支付 (https://pay.example.com/inv/12345?token=…),也没人希望按钮外面被画上一个框。把
data-lte-print="plain" 加在
<html>、<body>
或任意容器上,这两样就都没了,同时文字保持白底黑字:
<html lang="en" data-lte-print="plain">
当页面中仅有部分内容需要作为文档打印时,可将打印作用域限定到该部分:
<div class="card" data-lte-print="plain">……发票……</div>
如果页面仅以文档形式打印,请在打印时设置该属性:
document.querySelector("#print-invoice").addEventListener("click", () => {
document.documentElement.setAttribute("data-lte-print", "plain")
window.print()
})
反过来 —— 如果某个页面确实就是要按屏幕内容打印,连菜单一起 —— 在
<html> 或 <body> 上加
data-lte-print="app",就会把整个外壳都打印出来:
<html lang="en" data-lte-print="app">
如果想恢复其中某一块而不是全部三块,就用 Bootstrap
自带的显示工具类。头部(它是个 navbar)适合用
.d-print-flex,侧边栏和页脚则用 .d-print-block:
<aside class="app-sidebar bg-body-secondary shadow d-print-block" data-bs-theme="dark">…</aside>
.d-print-none 在两种模式下都能隐藏任何你不想印在纸上的东西 ——
比如发票上方那一排按钮工具栏。
接下来
- JavaScript 插件概述 —— 每个插件的事件名称和方法
- 推荐集成 —— 当您需要的超出此列表时
- 自定义 —— 更改颜色、侧边栏宽度、breakpoint