照着这篇做完,你能用纯 PHP 加 Tailwind CSS 搭出一个带侧边栏、列表页、表单页的响应式管理后台,全程不需要 Composer、不需要 Node 构建工具就能跑起来。
第一步:定好目录结构
后台最容易写乱的地方是文件散落。先固定一个结构,后面所有页面往里塞就行:
/admin
├─ index.php // 入口,做鉴权和路由分发
├─ layout.php // 公共布局:侧边栏 + 顶栏 + 内容区
├─ list.php // 列表页
├─ edit.php // 新增/编辑页
└─ admin.css // 编译后的 Tailwind 样式(可选)
`index.php` 里先做登录校验,再根据 `?page=` 参数 include 对应文件。这样每加一个功能只加一个文件,不用改布局。
第二步:引入 Tailwind,先用 CDN 快速起步
调试阶段直接用官方 CDN,改一行 class 保存就生效,比等编译快得多:
<!-- Tailwind v3 -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Tailwind v4 起改用 -->
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
注意:CDN 版是给开发调试用的,它会先加载一段 JS 再生成样式,页面刷新时会有一瞬间的「无样式闪烁」,而且必须联网。正式上线请换成编译版:`npx @tailwindcss/cli -i input.css -o admin.css --minify`,然后把 `<link rel="stylesheet" href="admin.css">` 写到 `<head>` 里。
第三步:写公共布局 layout.php
用一个函数接收标题和当前菜单,把侧边栏和内容区分开:
<?php
function render_header(string $title, string $active = ''): void {
$menus = [
'dashboard' => '仪表盘',
'posts' => '内容管理',
'users' => '用户管理',
'settings' => '系统设置',
];
?>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars($title) ?> - 管理后台</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-slate-100 text-slate-800">
<div class="flex min-h-screen">
<aside class="hidden md:block w-56 bg-slate-900 text-slate-300 p-4">
<?php foreach ($menus as $key => $label): ?>
<a href="?page=<?= $key ?>"
class="block px-3 py-2 rounded mb-1 <?= $key === $active
? 'bg-indigo-600 text-white'
: 'hover:bg-slate-800' ?>">
<?= $label ?>
</a>
<?php endforeach; ?>
</aside>
<main class="flex-1 p-4 md:p-8">
<?php } ?>
<?php
function render_footer(): void { ?>
</main>
</div>
</body>
</html>
<?php } ?>
`hidden md:block` 这一句就完成了响应式:手机上侧边栏收起,桌面端展开。列表页和表单页只要在开头调 `render_header('标题', 'posts')`,结尾调 `render_footer()`。
第四步:列表页用表格 + 分页
后台 80% 的页面都是「一页表格」。用 PDO 预处理做分页查询,前端只负责渲染:
$page = max(1, (int)($_GET['p'] ?? 1));
$size = 20;
$offset = ($page - 1) * $size;
$stmt = $pdo->prepare("SELECT id,title,status FROM posts ORDER BY id DESC LIMIT ? OFFSET ?");
$stmt->bindValue(1, $size, PDO::PARAM_INT);
$stmt->bindValue(2, $offset, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
输出时用 Tailwind 的表格类:
<table class="w-full bg-white rounded-lg shadow text-sm">
<thead class="bg-slate-50 text-slate-500">
<tr><th class="text-left p-3">ID</th><th class="text-left p-3">标题</th><th class="text-left p-3">操作</th></tr>
</thead>
<tbody>
<?php foreach ($rows as $r): ?>
<tr class="border-t hover:bg-slate-50">
<td class="p-3"><?= (int)$r['id'] ?></td>
<td class="p-3"><?= htmlspecialchars($r['title']) ?></td>
<td class="p-3">
<a class="text-indigo-600 hover:underline" href="?page=edit&id=<?= (int)$r['id'] ?>">编辑</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
注意:所有从数据库出来的文本,输出到 HTML 前必须过 `htmlspecialchars()`。后台是 XSS 重灾区,因为管理员能看到全部用户提交的内容,一个未转义的用户昵称就能偷走管理员会话。
第五步:表单页处理提交
表单页做两件事:GET 时回填数据,POST 时校验并写库。
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!hash_equals($_SESSION['csrf'] ?? '', $_POST['csrf'] ?? '')) {
exit('非法请求');
}
$title = trim($_POST['title'] ?? '');
if ($title === '') { $error = '标题不能为空'; }
else {
$stmt = $pdo->prepare("UPDATE posts SET title=? WHERE id=?");
$stmt->execute([$title, (int)$_POST['id']]);
header('Location: ?page=posts'); exit;
}
}
表单控件用几个固定类名就够了:`w-full border rounded px-3 py-2 focus:ring-2 focus:ring-indigo-500`。按钮用 `bg-indigo-600 text-white px-4 py-2 rounded hover:bg-indigo-700`。这样全站视觉统一,不用单独写 CSS 文件。
注意:`button` 标签写在 `<form>` 里默认就是 submit,如果只是想放个「取消」链接,用 `<a>` 而不是 `<button>`,否则会误提交。
第六步:避开动态类名这个坑
这是新手最容易卡住的地方:Tailwind 只扫描源码里出现的完整类名,`class="bg-<?= $color ?>-500"` 拼出来的类它扫不到,样式会直接失效。
解决办法只有两个:一是把类名写成完整的映射数组,比如 `['ok' => 'bg-green-500', 'warn' => 'bg-amber-500']`;二是改用编译版并在配置文件里加 safelist。状态色、权限角标这类场景优先用第一种,简单可靠。
小结
- 目录先定好:入口做鉴权,布局做 `render_header()` / `render_footer()` 两个函数,其余页面只写内容区。
- 调试用 CDN 引 Tailwind,上线换 CLI 编译出的静态 CSS,避免闪烁和依赖联网。
- 响应式靠 `hidden md:block` 这类断点前缀,一套 HTML 同时适配手机和桌面。
- 数据库查询用 PDO 预处理绑参数,输出一律 `htmlspecialchars()`,表单提交先验 CSRF。
- 类名不要用变量拼接,改成完整类名映射表,否则 Tailwind 扫不到。