赞
踩
功能:
技术:
- 基于Bootstrap搭建网站标签和样式
- 集成wangEditor插件实现富文本编辑器
- 使用原生JS完成增删改查等业务
- 基于axios与黑马头条线上接口交互
- 使用axios拦截器进行权限判断
目录管理:建议这样管理,方便查找
目标:完成验证码登录,后端设置验证码默认为246810
原因:因为短袖接口不是免费的,防止攻击者恶意盗刷
步骤:
- // axios 公共配置
- // 基地址
- axios.defaults.baseURL = 'http://geek.itheima.net'
index.js
- /**
- * 目标1:验证码登录
- * 1.1 在 utils/request.js 配置 axios 请求基地址
- * 1.2 收集手机号和验证码数据
- * 1.3 基于 axios 调用验证码登录接口
- * 1.4 使用 Bootstrap 的 Alert 警告框反馈结果给用户
- */
- // 1.2 收集手机号和验证码数据
-
- document.querySelector('.btn').addEventListener('click', () => {
- const form = document.querySelector('.login-form')
- const data = serialize(form, { hash: true, empty: true})
- console.log(data)
- // 1.3 基于 axios 调用验证码登录接口
- axios({
- url: '/v1_0/authorizations',
- method: 'POST',
- data: data
- }).then( result => {
- // 1.4 使用 Bootstrap 的 Alert 警告框反馈结果给用户
- myAlert(true, '登录成功')
- console.log(result)
- }).catch(error => {
- myAlert(false, error.response.data.message)
- console.dir(error.response.data.message)
- })
- })
验证码登录流程
概念:访问权限的令牌,本质上是一串字符串
创建:正确登录后,由后端签发并返回
作用:判断是否有登录状态等,控制访问权限
注意:前端只能判断token有无,而后端才能判断token的有效性
目标:只有登录状态,才可以访问内容页面
步骤:
1.在utils/auth.js中判断无token令牌字符串,则强制跳转到登录页(手动修改地址栏测试)
2.在登录成功后,保存token令牌字符串到本地,再跳转到首页(手动修改地址栏测试)
- // 权限插件(引入到了除登录页面,以外的其他所有页面)
- /**
- * 目标1:访问权限控制
- * 1.1 判断无 token 令牌字符串,则强制跳转到登录页
- * 1.2 登录成功后,保存 token 令牌字符串到本地,并跳转到内容列表页面
- */
- // 1.1 判断无 token 令牌字符串,则强制跳转到登录页
- const token = localStorage.getItem('token')
- if (!token) {
- location.href = '../login/index.html'
- }
- /**
- * 目标1:验证码登录
- * 1.1 在 utils/request.js 配置 axios 请求基地址
- * 1.2 收集手机号和验证码数据
- * 1.3 基于 axios 调用验证码登录接口
- * 1.4 使用 Bootstrap 的 Alert 警告框反馈结果给用户
- */
-
- // 1.2 收集手机号和验证码数据
-
- document.querySelector('.btn').addEventListener('click', () => {
- const form = document.querySelector('.login-form')
- const data = serialize(form, { hash: true, empty: true})
- console.log(data)
- // 1.3 基于 axios 调用验证码登录接口
- axios({
- url: '/v1_0/authorizations',
- method: 'POST',
- data: data
- }).then( result => {
- // 1.4 使用 Bootstrap 的 Alert 警告框反馈结果给用户
- myAlert(true, '登录成功')
- console.log(result)
-
- // 登录成功后,保存 token 令牌字符串到本地,并跳转到内容列表页面
- localStorage.setItem('token', result.data.data.token)
- setTimeout(() => {
- // 延迟跳转,让alert警告框停留一会
- location.href = '../content/index.html'
- },1500)
-
-
- }).catch(error => {
- myAlert(false, error.response.data.message)
- console.dir(error.response.data.message)
- })
- })
token的作用?
- 判断用户是否有登录状态等
token的注意:
- 前端只能判断token的有无
- 后端通过解密可以提取token字符串的原始信息,判断有效性
需求:设置用户昵称
语法:axios可以在headers选项传递请求头参数
问题:很多接口,都需要携带token令牌字符串
解决:在请求拦截器统一设置公共headers选项
axios请求拦截器:发起请求之前,触发的配置函数,对请求参数进行额外配置
对应代码
- /**
- * 目标2:设置个人信息
- * 2.1 在 utils/request.js 设置请求拦截器,统一携带 token
- * 2.2 请求个人信息并设置到页面
- */
- // 2.2 请求个人信息并设置到页面
- axios({
- url: '/v1_0/user/profile'
- }).then(result => {
- console.log(result)
- const username = result.data.data.name
- document.querySelector('.nick-name').innerHTML = username
- })
- // 添加请求拦截器
- axios.interceptors.request.use(function (config) {
- // 在发送请求之前做些什么
- // 统一携带 token 令牌字符串在请求头上
- const token = localStorage.getItem('token')
- token && (config.headers.Authorization = `Bearer ${token}`)
-
- return config;
- }, function (error) {
- // 对请求错误做些什么
- return Promise.reject(error);
- })
总结
1.什么是axios请求拦截器?
发起请求之前,调用的一个函数,对请求参数进行设置
2.axios请求拦截器,什么时候使用?
有公共配置和设置时,统一设置在请求拦截器中
axios响应拦截器:响应回到 then/catch 之前,触发的拦截函数,对响应结果统一处理
例如:身份验证失败,统一判断并做处理
- // 添加响应拦截器
- axios.interceptors.response.use(function (response) {
- // 2xx 范围内前状态码都会触发该函数
- // 对响应数据做点什么
- return response;
- },function (error) {
- // 超出 2xx 范围的状态码都会触发该函数
- // 对响应错误数做点什么,例如:统一对 401 身份验证失败错误做出处理
- console.dir(error)
- if (error?.response?.status === 401) {
- alert('身份验证失败,请重新登录')
- localStorage.clear()
- location.href = '../login/index.html'
- }
- return Promise.reject(error)
- })
总结
1.什么是axios响应拦截器?
响应回到then/catch之前,触发的拦截函数,对响应结果统一处理
2.axios响应拦截器,什么时候触发成功/失败的回调函数?
状态为2xx触发成功回调,其他则触发失败的回调函数
目标:axios直接接收服务器返回的响应结果
讲解:其实就是在响应拦截器里,response.data把后台返回的数据直接取出来统一返回给所有使用这个axios函数的逻辑页面位置的 then 的形参上
好处:可以让逻辑页面少一层data就能拿到后端返回的真正数据对象
对应代码
- axios.interceptors.response.use(function (response) {
- // 2xx 范围内的状态码都会触发该函数。
- // 对响应数据做点什么,例如:直接返回服务器的响应结果对象
- const result = response.data
- return result
- }, function (error) {
- // 超出 2xx 范围的状态码都会触发该函数。
- // 对响应错误做点什么,例如:判断响应状态为 401 代表身份验证失败
- if (error?.response?.status === 401) {
- alert('登录状态过期,请重新登录')
- window.location.href = '../login/index.html'
- }
- return Promise.reject(error);
- })
富文本:带样式,多格式的文本,在前端一般使用标签配合内联样式实现
富文本编辑器:用于编写富文本内容的容器
目标:发布文章页,富文本编辑器的集成
使用:wangEditor插件
步骤:参考文档
对应代码
- // 富文本编辑器
- // 创建编辑器函数,创建工具栏函数
- const { createEditor, createToolbar } = window.wangEditor
-
- const editorConfig = {
- // 占位提示文字
- placeholder: '发布文章内容...',
- // 编辑器变化时回调函数
- onChange(editor) {
- // 获取富文本内容
- const html = editor.getHtml()
- console.log('editor content', html)
- // 也可以同步到 <textarea>
- // 为了后续快速收集整个表单内容做铺垫
- document.querySelector('.publish-content').value = html
- }
- }
-
- const editor = createEditor({
- // 创建位置
- selector: '#editor-container',
- // 默认内容
- html: '<p><br></p>',
- // 配置项
- config: editorConfig,
- // 配置集成模式(default 全部) (simple 简洁)
- mode: 'default', // or 'simple'
- })
-
- // 工具栏配置对象
- const toolbarConfig = {}
-
- // 创建工具栏
- const toolbar = createToolbar({
- // 为指定编辑器创建工具栏
- editor,
- // 工具栏创建的位置
- selector: '#toolbar-container',
- // 工具栏配置对象
- config: toolbarConfig,
- // 配置集成模式
- mode: 'default', // or 'simple'
- })
目标:展示频道列表,供用户选择
步骤:
- /**
- * 目标1:设置频道下拉菜单
- * 1.1 获取频道列表数据
- * 1.2 展示到下拉菜单中
- */
- // 1.1 获取频道列表数据
- async function setChannleList() {
- const res = await axios({
- url: '/v1_0/channels'
- })
- // 1.2 展示到下拉菜单中
- const htmlStr = `<option value="" selected="">请选择文章频道</option>` + res.data.channels.map(item => `<option value="${item.id}">${item.name}</option>`).join('')
- console.log(htmlStr)
- document.querySelector('.form-select').innerHTML = htmlStr
- }
- // 网页运行后,默认调用一次
- setChannleList()
目标:文章封面的设置
步骤:
注意:图片地址临时存储在img标签上,并未和文章关联保存
- /**
- * 目标2:文章封面设置
- * 2.1 准备标签结构和样式
- * 2.2 选择文件并保存在 FormData
- * 2.3 单独上传图片并得到图片 URL 网址
- * 2.4 回显并切换 img 标签展示(隐藏 + 号上传标签)
- */
- // 2.2 选择文件并保存在 FormData
- document.querySelector('.img-file').addEventListener('change', async e => {
- const file = e.target.files[0]
- const fd = new FormData()
- fd.append('image', file)
- // 2.3 单独上传图片并得到图片 URL 网址
- const res = await axios({
- url: '/v1_0/upload',
- method: 'POST',
- data: fd
- })
- console.log(res)
- // 2.4 回显并切换 img 标签展示(隐藏 + 号上传标签)
- const imgUrl = res.data.url
- document.querySelector('.rounded').src = imgUrl
- document.querySelector('.rounded').classList.add('show')
- document.querySelector('.place').classList.add('hide')
- })
-
- // 优化:点击 img 可以重新切换封面
- // 思路: img 点击 => 用JS方式触发文件选择元素 click 事件方法
- document.querySelector('.rounded').addEventListener('click', () => {
- document.querySelector('.img-file').click()
- })
目标:收集文章内容,并提交保存
步骤:
1.基于form-serialize插件收集表单数据对象
2.基于axios提交到服务器保存
3.调用Alert警告框反馈结果给用户
- /**
- * 目标3:发布文章保存
- * 3.1 基于 form-serialize 插件收集表单数据对象
- * 3.2 基于 axios 提交到服务器保存
- * 3.3 调用 Alert 警告框反馈结果给用户
- * 3.4 重置表单并跳转到列表页
- */
- // 3.1 基于 form-serialize 插件收集表单数据对象
- document.querySelector('.send').addEventListener('click', async e => {
- const form = document.querySelector('.art-form')
- const data = serialize(form, { hash: true, empty: true})
- console.log(data)
- // 发布文章的时候,不需要 id 属性,所以可以删除掉(id为了后续做编辑使用)
- delete data.id
- console.log(data)
- // 自己收集封面图片地址并保存到 data 对象中
- data.cover = {
- type: 1, // 封面类型
- images: [document.querySelector('.rounded').src] // 封面图书 URL网址
- }
-
- // 3.2 基于 axios 提交到服务器保存
- try {
- const res = await axios({
- url: '/v1_0/mp/articles',
- method:'POST',
- data: data
- })
- // 3.3 调用 Alert 警告框反馈结果给用户
- myAlert(true, '发布成功')
- // 3.4 重置表单并跳转到列表页
- form.reset()
- // 封面需要手动重置
- document.querySelector('.rounded').src = ''
- document.querySelector('.rounded').classList.remove('show')
- document.querySelector('.place').classList.remove('hide')
- // 富文本编辑器重置
- editor.setHtml('')
-
- setTimeout(() => {
- location.href = '../content/index.html'
- },1500)
-
- } catch (error) {
- console.dir(error)
- myAlert(false, error.response.data.message)
- }
-
- })
目标:获取文章列表并展示
步骤:
1.准备查询参数对象
2.获取文章列表数据
3.展示到指定的标签结构中
- /**
- * 目标1:获取文章列表并展示
- * 1.1 准备查询参数对象
- * 1.2 获取文章列表数据
- * 1.3 展示到指定的标签结构中
- */
- // 1.1 准备查询参数对象
- const queryObj = {
- status: '', // 文章状态(1-待审核,2-审核通过)空字符串-全部
- channel_id: '', // 文章频道id,空字符串-全部
- page: 1, // 当前页码
- per_page: 2 // 当前页面条数
- }
- async function setArtileList() {
- // 1.2 获取文章列表数据
- const res = await axios({
- url: '/v1_0/mp/articles',
- params: queryObj
- })
- console.log(res)
- // 1.3 展示到指定的标签结构中
- const htmlStr = res.data.results.map( item => `<tr>
- <td>
- <img src=" ${item.cover.type === 0 ? `https://img2.baidu.com/it/u=2640406343,1419332367&fm=253&fmt=auto&app=138&f=JPEG?w=708&h=500` : item.cover.images[0]}" alt="">
- </td>
- <td>${item.title}</td>
- <td>
- ${item.status === 1 ? `<span class="badge text-bg-primary">待审核</span>` : `<span class="badge text-bg-success">审核通过</span>`}
- </td>
- <td>
- <span>${ item.pubdate }</span>
- </td>
- <td>
- <span> ${ item.read_count } </span>
- </td>
- <td>
- <span> ${ item.comment_count } </span>
- </td>
- <td>
- <span> ${ item.like_count }</span>
- </td>
- <td>
- <i class="bi bi-pencil-square edit"></i>
- <i class="bi bi-trash3 del"></i>
- </td>
- </tr>
- `).join('')
- // console.log(htmlStr)
- document.querySelector('.art-list').innerHTML = htmlStr
- }
- setArtileList()
目标:根据筛选条件,获取匹配数据展示
步骤:
1.设置频道列表数据
2.监听筛选条件改变,保存查询信息到查询参数对象
3.点击筛选时,传递查询参数对象到服务器
4.获取匹配数据,覆盖到页面展示
- /**
- * 目标2:筛选文章列表
- * 2.1 设置频道列表数据
- * 2.2 监听筛选条件改变,保存查询信息到查询参数对象
- * 2.3 点击筛选时,传递查询参数对象到服务器
- * 2.4 获取匹配数据,覆盖到页面展示
- */
- async function setChannleList() {
- const res = await axios({
- url: '/v1_0/channels'
- })
- const htmlStr = `<option value="" selected="">请选择文章频道</option>` + res.data.channels.map(item => `<option value="${item.id}">${item.name}</option>`).join('')
- document.querySelector('.form-select').innerHTML = htmlStr
- }
- setChannleList()
- // 2.2 监听筛选条件改变,保存查询信息到查询参数对象
- // 筛选状态标记数字 -> change事件 -> 绑定到查询参数对象上
- document.querySelectorAll('.form-check-input').forEach(radio => {
- radio.addEventListener('change', e => {
- // console.log(e.target.value)
- queryObj.status = e.target.value
- })
- })
- // 筛选频道 id -> change事件 -> 绑定到查询参数对象上
- document.querySelector('.form-select').addEventListener('change', e =>
- {
- // console.log(e.target.value)
- queryObj.channel_id = e.target.value
- })
- // 2.3 点击筛选时,传递查询参数对象到服务器
- document.querySelector('.sel-btn').addEventListener('click', () => {
- // 2.4 获取匹配数据,覆盖到页面展示
- setArtileList()
- })
内容管理-分页功能
目标:完成文章列表,分页管理功能
步骤:
1.保存并设置文章总条数
2.点击下一页,做临界值判断,并切换页面参数请求最新数据
3.点击上一页,做临界值判断,并切换页面参数请求最新数据
- /**
- * 目标3:分页功能
- * 3.1 保存并设置文章总条数
- * 3.2 点击下一页,做临界值判断,并切换页码参数并请求最新数据
- * 3.3 点击上一页,做临界值判断,并切换页码参数并请求最新数据
- */
- // 3.2 点击下一页,做临界值判断,并切换页码参数并请求最新数据
- document.querySelector('.next').addEventListener('click', e => {
- // 当前页码小于最大页码数
- if (queryObj.page < Math.ceil(totalCount / queryObj.per_page)) {
- queryObj.page++
- document.querySelector('.page-now').innerHTML = `第${queryObj.page}页`
- setArtileList()
- }
- } )
- // 3.3 点击上一页,做临界值判断,并切换页码参数并请求最新数据
- document.querySelector('.last').addEventListener('click', e => {
- // 大于 1 的时候,才能翻到上一页
- if (queryObj.page > 1) {
- queryObj.page--
- document.querySelector('.page-now').innerHTML = `第${queryObj.page}页`
- }
- setArtileList()
- })
目标:完成删除文章功能
步骤:
- /**
- * 目标4:删除功能
- * 4.1 关联文章 id 到删除图标
- * 4.2 点击删除时,获取文章 id
- * 4.3 调用删除接口,传递文章 id 到服务器
- * 4.4 重新获取文章列表,并覆盖展示
- * 4.5 删除最后一页的最后一条,需要自动向前翻页
- */
- // 4.2 点击删除时,获取文章 id
- document.querySelector('.art-list').addEventListener('click', async e => {
- // 判断点击的是删除元素
- if (e.target.classList.contains('del')) {
- const delId = e.target.parentNode.dataset.id
- console.log(delId)
- // 4.3 调用删除接口,传递文章 id 到服务器
- const res = await axios({
- url: `v1_0/mp/articles/${delId}` ,
- method: 'DELETE'
- })
- console.log(res)
- // 4.4 重新获取文章列表,并覆盖展示
- setArtileList()
- }
- })
目标:在删除最后一页,最后一条时有Bug
1.删除成功时,判断DOM元素只剩一条,让当前页码 page--
2.注意,当前页码为1时不能继续向前翻页
3.重新设置页码数,获取最新列表展示
- // 4.5 删除最后一页的最后一条,需要自动向前翻页
- const children = document.querySelector('.art-list').children
- if (children.length === 1 && queryObj.page !== 1){
- queryObj.page--
- document.querySelector('.page-now').innerHTML = `第${queryObj.page }页`
- }
目标:编辑文章时,回显数据到表单
步骤:
- // 点击编辑时,获取文章 id,跳转到发布文章页面传递文章 id 过去
- document.querySelector('.art-list').addEventListener('click', e => {
- if (e.target.classList.contains('edit')) {
- const artId = e.target.parentNode.dataset.id
- console.log(artId)
- location.href = `../publish/index.html?id=${artId}`
- }
- })
对应代码:
- /**
- * 目标4:编辑-回显文章
- * 4.1 页面跳转传参(URL 查询参数方式)
- * 4.2 发布文章页面接收参数判断(共用同一套表单)
- * 4.3 修改标题和按钮文字
- * 4.4 获取文章详情数据并回显表单
- */
- ;(function(){
- // 4.2 发布文章页面接收参数判断(共用同一套表单)
- const paramsStr = location.search
- const params = new URLSearchParams(paramsStr)
- params.forEach(async (value, key) => {
- // 当前有要编辑的文章 id 被传入过来
- if (key === 'id'){
- // 4.3 修改标题和按钮文字
- document.querySelector('.title span').innerHTML = '修改文章'
- document.querySelector('.send').innerHTML = '修改'
- // 4.4 获取文章详情数据并回显表单
- const res = await axios({
- url: `/v1_0/mp/articles/${value}`
- })
- console.log(res)
- // 组织我仅仅需要的数据对象,为后续遍历回显到页面上做铺垫
- const dataObj = {
- channel_id: res.data.channel_id,
- title: res.data.title,
- rounded: res.data.cover.images[0], // 封面图片地址
- content: res.data.content,
- id: res.data.id
- }
- // 遍历数据对象属性,映射到页面元素上,快速赋值
- Object.keys(dataObj).forEach(key => {
- if(key === 'rounded'){
- // 封面设置
- if(dataObj[key]){
- // 有封面
- document.querySelector('.rounded').src = dataObj[key]
- document.querySelector('.rounded').classList.add('show')
- document.querySelector('.place').classList.add('hide')
- }
- } else if( key === 'content'){
- // 富文本内容
- editor.setHtml(dataObj[key])
- } else {
- // 用数据对象属性名,作为标签 name 属性选择器值来找到匹配的标签
- document.querySelector(`[name=${key}]`).value = dataObj[key]
- }
- })
- }
- })
- })();
目标:确认修改,保存文章到服务器
步骤:
1.判断按钮文字,区分业务(因为共用一套表单)
2.调用编辑文章接口,保存信息到服务器
3.基于Alert反馈结果消息给用户
- /**
- * 目标5:编辑-保存文章
- * 5.1 判断按钮文字,区分业务(因为共用一套表单)
- * 5.2 调用编辑文章接口,保存信息到服务器
- * 5.3 基于 Alert 反馈结果消息给用户
- */
- document.querySelector('.send').addEventListener('click', async e => {
- // 5.1 判断按钮文字,区分业务(因为共用一套表单)
- if (e.target.innerHTML !== '修改') return
- // 修改文章逻辑
- const form = document.querySelector('.art-form')
- const data = serialize(form, { hash: true, empty: true})
- // console.log(data)
- // 5.2 调用编辑文章接口,保存信息到服务器
- try {
- const res = await axios({
- url: `/v1_0/mp/articles/${data.id}`,
- method: 'PUT',
- data: {
- ...data,
- cover:{
- type: document.querySelector('.rounded').src ? 1 : 0,
- images: [document.querySelector('.rounded').src]
- }
- }
- })
- console.log(res)
- myAlert(true,'修改文章成功')
- } catch (error) {
- myAlert(false,error.response.data.message)
- }
- })
目标:完成退出登录效果
步骤:
1.绑定点击事件
2.清空本地缓存,跳转到登录页面
- /**
- * 目标3:退出登录
- * 3.1 绑定点击事件
- * 3.2 清空本地缓存,跳转到登录页面
- */
- document.querySelector('.quit').addEventListener('click', e => {
- // 3.2 清空本地缓存,跳转到登录页面
- localStorage.clear()
- location.href = '../login/index.html'
- })
项目素材代码点击这里:【免费】黑马头条数据管理平台的素材、代码资源-CSDN文库
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。