Skip to content

05 黑马头条 数据管理平台

项目介绍

介绍我们要做的项目,为何做,以及怎么做

小结

  1. 黑马头条 - 数据管理平台,是什么样网站,要完成哪些功能?

    • 数据管理网站,登录后对数据进行增删改查
  2. 数据管理平台,未登录能否管理数据?

    • 不能,数据是公司内部的,需账号登录后管理

项目准备

了解项目需要准备哪些内容

  • 技术:

    • 基于 Bootstrap 搭建网站标签和样式
    • 集成 wangEditor 插件实现富文本编辑器
    • 使用原生 JS 完成增删改查等业务
    • 基于 axios 与黑马头条线上接口交互
    • 使用 axios 拦截器进行权限判断
  • 项目准备:准备配套的素材代码

    • 包含:html,css,js,静态图片,第三方插件等等
  • 目录管理:建议这样管理,方便查找

    • assets:资源文件夹(图片,字体等)
    • lib:资料文件夹(第三方插件,例如:form-serialize
    • page:页面文件夹
    • utils:实用程序文件夹(工具插件)
项目目录
bash
.
├── assets
├── lib
   └── form-serialize.js
├── page
   ├── content
   ├── index.css
   ├── index.html
   └── index.js
   ├── login
   └── publish
└── utils

登录退出

  • 完成验证码登录,后端设置验证码默认为 246810
  • 完成退出登录效果

验证码登录

  1. utils/request.js 配置 axios 请求基地址

    作用:提取公共前缀地址,配置后 axios 请求时都会 baseURL + url

    js
    axios.defaults.baseURL = 'http://geek.itheima.net';
  2. 收集手机号和验证码数据

  3. 基于 axios 调用验证码登录接口

  4. 使用 Bootstrap 的 Alert 警告框反馈结果给用户

99508cf1-63a1-4c23-98f8-0c56d39950f2

验证码登录流程

了解验证码登录的流程

  • 手机号 + 验证码,登录流程:

    440ebf48-0b35-42a7-8607-2f7fa612ea91

token 的介绍

了解前后端分离项目中 token 的作用

  • 概念:访问权限的令牌,本质上是一串字符串

  • 创建:正确登录后,由后端签发并返回

  • 作用:判断是否有登录状态等,控制访问权限

  • 注意:前端只能判断 token 有无,而后端才能判断 token 的有效性

    8b042724-fe82-4fb3-8881-525edd59bba9

  • 目标:只有登录状态,才可以访问内容页面

  • 步骤:

    1. utils/auth.js 中判断无 token 令牌字符串,则强制跳转到登录页(手动修改地址栏测试)
    2. 在登录成功后,保存 token 令牌字符串到本地,再跳转到首页(手动修改地址栏测试)
    js
    const token = localStorage.getItem('token');
    // 没有 token 令牌字符串,则强制跳转登录页
    if (!token) {
      location.href = '../login/index.html';
    }

小结

  1. token 的作用?

    • 判断用户是否有登录状态等
  2. token 的注意:

    • 前端只能判断 token 的有无
    • 后端通过解密可以提取 token 字符串的原始信息,判断有效性

个人信息设置和 axios 请求拦截器

了解 axios 请求拦截器的概念和使用场景

  • 需求:设置用户昵称
  • 语法:axios 可以在 headers 选项传递请求头参数
  • 问题:很多接口,都需要携带 token 令牌字符串
  • 解决:在请求拦截器统一设置公共 headers 选项

cdf62885-a109-4d2c-be47-9f513174b7b2

7c50caf5-85ee-4a66-8dde-4fda09a8540a

js
axios({
  url: '目标资源地址',
  headers: {
    Authorization: `Bearer ${localStorage.getItem('token')}`,
  },
});
js
axios.interceptors.request.use(
  function (config) {
    // 在发送请求之前做些什么
    return config;
  },
  function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  },
);
js
axios({
  // 个人信息
  url: '/v1_0/user/profile',
})
  .then((result) => {
    // result:服务器响应数据对象
  })
  .catch((error) => {});
js
axios.interceptors.request.use(function (config) {
  const token = location.getItem('token')
  token && config.headers.Authorization = `Bearer ${token}`
  // 在发送请求之前做些什么
  return config
}, function (error) {
  // 对请求错误做些什么
  return Promise.reject(error)
})

小结

  1. 什么是 axios 请求拦截器?

    • 发起请求之前,调用的一个函数,对请求参数进行设置
  2. axios 请求拦截器,什么时候使用?

    • 有公共配置和设置时,统一设置在请求拦截器中

axios 响应拦截器和身份验证失败

了解 axios 响应拦截器的概念和使用场景,以及身份验证失败的场景流程和判断使用

  • axios 响应拦截器:响应回到 then/catch 之前,触发的拦截函数,对响应结果统一处理
  • 例如:身份验证失败,统一判断并做处理

f8d8d0e7-2e31-430f-9a41-a8f6b8736bbd

js
axios.interceptors.response.use(
  function (response) {
    // 2xx 范围内的状态码都会触发该函数。
    return result;
  },
  function (error) {
    // 超出 2xx 范围的状态码都会触发该函数。
    // 对响应错误做点什么,例如:判断响应状态为 401 代表身份验证失败
    if (error?.response?.status === 401) {
      alert('登录状态过期,请重新登录');
      window.location.href = '../login/index.html';
    }
    return Promise.reject(error);
  },
);

小结

  1. 什么是 axios 响应拦截器?

    • 响应回到 then/catch 之前,触发的拦截函数,对响应结果统一处理
  2. axios 响应拦截器,什么时候触发成功/失败的回调函数?

    • 状态为 2xx 触发成功回调,其他则触发失败的回调函数

优化 axios 响应结果

axios 直接接收服务器返回的响应结果

  • 思路:其实就是在响应拦截器里,response.data 把后台返回的数据直接取出来统一返回给所有使用这个 axios 函数的逻辑页面位置的 then 的形参上
  • 好处:可以让逻辑页面少点一层 data 就能拿到后端返回的真正数据对象

21baff75-b194-4b09-9790-ed70acec912d

js
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);
  },
);

退出登录

完成退出登录效果

步骤:

  1. 绑定点击事件
  2. 清空本地缓存,跳转到登录页面

4b6df448-8bd9-4818-af86-b1c85809c0f4

相关代码

  • utils/auth.js:权限插件(引入到了除登录页面,以外的其他所有页面)

    • 访问权限控制

      • 判断无 token 令牌字符串,则强制跳转到登录页
      • 登录成功后,保存 token 令牌字符串到本地,并跳转到内容列表页面
    • 设置个人信息

      • utils/request.js 设置请求拦截器,统一携带 token
      • 请求个人信息并设置到页面
    • 退出登录

      • 绑定点击事件
      • 清空本地缓存,跳转到登录页面
  • utils/request.js:axios 请求封装

    • 配置 baseURL 基地址
    • 配置请求拦截器,从本地存储中获取 token 令牌字符串,统一携带 token 令牌字符串在请求头上
    • 配置响应拦截器,优化 axios 响应结果,统一处理 401 状态码
  • page/login/index.html:验证码登录

    • 使用 serialize 收集手机号和验证码数据,前提每个表单元素都要有 name 属性
    • 基于 axios 调用验证码登录接口
    • 使用 Bootstrap 的 Alert 警告框反馈结果给用户 (成功/失败)
    • 保存 token 令牌字符串到本地
    • 延时 1.5s 跳转到内容列表页面,让用户看到登录成功的提示
js
// 权限插件(引入到了除登录页面,以外的其他所有页面)
/**
 * 目标 1:访问权限控制
 * 1.1 判断无 token 令牌字符串,则强制跳转到登录页
 * 1.2 登录成功后,保存 token 令牌字符串到本地,并跳转到内容列表页面
 */
const token = localStorage.getItem('token');
if (!token && location.pathname !== '/content/index.html') {
  // 无 token 令牌字符串,则强制跳转到登录页
  location.href = '../login/index.html';
}

/**
 * 目标 2:设置个人信息
 * 2.1 在 utils/request.js 设置请求拦截器,统一携带 token
 * 2.2 请求个人信息并设置到页面
 */
axios.get('/v1_0/user/profile').then((res) => {
  console.log(res);
  // $('.nick-name').html(res.data.name); // jQuery 语法
  document.querySelector('.nick-name').innerHTML = res.data.name;
});

/**
 * 目标 3:退出登录
 *  3.1 绑定点击事件
 *  3.2 清空本地缓存,跳转到登录页面
 */
quit = document.querySelector('.quit');
quit.addEventListener('click', () => {
  localStorage.removeItem('token');
  location.href = '../login/index.html';
});
js
// axios 公共配置
// 基地址

// 文档:https://www.axios-http.cn/docs/config_defaults#全局-axios-默认值
axios.defaults.baseURL = 'https://geek.itheima.net';

// 文档:https://www.axios-http.cn/docs/interceptors
// 添加请求拦截器
axios.interceptors.request.use(
  (config) => {
    // 在发送请求之前做些什么

    // 从本地存储中获取 token 令牌字符串
    const token = localStorage.getItem('token');
    // 统一携带 token 令牌字符串在请求头上
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => {
    // 对请求错误做些什么
    // 创建了一个新的 Promise 对象,并将其状态设置为 rejected(失败)
    // 在后续使用时,可以通过 .catch() 方法拿到错误信息
    return Promise.reject(error);
  },
);

// 添加响应拦截器
axios.interceptors.response.use(
  (response) => {
    // 2xx 范围内的状态码都会触发该函数。
    // 对响应数据做点什么,例如:直接返回服务器的响应结果对象
    // 优化 axios 响应结果:可以让逻辑页面少点一层 data 就能拿到后端返回的真正数据对象
    return response.data;
  },
  (error) => {
    // 超出 2xx 范围的状态码都会触发该函数。
    // 对响应错误做点什么,例如:统一对 401 身份验证失败情况做出处理
    // console.dir(error);
    if (error?.response?.status === 401) {
      // error?.response?.status 为 es6 可选链语法:防止属性不存在报错
      console.log(error?.response?.data);

      // 401 身份验证失败
      alert('身份验证失败,请重新登录!');
      // 清除无效 token
      localStorage.removeItem('token');
      // 跳转到登录页面
      location.href = '../login/index.html';
    }
    return Promise.reject(error);
  },
);
js
/**
 * 目标:验证码登录
 * - 收集手机号和验证码数据
 * - 基于 axios 调用验证码登录接口
 * - 使用 Bootstrap 的 Alert 警告框反馈结果给用户
 */
async function codeLogin() {
  try {
    // 使用 serialize 收集表单数据,前提每个表单元素都要有 name 属性
    const form = document.querySelector('.login-form');
    const formData = serialize(form, { hash: true, empty: true });

    // 基于 axios 调用验证码登录接口
    const res = await axios.post('/v1_0/authorizations', formData);
    console.log(res);

    // 使用 Bootstrap 的 Alert 警告框反馈结果给用户
    myAlert(true, '登录成功!');

    // 保存 token 令牌字符串到本地
    localStorage.setItem('token', res.data.token);

    // 延时 1.5s 跳转到内容列表页面,让用户看到登录成功的提示
    setTimeout(() => {
      location.href = '../content/index.html';
    }, 1500);
  } catch (err) {
    console.dir(err);

    // 使用 Bootstrap 的 Alert 警告框反馈结果给用户
    // myAlert(false, '登录失败!');
    myAlert(false, err.response.data.message);
    console.log(err.response.data.message);
  }
}

// 监听表单提交事件
document.querySelector('.btn').addEventListener('click', codeLogin);

发布文章

富文本编辑器

了解富文本编辑器的概念,以及如何在前端网页中使用

  • 富文本:带样式,多格式的文本,在前端一般使用标签配合内联样式实现
  • 富文本编辑器:用于编写富文本内容的容器

064a72d4-f2b3-4403-9e65-29d40e5e5984

  • 目标:发布文章页,富文本编辑器的集成

  • 使用:wangEditor 插件

  • 步骤:参考文档

    1. 引入 CSS 定义样式

      html
      <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>
    2. 定义 HTML 结构

      html
      <div id="editor—wrapper">
        <div id="toolbar-container"><!-- 工具栏 --></div>
        <div id="editor-container"><!-- 编辑器 --></div>
      </div>
    3. 引入 JS 创建编辑器

      html
      <script src="https://unpkg.com/@wangeditor/editor@latest/dist/index.js"></script>
      <script>
        const { createEditor, createToolbar } = window.wangEditor;
      
        const editorConfig = {
          placeholder: 'Type here...',
          onChange(editor) {
            const html = editor.getHtml();
            console.log('editor content', html);
            // 也可以同步到 <textarea>
          },
        };
      
        const editor = createEditor({
          selector: '#editor-container',
          html: '<p><br></p>',
          config: editorConfig,
          mode: 'default', // or 'simple'
        });
      
        const toolbarConfig = {};
      
        const toolbar = createToolbar({
          editor,
          selector: '#toolbar-container',
          config: toolbarConfig,
          mode: 'default', // or 'simple'
        });
      </script>
    4. 监听内容改变,保存在隐藏文本域(便于后期收集)

      js
      const editorConfig = {
        placeholder: 'Type here...', // 编辑器的占位符文本
        onChange(editor) {
          // 监听编辑器内容变化,当编辑器的内容发生变化时,这个函数会被调用
          // 使用 editor.getHtml() 获取了编辑器的 HTML 内容,并将其打印到控制台。
          const html = editor.getHtml();
          console.log('editor content', html);
          // 也可以同步到 <textarea>: 为了后续快速收集整个表单内容做铺垫
          document.querySelector('textarea.publish-content').value = html;
        },
      };

频道列表

展示频道列表,供用户选择

步骤:

  1. 获取频道列表数据
  2. 展示到下拉菜单中
js
const channelId = document.querySelector('#channel_id');
axios.get('/v1_0/channels').then((res) => {
  console.log(res);
  channelId.innerHTML = res.data.channels
    .map((item) => {
      return `<option value="${item.id}">${item.name}</option>`;
    })
    .join('');
});

de8391cb-64ce-4d32-a4a9-9c183479f7fa

封面设置

文章封面的设置

步骤:

  1. 准备标签结构和样式
  2. 选择文件并保存在 FormData
  3. 单独上传图片并得到图片 URL 地址
  4. 回显并切换 img 标签展示(隐藏 + 号上传标签)

注意:图片地址临时存储在 img 标签上,并未和文章关联保存

b3570ab3-9f53-4bd5-a096-d92ce38c7594

收集并保存

收集文章内容,并提交保存

步骤:

  1. 基于 form-serialize 插件收集表单数据对象
  2. 基于 axios 提交到服务器保存
  3. 调用 Alert 警告框反馈结果给用户
  4. 重置表单并跳转到列表页

b31597a5-8d9a-49c2-bf29-b00a46c781e8

相关代码

js
// 富文本编辑器
// 创建编辑器函数,创建工具栏函数

// 从 window.wangEditor 对象中解构出 createEditor 和 createToolbar 方法
// createEditor 创建编辑器
// createToolbar 创建工具栏
const { createEditor, createToolbar } = window.wangEditor;

const editorConfig = {
  placeholder: 'Type here...', // 编辑器的占位符文本
  onChange(editor) {
    // 监听编辑器内容变化,当编辑器的内容发生变化时,这个函数会被调用
    // 使用 editor.getHtml() 获取了编辑器的 HTML 内容,并将其打印到控制台。
    const html = editor.getHtml();
    console.log('editor content', html);
    // 也可以同步到 <textarea>: 为了后续快速收集整个表单内容做铺垫
    document.querySelector('textarea.publish-content').value = html;
  },
};

const editor = createEditor({
  selector: '#editor-container', // 编辑器容器的选择器
  html: '<p><br></p>', // 编辑器的初始 HTML 内容
  config: editorConfig, // 编辑器的配置对象
  mode: 'default', // 编辑器的模式 'simple' or 'default'
});

const toolbarConfig = {}; // 配置工具栏

// 创建了工具栏
const toolbar = createToolbar({
  editor, // 关联的编辑器
  selector: '#toolbar-container', // 工具栏容器的选择器
  config: toolbarConfig, // 工具栏的配置对象
  mode: 'default', // 工具栏的模式 'simple' or 'default'
});
js

/**
 * 目标 1:设置频道下拉菜单
 *  1.1 获取频道列表数据
 *  1.2 展示到下拉菜单中
 */
const channelId = document.querySelector('#channel_id');

async function renderChannels() {
  try {
    const res = await axios.get('/v1_0/channels');
    console.log(res);
    channelId.innerHTML = res.data.channels
      .map((item) => {
        return `<option value="${item.id}">${item.name}</option>`;
      })
      .join('');
  } catch (error) {
    console.error(error);
  }
}

renderChannels();

/**
 * 目标 2:文章封面设置
 *  2.1 准备标签结构和样式
 *  2.2 选择文件并保存在 FormData
 *  2.3 单独上传图片并得到图片 URL 网址
 *  2.4 回显并切换 img 标签展示(隐藏 + 号上传标签)
 */
const imgFile = document.querySelector('.img-file');
const rounded = document.querySelector('.rounded');
const place = document.querySelector('.place');

function setCover() {
  imgFile.addEventListener('change', async function () {
    const formData = new FormData();
    formData.append('image', this.files[0]);

    try {
      const res = await axios.post('/v1_0/upload', formData);
      console.log(res);

      // 回显图片
      rounded.src = res.data.url;
      rounded.classList.add('show');

      // 隐藏上传按钮
      place.classList.add('hide');
    } catch (error) {
      console.error(error);
    }
  });

  // 点击图片,可以重新上传用于切换文章封面
  rounded.addEventListener('click', () => {
    imgFile.click();
  });
}

setCover();

内容管理

文章列表展示

获取文章列表并展示

步骤:

  1. 准备查询参数对象
  2. 获取文章列表数据
  3. 展示到指定的标签结构中

d213ee40-13da-4af4-8a7a-de690e8c425a

js
const queryObj = {
  status: '', // 筛选状态
  channel_id: '', // 频道 id
  page: 1, // 当前页码
  per_page: 2, // 每页条数
};
let totalCount = 0; // 总条数

筛选功能

根据筛选条件,获取匹配数据展示

步骤:

  1. 设置频道列表数据
  2. 监听筛选条件改变,保存查询信息到查询参数对象
  3. 点击筛选时,传递查询参数对象到服务器
  4. 获取匹配数据,覆盖到页面展示

bcbd10ff-a28e-4d67-bee5-7cb8a965096a

分页功能

完成文章列表,分页管理功能

步骤:

  1. 保存并设置文章总条数
  2. 点击下一页,做临界值判断,并切换页码参数请求最新数据
  3. 点击上一页,做临界值判断,并切换页码参数请求最新数据

26b287a0-f933-4aa6-ae93-7e18a3004162

删除功能

完成删除文章功能

步骤:

  1. 关联文章 id 到删除图标
  2. 点击删除时,获取文章 id
  3. 调用删除接口,传递文章 id 到服务器
  4. 重新获取文章列表,并覆盖展示

98b2211b-4f25-4c1c-8ea9-cc84c37cbebc

删除最后一条

在删除最后一页,最后一条时有 Bug

步骤:

  1. 删除成功时,判断 DOM 元素只剩一条,让当前页码 page--
  2. 注意,当前页码为 1 时不能继续向前翻页
  3. 重新设置页码数,获取最新列表展示

4e0e0458-9214-4a3d-940f-065f6bc7ba97

编辑文章 回显

编辑文章时,回显数据到表单

步骤:

  1. 页面跳转传参(URL 查询参数方式)
  2. 发布文章页面接收参数判断(共用同一套表单)
  3. 修改标题和按钮文字
  4. 获取文章详情数据并回显表单

09e305f1-d05e-4ea7-aacd-19bec507693a

js
const params = `?id=1001&name=xiaoli`;
// 查询参数字符串 => 查询参数对象
const result = new URLSearchParams(params);
// 需要遍历使用
result.forEach((value, key) => {
  console.log(value, key);
  // 1001 id
  // xiaoli name
});

编辑文章 保存

确认修改,保存文章到服务器

步骤:

  1. 判断按钮文字,区分业务(因为共用一套表单)
  2. 调用编辑文章接口,保存信息到服务器
  3. 基于 Alert 反馈结果消息给用户

e46904b2-29f8-4e55-a767-3ce0097cb2d1

相关代码

js
/**
 * 目标 1:获取文章列表并展示
 *  1.1 准备查询参数对象
 *  1.2 获取文章列表数据
 *  1.3 展示到指定的标签结构中
 */

const artList = document.querySelector('.align-middle.art-list');
let nowPage = '1';
let totalPage;

// 准备查询参数对象
const params = {
  // 文章状态:1-待审核,2-审核通过,不传为全部
  status: '',
  // 频道 id,不传为全部,可选
  channel_id: '',
  // 当前页码,可选
  page: nowPage,
  // 每页条数,可选
  per_page: 2,
};
async function renderArticles() {
  try {
    const paramsObj = new URLSearchParams(params);
    const queryParam = paramsObj.toString();
    // console.log(queryParam);
    const res = await axios.get(`/v1_0/mp/articles?${queryParam}`);
    console.log(res);

    const defaultCover = 'https://img2.baidu.com/it/u=2640406343,1419332367&fm=253&fmt=auto&app=138&f=JPEG?w=708&h=500';
    artList.innerHTML = res.data.results
      .map((item) => {
        return `
        <tr>
          <td><img src="${item.cover.type === 0 ? defaultCover : item.cover.images[0]}" alt="" referrerpolicy="no-referrer"></td>
          <td>${item.title}</td>
          <td>${
            item.status === 2
              ? `<span class="badge text-bg-success">审核通过</span>`
              : `<span class="badge text-bg-primary">待审核</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 data-id="${item.id}">
            <i class="bi bi-pencil-square edit"></i>
            <i class="bi bi-trash3 del"></i>
          </td>
        </tr>`;
      })
      .join('');

    // 设置页面总条数
    totalPage = Math.ceil(res.data.total_count / params.per_page); // 向上取整
    document.querySelector('.total-count.page-now').innerHTML = `共 ${totalPage} 页`;
    // 设置当前页码
    document.querySelector('.page-item.page-now').innerHTML = `第 ${nowPage} 页`;
  } catch (error) {
    console.dir(error);
  }
}

renderArticles();

/**
 * 目标 2:筛选文章列表
 *  2.1 设置频道列表数据
 *  2.2 监听筛选条件改变,保存查询信息到查询参数对象
 *  2.3 点击筛选时,传递查询参数对象到服务器
 *  2.4 获取匹配数据,覆盖到页面展示
 */
const channelId = document.querySelector('select.form-select');

// 渲染频道列表数据
async function renderChannels() {
  try {
    const res = await axios.get('/v1_0/channels');
    console.log(res);
    channelId.innerHTML = res.data.channels
      .map((item) => {
        return `<option value="${item.id}">${item.name}</option>`;
      })
      .join('');
  } catch (error) {
    console.error(error);
  }
}

renderChannels();

// 获取选择的审核状态
for (const radio of document.querySelectorAll('.form-check-input')) {
  radio.addEventListener('change', async function (e) {
    params.status = this.value;
  });
}

// 获取选择的频道 id
channelId.addEventListener('change', async function (e) {
  params.channel_id = this.value;
});

// 筛选按钮绑定事件
const selBtn = document.querySelector('button.sel-btn');
selBtn.addEventListener('click', async () => {
  renderArticles();
});

/**
 * 目标 3:分页功能
 *  3.1 保存并设置文章总条数
 *  3.2 点击下一页,做临界值判断,并切换页码参数并请求最新数据
 *  3.3 点击上一页,做临界值判断,并切换页码参数并请求最新数据
 */
const lastBtn = document.querySelector('.last');
const nextBtn = document.querySelector('.next');
lastBtn.addEventListener('click', async (e) => {
  nowPage > 1
    ? nowPage-- && (await renderArticles())
    : myAlert(false, '已经是第一页了') && console.log('已经是第一页了');
});
nextBtn.addEventListener('click', async (e) => {
  nowPage < totalPage
    ? nowPage++ && (await renderArticles())
    : myAlert(false, '已经是最后一页了') && console.log('已经是最后一页了');
});

/**
 * 目标 4:删除功能
 *  4.1 关联文章 id 到删除图标
 *  4.2 点击删除时,获取文章 id
 *  4.3 调用删除接口,传递文章 id 到服务器
 *  4.4 重新获取文章列表,并覆盖展示
 *  4.5 删除最后一页的最后一条,需要自动向前翻页
 */

async function delArticle() {
  artList.addEventListener('click', async (e) => {
    const delBtn = e.target.classList.contains('del');
    // console.log(delBtn);
    if (delBtn) {
      const id = e.target.parentNode.getAttribute('data-id');
      try {
        const res = await axios.delete(`/v1_0/mp/articles/${id}`);
        // console.log(res);
        // 如果删除的是当前页面的最后一条,需要自动向前翻页
        const articleNum = artList.children.length;
        if (articleNum === 1 && nowPage > 1) {
          nowPage--;
          await renderArticles();
        }
      } catch (error) {
        console.error(error);
      }
    }
  });
}

delArticle();

// 点击编辑时,获取文章 id,跳转到发布文章页面传递文章 id 过去
artList.addEventListener('click', (e) => {
  const editBtn = e.target.classList.contains('edit');
  console.log(editBtn);
  if (editBtn) {
    const id = e.target.parentNode.getAttribute('data-id');
    location.href = `../publish/index.html?id=${id}`;
  }
});
js

/**
 * 目标 3:发布文章保存
 *  3.1 基于 form-serialize 插件收集表单数据对象
 *  3.2 基于 axios 提交到服务器保存
 *  3.3 调用 Alert 警告框反馈结果给用户
 *  3.4 重置表单并跳转到列表页
 */
const sendBtn = document.querySelector('button.send');
const form = document.querySelector('.art-form');

async function publishArticle() {
  sendBtn.addEventListener('click', async (e) => {
    if (e.target.innerHTML !== '发布') return;
    const formData = serialize(form, { hash: true, empty: true });
    console.log(formData); // channel_id, content, id, title

    // formData 中多了 id 属性,少了 cover(type, images) 属性
    formData.id = undefined;
    formData.cover = {
      type: 1, // 文章封面类型,默认传递 1-1 张图
      images: [rounded.src], // 文章封面地址数组
    };

    try {
      const res = await axios.post('/v1_0/mp/articles', formData);
      console.log(res);
      myAlert(true, `发布成功 ~ ${res.message}`);

      // 重置表格,封面,富文本编辑器和 textarea
      form.reset();
      rounded.src = '';
      rounded.classList.remove('show');
      place.classList.remove('hide');
      editor.setHtml('');
      document.querySelector('textarea.publish-content').value = '';

      // 1.5s 后跳转到内容管理页面
      setTimeout(() => {
        window.location.href = '../content/index.html';
      }, 1500);
    } catch (error) {
      console.dir(error);
      myAlert(false, `发布失败 ~ ${error.response.data.message}`);
    }
  });
}

publishArticle();
/**
 * 目标 4:编辑 - 回显文章
 *  4.1 页面跳转传参(URL 查询参数方式)
 *  4.2 发布文章页面接收参数判断(共用同一套表单)
 *  4.3 修改标题和按钮文字
 *  4.4 获取文章详情数据并回显表单
 */
(async () => {
  // 4.2 发布文章页面接收参数判断(共用同一套表单)
  const params = new URLSearchParams(location.search);
  for (const [key, value] of params) {
    // 当前有要编辑的文章 id 被传入过来
    if (key === 'id') {
      // 4.3 修改标题和按钮文字
      document.querySelector('.title span').innerHTML = '修改文章';
      sendBtn.innerHTML = '修改';
      // 4.4 获取文章详情数据并回显表单
      const res = await axios.get(`/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,
      };
      // 遍历数据对象属性,映射到页面元素上,快速赋值
      for (const key of Object.keys(dataObj)) {
        if (key === 'rounded') {
          if (dataObj[key]) {
            rounded.src = dataObj[key];
            rounded.classList.add('show');
            place.classList.add('hide');
          }
        } else if (key === 'content') {
          editor.setHtml(dataObj[key]);
        } else {
          // 用数据对象属性名,作为标签 name 属性选择器值来找到匹配的标签
          document.querySelector(`[name=${key}]`).value = dataObj[key];
        }
      }
    }
  }
})();

/**
 * 目标 5:编辑 - 保存文章
 *  5.1 判断按钮文字,区分业务(因为共用一套表单)
 *  5.2 调用编辑文章接口,保存信息到服务器
 *  5.3 基于 Alert 反馈结果消息给用户
 */
sendBtn.addEventListener('click', async (e) => {
  if (e.target.innerHTML !== '修改') return;
  const formData = serialize(form, { hash: true, empty: true });
  console.log(formData); // channel_id, content, id, title

  // formData 中少了 cover(type, images) 属性
  formData.cover = {
    type: rounded.src ? 1 : 0, // 文章封面类型,默认传递 1-1 张图
    images: [rounded.src], // 文章封面地址数组
  };

  try {
    const res = await axios.put(`/v1_0/mp/articles/${formData.id}`, formData);
    console.log(res);
    myAlert(true, `修改成功 ~ ${res.message}`);

    // 1.5s 后跳转到内容管理页面
    setTimeout(() => {
      window.location.href = '../content/index.html';
    }, 1500);
  } catch (error) {
    console.dir(error);
    myAlert(false, `修改失败 ~ ${error.response.data.message}`);
  }
});