hm头条-admin

本节目标

完成项目

  • 项目介绍
  • 验证码登录
  • 统一处理请求
  • 富文本编辑器
  • 频道下拉菜单
  • 封面上传
  • 文章列表展示
  • 筛选功能
  • 分页功能
  • 删除功能
  • 编辑文章(回显)
  • 编辑文章(保存)
  • 退出登录

项目介绍

介绍

头条数据管理平台: 对IT资源移动网站的数据进行管理

移动网站(演示): 极客园

主要功能

  1. 登录和权限判断
  2. 查看文章内容列表(筛选, 分页)
  3. 编辑文章(数据回显)
  4. 删除文章
  5. 发布文章(图片上传, 富文本编辑器)

技术选型

  1. 基于Bootstrap搭建网站标签和样式
  2. 集成wangEditor插件, 实现富文本编辑器
  3. 使用原生JS完成增删改查业务
  4. 基于axios进行前后端交互
  5. 使用axios拦截器进行权限判断

项目准备

html, css, js, 图片, 第三方插件

目录结构

assets: 资源文件夹(图片/字体)

lib: 资料文件夹(第三方插件)

page: 页面文件夹

utils: 工具文件夹(自定义js等)

验证码登录

登录流程

实现登录

// axios 公共配置
// 基地址
axios.defaults.baseURL = 'https://geek.itheima.net'

/**
 * 目标1:验证码登录
 * 1.1 在 utils/request.js 配置 axios 请求基地址
 * 1.2 收集手机号和验证码数据
 * 1.3 基于 axios 调用验证码登录接口
 * 1.4 使用 Bootstrap 的 Alert 警告框反馈结果给用户
 */
document.querySelector('.btn').addEventListener('click', async function () {
  try {
    const form = document.querySelector('.login-form')
    const data = serialize(form, { hash: true, empyt: true })
    const login = await axios({ url: '/v1_0/authorizations', method: 'post', data })
    myAlert(true, '登录成功')

  } catch (error) {
    console.dir(error);
    myAlert(false, error.response.data.message)
  }

})

权限控制

token是访问权限的令牌, 本质是一个字符串, 前端只能判断有无token, 后端判断token的有效性

/**
 * 目标1:访问权限控制
 * 1.1 判断无 token 令牌字符串,则强制跳转到登录页
 * 1.2 登录成功后,保存 token 令牌字符串到本地,并跳转到内容列表页面
 */
const token = localStorage.getItem('token')
// 没有token,返回登录页
if (!token) {
  location.href = '../login/index.html'
}

document.querySelector('.btn').addEventListener('click', async function () {
  try {
    ... ...
    myAlert(true, '登录成功')
    localStorage.setItem('token', login.data.data.token)
    setTimeout(() => {
      location.href = '../content/index.html'
    }, 1200)
  } 
})

统一处理请求

请求拦截器

请求发起之前,触发请求拦截器函数, 可以对请求参数进行额外配置

拦截流程

设置请求头参数

1,在拦截器中统一添加请求头参数

// 请求拦截器
// 语法:   axios.interceptors.request.use(函数1, 函数2)
// 函数1:  请求成功的函数
// 函数2:  请求失败的函数(可省略,很少用)
axios.interceptors.request.use(function (config) {
  // 请求之前的处理
  const token = localStorage.getItem('token')
  token && (config.headers.Authorization = `Bearer ${token}`)

  return config
}, function (error) {
  // 请求错误的处理
  return Promise.reject(error)
})

2, 请求时单独添加请求头参数

axios({
  url: '',
  headers: {
    Authorization: 'Bearer xxxxxxxx'
  }
})

响应拦截器

服务器响应结果后, 首先触发响应拦截器函数, 可以对响应结果进行统一处理, 再回到then/catch中

拦截流程

处理响应结果

  1. 简化axios的响应数据结构 (注意会影响之前的数据读取)

  2. 理登录过期的状态

    // 响应拦截器
    // axios.interceptors.request.use(函数1, 函数2)
    // 函数1: 请求成功的函数
    // 函数2: 请求失败的函数
    axios.interceptors.response.use(function (response) {
    // 状态码2xx范围内的请求触发成功处理函数
    // 简化axios的响应数据结构
    return response.data

    }, function (error) {
    // 超出2xx范围内的请求触发失败处理函数
    console.dir(error)
    // 处理登录过期的状态
    if (error?.response?.status === 401) {
    alert('登录过期, 请重新登录')
    localStorage.clear()
    location.href = '../login/index.html'
    }

    return Promise.reject(error)
    })

富文本编辑器

富文本: 带样式, 多格式的文本, 在前端中一般使用标签配合内联样式实现

富文本编辑器: 用于编辑富文本内容的容器

使用wangEditor插件, 完成富文本编辑器的集成

官网: wangEditor

使用步骤

  1. 引入css样式

  2. 定义html结构

  3. 引入js创建编辑器

  4. 监听内容变化, 进行处理

  5. 富文本编辑器重置内容: editor.setHtml('')

    <link href="https://unpkg.com/@wangeditor/editor@latest/dist/css/style.css" rel="stylesheet"> <style> /* 富文本编辑器 */ #editor---wrapper { border: 1px solid #ccc; z-index: 100; /* 按需定义 */ } #toolbar-container { border-bottom: 1px solid #ccc; } #editor-container { height: 500px; } </style> <body>
    <script src="https://unpkg.com/@wangeditor/editor@latest/dist/index.js"></script> </body>

    // 富文本编辑器
    // 创建编辑器函数,创建工具栏函数
    const { createEditor, createToolbar } = window.wangEditor

    // 编辑器配置文件
    const editorConfig = {
    // 占位提示文字
    placeholder: '请输入文章内容',
    // 内容改变事件
    onChange(editor) {
    const html = editor.getHtml()
    console.log('editor content', html)
    // 也可以同步到 <textarea>
    // 目的: 方便使用插件统一收集数据
    document.querySelector('.publish-content').innerHTML = html
    }
    }
    // 创建编辑器
    const editor = createEditor({
    // 指定创建位置
    selector: '#editor-container',
    // 默认内容
    html: '


    ',
    // 添加配置对象
    config: editorConfig,
    // 设置模式 default(完整) simple(简洁)
    mode: 'default', // or 'simple'
    })

    // 工具栏配置文件
    const toolbarConfig = {}
    // 创建工具栏
    const toolbar = createToolbar({
    // 为指定的编辑器创建工具栏
    editor,
    // 指定创建位置
    selector: '#toolbar-container',
    // 添加配置对象
    config: toolbarConfig,
    // 设置模式 default(完整) simple(简洁)
    mode: 'default', // or 'simple'
    })

频道下拉菜单

/**
 * 目标1:设置频道下拉菜单
 *  1.1 获取频道列表数据
 *  1.2 展示到下拉菜单中
 */
async function initChannel() {
  const { data } = await axios.get('/v1_0/channels')
  const channelStr = '<option value="" selected="">请选择文章频道</option>' + data.channels.map(item => {
    return `<option value="${item.id}" selected="">${item.name}</option>`
  }).join('')
  document.querySelector('.form-select').innerHTML = channelStr
}
initChannel()

封面上传

/**
 * 目标2:文章封面设置
 *  2.1 准备标签结构和样式
 *  2.2 选择文件并保存在 FormData
 *  2.3 单独上传图片并得到图片 URL 网址
 *  2.4 回显并切换 img 标签展示(隐藏 + 号上传标签)
 */
document.querySelector('.img-file').addEventListener('input', async function (e) {
  const file = e.target.files[0]
  const fd = new FormData()
  fd.append('image', file)
  const res = await axios.post('/v1_0/upload', fd)

  document.querySelector('.rounded').src = res.data.url
  document.querySelector('.rounded').classList.add('show')
  document.querySelector('.place').classList.add('hide')
})
// 点击封面重新上传图片
document.querySelector('.rounded ').addEventListener('click', function () {
  document.querySelector('.img-file').click()
})

发布文章

/**
 * 目标3:发布文章保存
 *  3.1 基于 form-serialize 插件收集表单数据对象
 *  3.2 基于 axios 提交到服务器保存
 *  3.3 调用 Alert 警告框反馈结果给用户
 *  3.4 重置表单并跳转到列表页
 */
document.querySelector('.send').addEventListener('click', async function () {
  try {
    const form = document.querySelector('.art-form')
    const formData = serialize(form, { hash: true, empty: true })
    delete formData.id
    // 手动补充封面数据
    formData.cover = {
      type: 1,
      images: [document.querySelector('.rounded').src]
    }
    // 发布文章
    const res = await axios({
      url: '/v1_0/mp/articles',
      method: 'post',
      data: formData
    })

    // 提示用户
    myAlert(true, '发布成功')
    // 清空表单 
    form.reset()
    // 富文本编辑器重置内容
    editor.setHtml('')
    document.querySelector('.rounded').src = ''
    document.querySelector('.rounded').classList.remove('show')
    document.querySelector('.place').classList.remove('hide')
    // 跳转页面
    setTimeout(() => {
      location.href = '../content/index.html'
    }, 1200)
  } catch (err) {
    console.dir(err)
    myAlert(false, err.response.data.message)
  }

})

文章列表展示

/**
 * 目标1:获取文章列表并展示
 *  1.1 准备查询参数对象
 *  1.2 获取文章列表数据
 *  1.3 展示到指定的标签结构中
 */
const params = {
  status: '', // 1-待审核, 2-审核通过, 不传为全部
  channel_id: '', // 频道id,不传为全部
  page: 1,
  per_page: 2
}
const onLoad = async () => {
  const res = await axios({
    url: '/v1_0/mp/articles',
    params
  })

  const listStr = res.data.results.map(item => {
    return `<tr>
    <td>
      <img src="${item.cover.type === 0 ? 'https://img2.baidu.com/it/u=2640406343,1419332367&amp;fm=253&amp;fmt=auto&amp;app=138&amp;f=JPEG?w=708&amp;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('')

  document.querySelector('.art-list').innerHTML = listStr
}

onLoad()

筛选功能

/**
 * 目标2:筛选文章列表
 *  2.1 设置频道列表数据
 *  2.2 监听筛选条件改变,保存查询信息到查询参数对象
 *  2.3 点击筛选时,传递查询参数对象到服务器
 *  2.4 获取匹配数据,覆盖到页面展示
 */
// 设置频道列表数据
async function initChannel() {
  const { data } = await axios.get('/v1_0/channels')
  const channelStr = '<option value="" selected="">请选择文章频道</option>' + data.channels.map(item => {
    return `<option value="${item.id}" selected="">${item.name}</option>`
  }).join('')
  document.querySelector('.form-select').innerHTML = channelStr
}
initChannel()

// 监听筛选条件改变
document.querySelectorAll('.form-check-input').forEach(item => {
  item.addEventListener('click', e => {
    params.status = e.target.value
  })
});

// 监听筛选条件改变
document.querySelector('.form-select').addEventListener('change', e => {
  params.channel_id = e.target.value
})

// 点击筛选时
document.querySelector('.sel-btn').addEventListener('click', () => {
  onLoad()
})

分页功能

/**
 * 目标3:分页功能
 *  3.1 保存并设置文章总条数
 *  3.2 点击下一页,做临界值判断,并切换页码参数并请求最新数据
 *  3.3 点击上一页,做临界值判断,并切换页码参数并请求最新数据
 */
// 点击下一页
document.querySelector('.next').addEventListener('click', function () {
  if (params.page < Math.ceil(tatalCount / params.per_page)) {
    params.page++
    onLoad()
    document.querySelector('.page-now').innerHTML = `第 ${params.page} 页`
  }
})
// 点击上一页
document.querySelector('.last').addEventListener('click', function () {
  if (params.page > 1) {
    params.page--
    onLoad()
    document.querySelector('.page-now').innerHTML = `第 ${params.page} 页`
  }
})

删除文章

/**
 * 目标4:删除功能
 *  4.1 关联文章 id 到删除图标
 *  4.2 点击删除时,获取文章 id
 *  4.3 调用删除接口,传递文章 id 到服务器
 *  4.4 重新获取文章列表,并覆盖展示
 *  4.5 删除最后一页的最后一条,需要自动向前翻页
 */
document.querySelector('.art-list').addEventListener('click', async function (e) {
  // 确定点击删除按钮
  if (e.target.classList.contains('del')) {
    const id = e.target.parentNode.dataset.id
    // 删除操作
    await axios({
      url: `/v1_0/mp/articles/${id}`,
      method: 'delete',
    })

    // 删除最后一页最后一条,自动翻页
    if (tatalCount % params.per_page === 1 && params.page > 1) {
      params.page--
    }

    // 刷新页面
    onLoad()
  }
})

编辑文章(回显)

/**
 * 目标5:编辑功能
 * 步骤:  点击编辑时,获取文章 id,跳转到发布文章页面, 传递文章 id 过去
 */
document.querySelector('.art-list').addEventListener('click', async function (e) {
  // 确定点击修改按钮
  if (e.target.classList.contains('edit')) {
    const id = e.target.parentNode.dataset.id
    // 页面跳转并传参
    location.href = `../publish/index.html?id=${id}`
  }
})

/**
   * 目标4:编辑-回显文章
   *  4.1 页面跳转传参(URL 查询参数方式)
   *  4.2 发布文章页面接收参数判断(共用同一套表单)
   *  4.3 修改标题和按钮文字
   *  4.4 获取文章详情数据并回显表单
   */
  ; (function () {
    const paramsObj = new URLSearchParams(location.search)
    paramsObj.forEach(async (value, name) => {
      // value: 查询参数值  name: 查询参数名
      // id存在,编辑文章
      if (name === 'id') {
        // 修改文字
        document.querySelector('.title').innerHTML = `<span>编辑文章</span>`
        document.querySelector('.send').innerHTML = '编辑'
        // 查询详情
        const res = await axios({
          url: `/v1_0/mp/articles/${value}`,
        })
        console.log(res);
        // 转存数据(精简后台冗余数据)
        const resObj = {
          id: res.data.id,  // 文章id
          channel_id: res.data.channel_id, // 频道
          content: res.data.content, // 文章内容
          cover: res.data.cover.images[0],  // 封面图片
          title: res.data.title, //标题
        }
        // 回显数据
        Object.keys(resObj).forEach(key => {
          if (key === 'cover') {
            // 单独处理图片回显
            document.querySelector('.rounded').src = resObj[key]
            document.querySelector('.rounded').classList.add('show')
            document.querySelector('.place').classList.add('hide')
          } else if (key === 'content') {
            // 单独处理文章内容
            editor.setHtml(resObj[key])
          } else {
            // 无需单独处理
            // 用数据对象属性名, 作为标签name属性选择器的值, 匹配标签
            document.querySelector(`[name=${key}]`).value = resObj[key]
          }
        })
      }
    })

  })()

编辑文章(保存)

/**
 * 目标5:编辑-保存文章
 *  5.1 判断按钮文字,区分业务(因为共用一套表单)(添加的事件中也要加判断)
 *  5.2 调用编辑文章接口,保存信息到服务器
 *  5.3 基于 Alert 反馈结果消息给用户
 */
document.querySelector('.send').addEventListener('click', async function () {
  if (document.querySelector('.send').innerHTML !== '编辑') return

  try {
    const form = document.querySelector('.art-form')
    const formData = serialize(form, { hash: true, empty: true })

    await axios({
      url: `/v1_0/mp/articles/${formData.id}`,
      method: 'put',
      data: {
        ...formData,
        cover: {
          type: document.querySelector('.rounded').src ? 1 : 0,
          images: [document.querySelector('.rounded').src]
        }
      }
    })
    myAlert(true, '编辑成功')
  } catch (error) {
    console.dir(error)
    myAlert(false, error.response.data.message)
  }

})

退出登录

/**
 * 目标3:退出登录
 *  3.1 绑定点击事件
 *  3.2 清空本地缓存,跳转到登录页面
 */
document.querySelector('.quit').addEventListener('click', () => {
  localStorage.clear()
  location.href = '../login/index.html'
})
相关推荐
万叶学编程2 小时前
Day02-JavaScript-Vue
前端·javascript·vue.js
萧鼎4 小时前
Python常见问题解答:从基础到进阶
开发语言·python·ajax
天涯学馆4 小时前
Next.js与NextAuth:身份验证实践
前端·javascript·next.js
HEX9CF5 小时前
【CTF Web】Pikachu xss之href输出 Writeup(GET请求+反射型XSS+javascript:伪协议绕过)
开发语言·前端·javascript·安全·网络安全·ecmascript·xss
ConardLi5 小时前
Chrome:新的滚动捕捉事件助你实现更丝滑的动画效果!
前端·javascript·浏览器
ConardLi5 小时前
安全赋值运算符,新的 JavaScript 提案让你告别 trycatch !
前端·javascript
积水成江5 小时前
关于Generator,async 和 await的介绍
前端·javascript·vue.js
Z3r4y5 小时前
【Web】portswigger 服务端原型污染 labs 全解
javascript·web安全·nodejs·原型链污染·wp·portswigger
人生の三重奏5 小时前
前端——js补充
开发语言·前端·javascript