本节目标
完成项目
- 项目介绍
- 验证码登录
- 统一处理请求
- 富文本编辑器
- 频道下拉菜单
- 封面上传
- 文章列表展示
- 筛选功能
- 分页功能
- 删除功能
- 编辑文章(回显)
- 编辑文章(保存)
- 退出登录
项目介绍
介绍
头条数据管理平台: 对IT资源移动网站的数据进行管理
移动网站(演示): 极客园
主要功能
- 登录和权限判断
- 查看文章内容列表(筛选, 分页)
- 编辑文章(数据回显)
- 删除文章
- 发布文章(图片上传, 富文本编辑器)
技术选型
- 基于Bootstrap搭建网站标签和样式
- 集成wangEditor插件, 实现富文本编辑器
- 使用原生JS完成增删改查业务
- 基于axios进行前后端交互
- 使用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中
拦截流程
处理响应结果
- 简化axios的响应数据结构 (注意会影响之前的数据读取)
- 理登录过期的状态
// 响应拦截器
// 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
使用步骤
- 引入css样式
- 定义html结构
- 引入js创建编辑器
- 监听内容变化, 进行处理
- 富文本编辑器重置内容: 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>
<!-- 富文本编辑器位置 -->
<div id="editor—wrapper">
<div id="toolbar-container"><!-- 工具栏 --></div>
<div id="editor-container"><!-- 编辑器 --></div>
</div>
<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: '<p><br></p>',
// 添加配置对象
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 onl oad = 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&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('')
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', () => {
onl oad()
})
分页功能
/**
* 目标3:分页功能
* 3.1 保存并设置文章总条数
* 3.2 点击下一页,做临界值判断,并切换页码参数并请求最新数据
* 3.3 点击上一页,做临界值判断,并切换页码参数并请求最新数据
*/
// 点击下一页
document.querySelector('.next').addEventListener('click', function () {
if (params.page < Math.ceil(tatalCount / params.per_page)) {
params.page++
onl oad()
document.querySelector('.page-now').innerHTML = `第 ${params.page} 页`
}
})
// 点击上一页
document.querySelector('.last').addEventListener('click', function () {
if (params.page > 1) {
params.page--
onl oad()
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--
}
// 刷新页面
onl oad()
}
})
编辑文章(回显)
/**
* 目标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'
})
标签:axios,const,admin,querySelector,hm,document,data,id,头条
From: https://blog.csdn.net/CSDN20221005/article/details/139367617