主题
删除管理员与使用 sweetalert2 优化确认框(本地存储 2/3)
本页对应 课堂视频,共 5 段视频,约 53 分钟。
学习目标
- 能使用
setItem、getItem、removeItem、clear完成localStorage的存取、删除与清空,并在同源页面中读取验证结果。 - 能区分
localStorage与sessionStorage的作用域和生命周期,并说明二者与cookie在存储大小、设置方式、过期时间、请求携带和安全性上的差异。 - 能通过表单
submit事件阻止默认提交,获取账号密码并完成非空校验,调用登录 API 后将账号写入localStorage并跳转到管理员列表页。 - 能在页面组件中读取
localStorage中的账号信息,并在欢迎区域正确显示登录账号名。 - 能实现退出登录:清空本地存储并跳转登录页;同时在入口处进行全局登录验证,未登录访问受保护页面时自动跳转到登录页。
知识点
删除功能要解决什么问题
这一节紧接管理员列表:列表已经能通过 getAdminExec 请求数据并渲染到 #amdinListBox,接下来要让每一行的“删除”按钮真正可用。为什么需要单独处理?因为管理员列表不是写死的 HTML,而是每次请求后把组件渲染结果整体塞进容器:
js
document.querySelector('#amdinListBox').innerHTML = AdminTabelComponent({adminList: res.data})这意味着列表里的删除按钮会随着刷新不断重建。怎么想?如果每次渲染后逐个给删除按钮绑定 click,代码会重复,而且列表一刷新,旧按钮被替换,绑定也要重做。更稳的做法是把事件监听挂在一直存在的父容器 #amdinListBox 上,利用事件冒泡,在父容器里判断“这次点击是不是删除按钮”。怎么做?在默认导出里给列表容器监听 click,回调统一交给 deleteAdminExec。易错点是父容器必须和模板中的 id 一致,笔记代码里是 #amdinListBox,注意不是 adminListBox,拼写要和模板保持一致。
用事件委托判断删除按钮
deleteAdminExec 的第一件事不是直接删除,而是判断事件目标:
js
if (event.target.classList.contains('btn-danger')) {为什么需要判断?因为 #amdinListBox 里不只有删除按钮,可能还有表格、文本、其他按钮;父容器监听到的点击不一定来自删除按钮。怎么想?删除按钮带有 btn-danger 类,点击它时 event.target 就是按钮本身,于是可以用 classList.contains('btn-danger') 做入口判断。怎么做?只有判断通过,才进入确认框和接口调用。易错点:如果按钮内部还包了 span 或图标,event.target 可能变成子元素,此时 classList 不含 btn-danger,判断会失效;更稳妥的写法是用 event.target.closest('.btn-danger'),但本节笔记代码没有展开这一层,先按现有代码理解。另一个易错点是 dataset.id 对应模板里的 data-id,如果模板写成 data-id,这里才能取到;写成 dataId 或 id 都会取不到。
确认后调用删除接口并刷新
判断是删除按钮后,代码先弹确认框:
js
if (confirm('确定删除?')) {
deleteAdmin(event.target.dataset.id)
.then(res => {
alert('删除成功!');
getAdminExec();
})
}为什么需要确认?删除是不可逆操作,误点会造成数据丢失。怎么想?用户确认后才把该行按钮上的 data-id 传给后端删除接口;删除成功后提示,并重新请求列表,让页面数据与后端一致。怎么做?deleteAdmin 从 ../api/admin 导入,参数是 event.target.dataset.id;成功回调里执行 alert 和 getAdminExec()。易错点:删除后一定要刷新列表,否则页面还显示已删除数据;confirm 返回布尔值,取消时不能继续调用接口;本节代码只写了成功回调,失败分支没有展开,实际项目中要依赖已有请求封装或拦截器处理错误,不能误以为删除一定成功。
完整控制器代码
下面保留本节笔记中的控制器代码,删除逻辑集中在 deleteAdminExec 和默认导出中的列表监听:
js
// 导入 toastr
import toastr from 'toastr';
import '../../node_modules/toastr/build/toastr.css';
// 导入发送请求的函数
import {postAdmin, getAdmin, deleteAdmin} from '../api/admin';
// 导入管理员列表表格的组件
import AdminTabelComponent from '../components/AdminTable';
// 导入模板页
import adminV from '@/views/admin';
// 执行添加管理员账号
const addAdminExec = () => {
// 获取表单数据
const adminName = document.adminForm.adminName.value.trim();
const passWord = document.adminForm.passWord.value.trim();
const rePassWord = document.adminForm.rePassWord.value.trim();
// 管理员账号只能由英文字母组成 ^[a-zA-Z]+$
if (adminName.search(/^[a-zA-Z]+$/) === -1) {
toastr.error('管理员账号只能由英文字母组成!');
return;
}
// 密码是6-18位的数字、字母、下划线 ^\w{6,18}$
if (passWord.search(/^\w{6,18}$/) === -1) {
toastr.error('密码必须由6到18位数字、字母、下划线组成');
return;
}
// 确认密码
if (passWord !== rePassWord) {
toastr.error('两次密码不一致');
return;
}
// 调用后端API 添加管理员
postAdmin({
adminName,
passWord
})
.then( res => {
toastr.success('管理员添加成功');
// 关闭模态框
$('#addAdminModal').modal('hide');
// 清空表单
document.adminForm.reset();
// 更新列表
getAdminExec();
});
}
// 获取管理员信息 并渲染页面
const getAdminExec = () => {
// 向后端API请求
getAdmin()
.then(res => {
// 将数据发送到管理员表格组件中 将组件设置到模板的指定位置
document.querySelector('#amdinListBox').innerHTML = AdminTabelComponent({adminList: res.data})
});
}
// 执行管理员的删除
const deleteAdminExec = event => {
// 判断目标元素是否是删除按钮
if (event.target.classList.contains('btn-danger')) {
// 弹出确认框
if (confirm('确定删除?')) {
// 调用后端API执行删除
deleteAdmin(event.target.dataset.id)
.then(res => {
alert('删除成功!');
// 更新列表
getAdminExec();
})
}
}
};
export default (req, res) => {
// 加载模板内容
res.render(adminV());
// 获取管理员信息
getAdminExec();
// 给添加按钮监听click事件
document.querySelector('#addAdminBtn').addEventListener('click', addAdminExec)
// 给管理员列表中的删除按钮监听单击事件 事件位置
document.querySelector('#amdinListBox').addEventListener('click', deleteAdminExec)
}易错点清单
第一,#amdinListBox 的拼写要和模板完全一致,笔记里是 amdin,不要擅自改成 admin。第二,删除按钮必须有 btn-danger 类和 data-id,否则判断或取 id 会失败。第三,列表是动态渲染的,删除监听要挂在父容器上,不要写成给每个按钮单独绑定。第四,删除成功后要调用 getAdminExec() 刷新列表。第五,注释里的“事件位置”实际指把事件监听放在列表容器上,不是给按钮写 onclick。本节转写稿随后转向登录与本地存储,删除管理员这一节的重点仍是:事件委托、确认、调用 deleteAdmin、刷新列表。
本节要解决的问题与依赖安装
为什么需要:删除管理员是不可逆操作,直接点按钮就发请求风险很高;原生 confirm 样式固定、交互生硬。用 sweetalert2 可以把“确认删除”和“删除成功”做成统一、可配置的弹框。
怎么想:sweetalert2 是运行时产品依赖,不是构建工具依赖,所以安装时不要加 -D。安装后以 swal 名字导入,调用 swal.fire({...}),它返回 Promise,确认结果在 result.isConfirmed 中。
怎么做:
shell
npm install sweetalert2js
import swal from 'sweetalert2';易错点:不要误装成开发依赖;导入名要与调用名一致;本节笔记没有额外导入 sweetalert2 的 CSS,按笔记保留即可,不要擅自添加不存在的路径。
删除管理员前先弹确认框
为什么需要:列表里的删除按钮是事件委托绑定在 #amdinListBox 上的,点击后不能立刻调用删除 API,否则用户误点就没有挽回机会。
怎么想:先判断 event.target.classList.contains('btn-danger'),确认点的是删除按钮;再取 event.target.dataset.id 作为要删除的 ID;然后 swal.fire 弹出“确定删除?”,只有 result.isConfirmed 为真才执行 deleteAdmin。
怎么做:核心代码如下,注意确认框的 showCancelButton、确认/取消文案。
js
const deleteAdminExec = event => {
if (event.target.classList.contains('btn-danger')) {
swal.fire({
title: '确定删除?',
icon: 'warning',
showCancelButton: true,
confirmButtonText: '确定',
cancelButtonText: '取消'
}).then((result) => {
if (result.isConfirmed) {
deleteAdmin(event.target.dataset.id)
.then(res => {
getAdminExec();
swal.fire({
title: '删除成功!',
icon: 'success'
})
})
}
})
}
};易错点:不要在 if (event.target.classList.contains('btn-danger')) 里直接删除;要等 isConfirmed;dataset.id 对应模板里的 data-id;#amdinListBox 是笔记原拼写,必须和模板 ID 一致,不要想当然改成 adminListBox。转写稿里调试表单提交时强调“先看控制台、再检查拼写和函数是否真正调用”,这里排查“确认框不弹”也可按同样顺序:看控制台报错、看事件目标、看 swal.fire 是否执行。
删除成功后的反馈与列表刷新
为什么需要:删除成功后如果只弹提示不刷新列表,页面数据会与后端不一致;如果只刷新不提示,用户不确定操作是否成功。
怎么想:在 deleteAdmin(...).then 中先调用 getAdminExec() 重新拉取列表并渲染,再 swal.fire 提示成功。成功提示不需要取消按钮。
怎么做:
js
deleteAdmin(event.target.dataset.id)
.then(res => {
getAdminExec();
swal.fire({
title: '删除成功!',
icon: 'success'
})
})易错点:不要忘记 getAdminExec();成功提示不要复用删除确认框的 showCancelButton;deleteAdmin 的失败分支笔记未展示,实际项目可按已有 API 封装和拦截器机制处理,不要在本节代码里凭空编造。
与 toastr 的分工:轻提示和强确认
为什么需要:添加管理员时的账号、密码、确认密码校验属于轻量错误提示,用 toastr 更合适;删除确认和删除成功属于需要用户注意的强反馈,用 swal 更合适。
怎么想:toastr.error 用于校验失败,toastr.success 用于添加成功;swal.fire 用于删除确认和删除成功。两者可以共存。
怎么做:导入 toastr 和样式,校验处继续用 toastr.error:
js
import toastr from 'toastr';
import '../../node_modules/toastr/build/toastr.css';js
if (adminName.search(/^[a-zA-Z]+$/) === -1) {
toastr.error('管理员账号只能由英文字母组成!');
return;
}易错点:toastr 的 CSS 只导入一次;路径按笔记保留;不要把校验失败也做成 swal 确认框,交互会过重。转写稿里提到的“蓝金器/显应蓝电器”应理解为响应拦截器,“库ook/酷比”是 cookie,“三信”是 session,这些是其他知识点的同音转写,本节不展开,避免混淆。
完整代码与检查清单
js
// 导入 toastr
import toastr from 'toastr';
import '../../node_modules/toastr/build/toastr.css';
// 导入 sweetalert2
import swal from 'sweetalert2';
// 导入发送请求的函数
import { postAdmin, getAdmin, deleteAdmin } from '../api/admin';
// 导入管理员列表表格的组件
import AdminTabelComponent from '../components/AdminTable';
// 导入模板页
import adminV from '@/views/admin';
// 执行添加管理员账号
const addAdminExec = () => {
// 获取表单数据
const adminName = document.adminForm.adminName.value.trim();
const passWord = document.adminForm.passWord.value.trim();
const rePassWord = document.adminForm.rePassWord.value.trim();
// 管理员账号只能由英文字母组成 ^[a-zA-Z]+$
if (adminName.search(/^[a-zA-Z]+$/) === -1) {
toastr.error('管理员账号只能由英文字母组成!');
return;
}
// 密码是6-18位的数字、字母、下划线 ^\w{6,18}$
if (passWord.search(/^\w{6,18}$/) === -1) {
toastr.error('密码必须由6到18位数字、字母、下划线组成');
return;
}
// 确认密码
if (passWord !== rePassWord) {
toastr.error('两次密码不一致');
return;
}
// 调用后端API 添加管理员
postAdmin({
adminName,
passWord
})
.then(res => {
toastr.success('管理员添加成功');
// 关闭模态框
$('#addAdminModal').modal('hide');
// 清空表单
document.adminForm.reset();
// 更新列表
getAdminExec();
});
}
// 获取管理员信息 并渲染页面
const getAdminExec = () => {
// 向后端API请求
getAdmin()
.then(res => {
// 将数据发送到管理员表格组件中 将组件设置到模板的指定位置
document.querySelector('#amdinListBox').innerHTML = AdminTabelComponent({ adminList: res.data })
});
}
// 执行管理员的删除
const deleteAdminExec = event => {
// 判断目标元素是否是删除按钮
if (event.target.classList.contains('btn-danger')) {
// 弹出确认框
swal.fire({
title: '确定删除?',
icon: 'warning',
showCancelButton: true,
confirmButtonText: '确定',
cancelButtonText: '取消'
}).then((result) => {
if (result.isConfirmed) {
// 调用后端API执行删除
deleteAdmin(event.target.dataset.id)
.then(res => {
// 更新列表
getAdminExec();
// 弹框提示删除成功
swal.fire({
title: '删除成功!',
icon: 'success'
})
})
}
})
}
};
export default (req, res) => {
// 加载模板内容
res.render(adminV());
// 获取管理员信息
getAdminExec();
// 给添加按钮监听click事件
document.querySelector('#addAdminBtn').addEventListener('click', addAdminExec)
// 给管理员列表中的删除按钮监听单击事件 事件位置
document.querySelector('#amdinListBox').addEventListener('click', deleteAdminExec)
}检查清单:依赖是否装成产品依赖;swal 是否导入;删除是否先确认再请求;result.isConfirmed 是否判断;getAdminExec() 是否在删除成功后调用;#amdinListBox、data-id、btn-danger 是否与模板一致。
从控制器串起登录流程
为什么需要:登录要把“表单输入、接口请求、登录后跳转”串起来,不能只写模板。
怎么想:控制器负责渲染登录模板、监听表单提交;提交时先阻止默认行为,再取表单值、做非空校验、调用 API,成功后保存账号名并跳转。
怎么做:src/controllers/login.js:
js
import toastr from 'toastr';
// 导入模板
import loginV from '@/views/login';
// 导入api函数
import {postLogin} from '../api/login';
// 执行登录的函数
const postLoginExec = event => {
// 阻止默认行为
event.preventDefault();
// 获取表单数据
const adminName = document.loginForm.adminName.value.trim();
const passWord = document.loginForm.passWord.value.trim();
// 账号不能为空
if (adminName.length === 0) {
toastr.error('请填写管理员账号!');
return;
}
// 密码不能为空
if (passWord.length === 0) {
toastr.error('密码不能为空!');
return;
}
// 使用后端API 发送数据执行登录
postLogin({
adminName,
passWord
})
.then(res => {
// 将用户信息记录到 localStorage
localStorage.setItem('adminName', adminName);
// 跳转管理员列表页
router.go('/index/admin');
});
}
export default (req, res) => {
// 渲染模板
res.render(loginV());
// 给表单监听提交事件
document.loginForm.addEventListener('submit', postLoginExec)
}易错点:preventDefault 拼写和大小写必须准确;document.loginForm 依赖模板里 form name="loginForm";adminName、passWord 的字段名要与输入框 name 一致;toastr 要先导入。
模板里给表单和输入框起名
为什么需要:控制器要通过 document.loginForm.adminName、document.loginForm.passWord 取值,模板必须提供对应的 name。
怎么想:表单用 name="loginForm",两个输入框分别用 name="adminName"、name="passWord";提交按钮保持 type="submit",但提交事件交给 JS 处理。
怎么做:src/views/login.ejs:
ejs
<div class="wrapper">
<div class="login-page">
<div class="login-box">
<!-- /.login-logo -->
<div class="card">
<div class="card-body login-card-body">
<p class="login-box-msg">尚硅谷广告后台管理系统</p>
<form name="loginForm">
<div class="input-group mb-3">
<input name="adminName" type="text" class="form-control" placeholder="管理员账号">
<div class="input-group-append">
<div class="input-group-text">
<span class="fas fa-user"></span>
</div>
</div>
</div>
<div class="input-group mb-3">
<input name="passWord" type="password" class="form-control" placeholder="请输入密码">
<div class="input-group-append">
<div class="input-group-text">
<span class="fas fa-lock"></span>
</div>
</div>
</div>
<div class="row">
<!-- /.col -->
<div class="col-12">
<button type="submit" id="loginBtn" class="btn btn-primary btn-block">登陆</button>
</div>
<!-- /.col -->
</div>
</form>
</div>
<!-- /.login-card-body -->
</div>
</div>
</div>
</div>易错点:name 写错会导致取值 undefined;给按钮监听 click 仍可能触发表单默认提交,所以这里监听 form 的 submit 并阻止默认行为。
API 只做薄封装
为什么需要:把请求地址和请求实例集中到 API 层,控制器只关心“传什么、成功后做什么”。
怎么想:登录是 POST,请求体包含 adminName、passWord,路径为 /login。
怎么做:src/api/login.js:
js
// 导入实例
import advServer from '../request/advserver';
export const postLogin = body => {
return advServer.post('/login', body);
}易错点:路径、请求方法、字段名要与后端一致;不要把 adminName 写成 username。
登录成功后存账号名并显示
为什么需要:登录成功后跳到后台页面,侧边栏要显示“欢迎您:账号名”,所以要把账号名保存到浏览器本地。
怎么想:项目用 localStorage 存 adminName,后续页面可直接读取;侧边栏组件无需再层层传参。
怎么做:成功回调里执行 localStorage.setItem('adminName', adminName),再 router.go('/index/admin')。侧边栏 src/components/Sidebar.ejs:
ejs
<aside class="main-sidebar sidebar-dark-primary elevation-4">
<!-- Brand Logo -->
<a href="index3.html" class="brand-link">
<img src="/img/logo.png" alt="广告管理LOGO" class="brand-image img-circle elevation-3" style="opacity: .8">
<span class="brand-text font-weight-light">硅谷后台管理系统</span>
</a>
<!-- Sidebar -->
<div class="sidebar">
<!-- Sidebar user panel (optional) -->
<div class="user-panel mt-3 pb-3 mb-3 d-flex">
<div class="info">
<a href="#" class="d-block">欢迎您:<%= localStorage.getItem('adminName') %></a>
</div>
</div>
<!-- Sidebar Menu -->
<nav class="mt-2">
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
<!-- 菜单列表 -->
<% data.routes.filter(item=>item.isNav).forEach(item => { %>
<li class="nav-item menu-open">
<a href="javascript:;" class="nav-link <%= data.url === item.path ? 'active' : '' %>" onclick="router.go('<%= item.path %>')">
<i class="nav-icon fas <%= item.icon %>"></i>
<p><%= item.title %></p>
</a>
</li>
<% }) %>
</ul>
</nav>
<!-- /.sidebar-menu -->
</div>
<!-- /.sidebar -->
</aside>易错点:模板里直接读 localStorage 时,未登录会是 null;本节只完成“登录并显示账号名”,登录拦截应另做。存的是输入账号名,不是 token。
调试过程:表单为什么还在提交
转写稿里先给表单监听 submit,点击后页面仍然提交。排查时发现阻止默认行为写成了类似 event.PreventDefault() 的错误形式,Default 被当成函数调用,实际没有调用到 preventDefault。改成 event.preventDefault() 后,表单不再刷新。另一个调试点是错误账号:请求 HTTP 状态仍是 200,但响应体里 msg 是账号或密码错误,被响应拦截器拦截,因此 .then 不会执行;只有正确账号才进入成功回调。易错点:不要只看 200,要看响应体;preventDefault 必须小写开头并加括号。
退出登录:按钮在组件里,逻辑在控制器里接
为什么需要:登录成功后,用户信息被写进 localStorage,页面能显示“欢迎您 xxx”。但用户点“退出登陆”时,如果只跳回登录页而不清本地状态,下一次进入系统仍可能被当成已登录,所以退出必须同时做两件事:清空本地登录信息、跳转登录页。
怎么想:退出按钮写在 Header.ejs 组件里,而组件本身不直接处理业务。转写稿里明确说,这个按钮被 index 控制器导入并渲染,因此监听逻辑放在 src/controllers/index 中。先给按钮一个稳定 id,再在控制器里查询并绑定 click。转写稿还提到,按钮外层套了 <a>,为了避免 <a> 抢行为,可把它的 href 处理成 javascript:;;笔记代码保留原样,重点是按钮的 id="logoutBtn"。
怎么做:组件代码如下。
ejs
<nav class="main-header navbar navbar-expand navbar-white navbar-light">
<!-- Left navbar links -->
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" data-widget="pushmenu" href="#" role="button"><i class="fas fa-bars"></i></a>
</li>
</ul>
<!-- Right navbar links -->
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" data-widget="control-sidebar" data-slide="true" href="javascript:;" role="button">
<button type="button" id="logoutBtn" class="btn btn-block btn-danger btn-sm">退出登陆</button>
</a>
</li>
</ul>
</nav>控制器中导入 Header 组件,并在渲染后监听退出按钮:
js
// 导入组件
import HeaderComponent from '@/components/Header';
import SidebarComponent from '@/components/Sidebar';
import FooterComponent from '@/components/Footer';
import ContentComponent from '@/components/Content';
// 导入模板
import indexV from '@/views/index';
// 导入路由配置对象
import routes from '../routes';
export default ({url}, res, next) => {
// next函数可以 让二级路由的页面内容 呈现该该路由页面内容的内部
next(indexV({
// 页头组件
Header: HeaderComponent(),
// 侧边栏组件
Sidebar: SidebarComponent({
url,
routes
}),
// 页脚组件
Footer: FooterComponent(),
// 主要内容组件
Content: ContentComponent({
title: routes.find(item=>item.path===url).title,
subRouteContent: res.subRoute()
}),
}));
// 点击退出登录按钮
document.querySelector('#logoutBtn').addEventListener('click', () => {
// 清空 localStorage
localStorage.clear();
// 跳转到登录页面
router.go('/login');
});
}易错点:id 必须和查询选择器一致;router 要在入口文件暴露到 window.router 后才能直接使用;localStorage.clear() 会清空本站 localStorage,本项目当前只存登录名,所以可以这样用;如果只想删登录名,也可用 removeItem('adminName')。转写稿里调试时出现过“点了还有用”的情况,本质是监听没绑上或默认行为没处理,先检查 id、查询结果和事件是否真正执行。
全局登录验证:在入口处拦住未登录访问
为什么需要:只做退出登录还不够。转写稿里演示过:退出后手动访问 index 仍能进来,只是拿不到数据。不能靠用户自觉登录,必须在应用入口做全局验证:没有登录信息就跳登录页。
怎么想:验证要放在“最开始的地方”,也就是 src/app.js 创建路由、暴露 window.router 之后、遍历 routes 注册路由之前。判断依据是登录成功时写入的 localStorage 键,转写稿中使用的是 adminName。键名必须和登录写入时一致。
怎么做:入口文件代码如下。
js
// 导入
import SMERouter from 'sme-router'
// 导入样式
import './assets/less/app.less'
// 导入路由配置
import routes from './routes';
// 创建实例
// 默认是 hash 模式,特点 http://adv.manage.fuming.site/#/index/advlist
// 可以改为 history 模式,需要第二个参数设置为 html5 特点 http://adv.manage.fuming.site/index/advlist
const router = new SMERouter('app', 'html5');
// 将 router 暴露到全局
window.router = router;
// 如果没有登录 跳转到登录页
if (!localStorage.getItem('adminName')) {
router.go('/login');
}
// 遍历路由配置 设置路由
routes.forEach(routeItem => {
router.route(routeItem.path, routeItem.element);
})易错点:验证代码不要放到路由注册之后,否则可能先进入页面再跳转;getItem('adminName') 的键名要和登录成功时 setItem 的键名完全一致;router.go('/login') 依赖全局 router,所以先执行 window.router = router。转写稿中反复纠正的同音词也要注意:Ajax 请求、cookie、session、localStorage、sessionStorage、会话、响应拦截器。本节的核心就是:退出时清状态并回登录页,入口处统一验证,未登录不得进入业务页面。
课堂画面





代码示例
示例一:管理员删除 + sweetalert2 确认框(src/controllers/admin.js)
js
// src/controllers/admin.js
// 导入 toastr 及其样式
import toastr from 'toastr';
import '../../node_modules/toastr/build/toastr.css';
// 导入 sweetalert2
import swal from 'sweetalert2';
// 导入发送请求的函数
import { postAdmin, getAdmin, deleteAdmin } from '../api/admin';
// 导入管理员列表表格的组件
import AdminTabelComponent from '../components/AdminTable';
// 导入模板页
import adminV from '@/views/admin';
// 执行添加管理员账号
const addAdminExec = () => {
// 获取表单数据
const adminName = document.adminForm.adminName.value.trim();
const passWord = document.adminForm.passWord.value.trim();
const rePassWord = document.adminForm.rePassWord.value.trim();
// 管理员账号只能由英文字母组成 ^[a-zA-Z]+$
if (adminName.search(/^[a-zA-Z]+$/) === -1) {
toastr.error('管理员账号只能由英文字母组成!');
return;
}
// 密码是 6-18 位的数字、字母、下划线 ^\w{6,18}$
if (passWord.search(/^\w{6,18}$/) === -1) {
toastr.error('密码必须由6到18位数字、字母、下划线组成');
return;
}
// 确认密码
if (passWord !== rePassWord) {
toastr.error('两次密码不一致');
return;
}
// 调用后端 API 添加管理员
postAdmin({
adminName,
passWord
})
.then(res => {
toastr.success('管理员添加成功');
// 关闭模态框
$('#addAdminModal').modal('hide');
// 清空表单
document.adminForm.reset();
// 更新列表
getAdminExec();
});
};
// 获取管理员信息 并渲染页面
const getAdminExec = () => {
// 向后端 API 请求
getAdmin()
.then(res => {
// 将数据发送到管理员表格组件中,将组件设置到模板的指定位置
document.querySelector('#amdinListBox').innerHTML =
AdminTabelComponent({ adminList: res.data });
});
};
// 执行管理员的删除
const deleteAdminExec = event => {
// 判断目标元素是否是删除按钮
if (event.target.classList.contains('btn-danger')) {
// 弹出确认框
swal.fire({
title: '确定删除?',
icon: 'warning',
showCancelButton: true,
confirmButtonText: '确定',
cancelButtonText: '取消'
}).then((result) => {
if (result.isConfirmed) {
// 调用后端 API 执行删除
deleteAdmin(event.target.dataset.id)
.then(res => {
// 更新列表
getAdminExec();
// 弹框提示删除成功
swal.fire({
title: '删除成功!',
icon: 'success'
});
});
}
});
}
};
export default (req, res) => {
// 加载模板内容
res.render(adminV());
// 获取管理员信息
getAdminExec();
// 给添加按钮监听 click 事件
document.querySelector('#addAdminBtn').addEventListener('click', addAdminExec);
// 给管理员列表中的删除按钮监听单击事件(事件委托,事件位置在外层容器)
document.querySelector('#amdinListBox').addEventListener('click', deleteAdminExec);
};关键行与易错点
- 删除按钮是动态渲染出来的,所以事件监听挂在外层
#amdinListBox上做事件委托,不能在getAdminExec之前直接给.btn-danger绑定。 event.target.classList.contains('btn-danger')判断目标元素,若删除按钮内部还有<i>图标,event.target会变成图标而匹配失败——这是最常见的坑。deleteAdmin(event.target.dataset.id):id必须提前写在按钮的data-id属性上。swal.fire({...})返回 Promise,必须.then(result => { if (result.isConfirmed) {...} }),只有点“确定”才真正发请求,点“取消”什么都不做。- 删除成功后要先
getAdminExec()刷新列表,再弹成功提示,否则列表数据与界面不同步。
示例二:登录并显示账号名(src/api/login.js + src/views/login.ejs + src/controllers/login.js)
js
// src/api/login.js
// 导入实例
import advServer from '../request/advserver';
export const postLogin = body => {
return advServer.post('/login', body);
};ejs
<!-- src/views/login.ejs -->
<div class="wrapper">
<div class="login-page">
<div class="login-box">
<div class="card">
<div class="card-body login-card-body">
<p class="login-box-msg">尚硅谷广告后台管理系统</p>
<form name="loginForm">
<div class="input-group mb-3">
<input name="adminName" type="text" class="form-control" placeholder="管理员账号">
<div class="input-group-append">
<div class="input-group-text">
<span class="fas fa-user"></span>
</div>
</div>
</div>
<div class="input-group mb-3">
<input name="passWord" type="password" class="form-control" placeholder="请输入密码">
<div class="input-group-append">
<div class="input-group-text">
<span class="fas fa-lock"></span>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<button type="submit" id="loginBtn" class="btn btn-primary btn-block">登陆</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>js
// src/controllers/login.js
import toastr from 'toastr';
// 导入模板
import loginV from '@/views/login';
// 导入 api 函数
import { postLogin } from '../api/login';
// 执行登录的函数
const postLoginExec = event => {
// 阻止默认行为
event.preventDefault();
// 获取表单数据
const adminName = document.loginForm.adminName.value.trim();
const passWord = document.loginForm.passWord.value.trim();
// 账号不能为空
if (adminName.length === 0) {
toastr.error('请填写管理员账号!');
return;
}
// 密码不能为空
if (passWord.length === 0) {
toastr.error('密码不能为空!');
return;
}
// 使用后端 API 发送数据执行登录
postLogin({
adminName,
passWord
})
.then(res => {
// 将用户信息记录到 localStorage
localStorage.setItem('adminName', adminName);
// 跳转管理员列表页
router.go('/index/admin');
});
};
export default (req, res) => {
// 渲染模板
res.render(loginV());
// 给表单监听提交事件
document.loginForm.addEventListener('submit', postLoginExec);
};ejs
<!-- src/components/Sidebar.ejs(只展示显示账号名的关键片段) -->
<div class="user-panel mt-3 pb-3 mb-3 d-flex">
<div class="info">
<a href="#" class="d-block">欢迎您:<%= localStorage.getItem('adminName') %></a>
</div>
</div>关键行与易错点
- 登录按钮在
<form>内部且type="submit",所以监听submit事件而不是click,并且必须event.preventDefault()(注意要加括号调用,写成event.preventDefault不调用是无效的,表单照样刷新跳转)。 document.loginForm依赖<form name="loginForm">,document.adminForm同理——name写错就取不到表单,后面全部报undefined。- 空值校验用
.length === 0,字符串长度不可能小于 0,写<= 0没有意义。 - 登录成功后用
localStorage.setItem('adminName', adminName)保存状态;localStorage是window的属性,天生是全局变量,模板和组件里可直接用,不需要层层传参。 - 错误提示(账号密码错误)由响应拦截器统一拦截弹出,
then里只会走成功分支,所以不用写catch。 - 跳转用全局暴露的
router.go('/index/admin'),注意路径要和路由配置一致。
示例三:登录验证与退出登录(src/app.js + src/controllers/index.js + src/components/Header.ejs)
ejs
<!-- src/components/Header.ejs(关键片段:给退出按钮加 id) -->
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" data-widget="control-sidebar" data-slide="true" href="javascript:;" role="button">
<button type="button" id="logoutBtn" class="btn btn-block btn-danger btn-sm">退出登陆</button>
</a>
</li>
</ul>js
// src/controllers/index.js
// 导入组件
import HeaderComponent from '@/components/Header';
import SidebarComponent from '@/components/Sidebar';
import FooterComponent from '@/components/Footer';
import ContentComponent from '@/components/Content';
// 导入模板
import indexV from '@/views/index';
// 导入路由配置对象
import routes from '../routes';
export default ({ url }, res, next) => {
// next 函数可以让二级路由的页面内容呈现在该路由页面内容的内部
next(indexV({
// 页头组件
Header: HeaderComponent(),
// 侧边栏组件
Sidebar: SidebarComponent({
url,
routes
}),
// 页脚组件
Footer: FooterComponent(),
// 主要内容组件
Content: ContentComponent({
title: routes.find(item => item.path === url).title,
subRouteContent: res.subRoute()
})
}));
// 点击退出登录按钮
document.querySelector('#logoutBtn').addEventListener('click', () => {
// 清空 localStorage
localStorage.clear();
// 跳转到登录页面
router.go('/login');
});
};js
// src/app.js
import SMERouter from 'sme-router';
import './assets/less/app.less';
import routes from './routes';
// 创建实例(html5 模式)
const router = new SMERouter('app', 'html5');
// 将 router 暴露到全局
window.router = router;
// 全局登录验证:如果没有登录,跳转到登录页
if (!localStorage.getItem('adminName')) {
router.go('/login');
}
// 遍历路由配置 设置路由
routes.forEach(routeItem => {
router.route(routeItem.path, routeItem.element);
});关键行与易错点
- 退出按钮在
Header组件里,但Header是被controllers/index.js导入并渲染的,所以监听代码要写在 index 控制器里;组件文件本身只负责结构,不负责绑定。 <a href="javascript:;">用来去掉<a>的默认跳转,否则点按钮会顺带触发链接行为。- 退出时
localStorage.clear()是清空全部键值;如果只删一个账号名应使用localStorage.removeItem('adminName')。 - 登录验证必须放在入口文件
app.js的路由注册之前,这是全局拦截,不能只写在某个控制器里,否则用户手动改 hash 依然能进页面。 if (!localStorage.getItem('adminName')):getItem取不到时返回null,必须取反判断;直接比较=== undefined是永远不成立的。router.go('/login')与router.route(...)的区别:前者是“跳转”,后者是“注册路由”,写反了页面不会显示。
常见错误
- 删除按钮点图标没反应:现象是点击删除按钮内的图标时不触发删除;原因是事件委托中只判断
event.target.classList.contains('btn-danger'),实际目标可能是子元素;修法是使用event.target.closest('.btn-danger')并判断非空,再取dataset.id。 - sweetalert2 确认后流程乱序:现象是未确认也执行删除,或成功提示早于删除完成;原因是把异步
swal.fire().then()当同步代码写,未把删除逻辑放进确认分支;修法是把删除请求放进if (result.isConfirmed),并在删除请求完成后再提示成功、刷新列表。 - 登录表单仍刷新页面:现象是点击登录后页面跳走/参数丢失;原因是监听了按钮点击但未正确阻止表单默认提交,或
preventDefault写成属性未调用;修法是监听form的submit,首行调用event.preventDefault()。 - 未登录仍能进入后台页面:现象是清空登录态后直接访问
/index/admin仍可进入;原因是登录校验放在路由注册之后且未中断后续流程;修法是在入口最早处判断登录态,未登录时router.go('/login')后return,避免继续注册/渲染目标页。 - 退出登录误清全部缓存:现象是退出后其他页面临时数据也丢失;原因是直接
localStorage.clear();修法是只删除登录标识,如localStorage.removeItem('adminName'),确需全清时再明确说明。 - 动态列表刷新后删除失效:现象是首次删除正常,刷新列表后新按钮无事件;原因是给每个删除按钮单独绑定事件;修法是继续使用列表容器的委托监听,并在每次渲染后无需重新绑定。
练习
- 将管理员删除改为事件委托:要求点击按钮内任意子元素都能取到
data-id,并避免误触页面其他.btn-danger。 - 用 sweetalert2 实现删除流程:确认框选择取消时不发请求;确认后删除成功再刷新列表并提示成功。
- 完成登录表单提交:阻止默认提交,校验账号/密码非空,成功后保存登录标识并跳转管理员列表页。
- 实现退出登录与全局登录守卫:退出只清理登录标识;未登录访问任意后台路由时跳转登录页,并确保不继续渲染目标页面。
- 设计“写文章草稿”临时保存:用
sessionStorage定时保存文本域内容,刷新/重新打开当前会话时可恢复,并说明为何不选localStorage或cookie。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | localStorage浏览器本地存储 | 13:42 |
| 2 | sessionStorage浏览器会话本地存储 | 04:58 |
| 3 | localStorage、sessionStorage与cookie的区别 | 05:25 |
| 4 | 登录并显示账号名 | 19:42 |
| 5 | 登录验证和退出登录 | 09:48 |
小结
- 动态列表的交互优先用事件委托,删除类操作要用
closest定位真实按钮,避免子元素干扰。 - sweetalert2 的核心是 Promise 链,确认、请求、提示、刷新必须按异步顺序组织。
- 表单登录应监听
submit而非只监听按钮点击,preventDefault()必须作为函数调用。 - 登录态存储只是前端状态管理,不等于安全认证;敏感凭证不应长期明文放在本地存储。
- 全局登录守卫必须放在入口最早执行,并在跳转后中断后续流程。
- 退出登录应最小化清理,只移除当前业务需要的登录标识,避免误伤其他缓存。
localStorage、sessionStorage、cookie的选择取决于作用域、生命周期和是否随请求发送,不要混用。- 删除、登录、退出这类流程要同时验证成功、失败、取消和刷新后的状态一致性。