数据管理平台
1. 项目介绍
2. 项目准备
3. 验证码登录
/**
* 目标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
}).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)
})
})
// axios 公共配置
// 基地址
axios.defaults.baseURL = 'http://geek.itheima.net'
// 弹窗插件
// 需要先准备 alert 样式相关的 DOM
/**
* BS 的 Alert 警告框函数,2秒后自动消失
* @param {*} isSuccess 成功 true,失败 false
* @param {*} msg 提示消息
*/
function myAlert(isSuccess, msg) {
const myAlert = document.querySelector('.alert')
myAlert.classList.add(isSuccess ? 'alert-success' : 'alert-danger')
myAlert.innerHTML = msg
myAlert.classList.add('show')
setTimeout(() => {
myAlert.classList.remove(isSuccess ? 'alert-success' : 'alert-danger')
myAlert.innerHTML = ''
myAlert.classList.remove('show')
}, 2000)
}
3.1 验证码登录-流程
4. token 的介绍
5. token 的使用
/**
* 目标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
}).then(result => {
// 1.4 使用 Bootstrap 的 Alert 警告框反馈结果给用户
myAlert(true, '登录成功')
console.log(result)
// 登录成功后,保存 token 令牌字符串到本地,并跳转到内容列表页面
localStorage.setItem('token', result.data.token)
setTimeout(() => {
// 延迟跳转,让 alert 警告框停留一会儿
location.href = '../content/index.html'
}, 1500)
}).catch(error => {
myAlert(false, error.response.data.message)
console.dir(error.response.data.message)
})
})
* //目标1:访问权限控制
* //1.1 判断无 token 令牌字符串,则强制跳转到登录页
* //1.2 登录成功后,保存 token 令牌字符串到本地,并跳转到内容列表页面
*/
// 1.1 判断无 token 令牌字符串,则强制跳转到登录页
const token = localStorage.getItem('token')
if (!token) {
location.href = '../login/index.html'
}
6. 个人信息设置和axios 请求拦截器
在use方法调用时,传递2个函数体进去就行了,
第一个函数体,就是请求拦截器要对本次请求的相关参数的配置对象可以进行一些额外的设置
比如说从本地取出token,如果有token,就往这个请求参数的配置对象当中,往他的headers,也就是请求头里面添加一个参数名和对应的值,接着把这个config请求参数的配置对象在return到这个axios的源码内,
而第二个函数体,则是在我们请求发起之前有什么错误信息,然后做一些相关处理
/**
* 目标2:设置个人信息
* 2.1 在 utils/request.js 设置请求拦截器,统一携带 token
* 2.2 请求个人信息并设置到页面
*/
// 2.2 请求个人信息并设置到页面
axios({
url: '/v1_0/user/profile'
}).then(result => {
const username = result.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);
});
7. axios 响应拦截器和身份验证失败
// 添加响应拦截器
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);
});
8. 优化-axios 响应结果
就是在裸界面发起的这些axios请求,在他成功的响应结果的result这个参数上,我想让result直接就是服务器返回的数据对象,那我们现在axios返回的这个result成功的结果对象是这个样子
那这个对象,他就是axios内部封装的一个接口对象,他会把我们服务器返回的这个数据对象挂载到data的这个属性下,所以要访问他的昵称,需要用这个result怎么访问这个name属性的值,先.data,再.data后端返回的这个对象里面的data属性,然后再.name属性就拿到
现在要优化,就像让这个项目当中所有成功的result结果,直接就是我们这个服务器返回 的这个数据对象
就可以基于axios的响应拦截器,因为我们整个项目用的这个相同的axios在服务器返回数据的时候,都会统一经过这个响应拦截器的拦截,而这个respense的值就是axios这个截图的这个结果对象
所以我们可以在这个response的基础上.data属性,就取出本次服务器返回的成功数据对象,接着在这个函数中的return这个result的这个服务器返回的这个数据对象会return到这个axios的源码内,axios就会把他当作成功的promise状态的对应结果,就会返回到当时axios调用时,在原地留下一个proise对象,这个promise对象的状态就会变成成功已兑现的状态,这个已兑现的值就是return这个result会传递给then的回调函数
/ 添加响应拦截器
axios.interceptors.response.use(function (response) {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么,例如:直接返回服务器的响应结果对象
const result = response.data
return result;
}, function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么,例如:统一对 401 身份验证失败情况做出处理
console.dir(error)
if (error?.response?.status === 401) {
alert('身份验证失败,请重新登录')
localStorage.clear()
location.href = '../login/index.html'
}
return Promise.reject(error);
});
/**
* 目标2:设置个人信息
* 2.1 在 utils/request.js 设置请求拦截器,统一携带 token
* 2.2 请求个人信息并设置到页面
*/
// 2.2 请求个人信息并设置到页面
axios({
url: '/v1_0/user/profile'
}).then(result => {
const username = result.data.name
document.querySelector('.nick-name').innerHTML = username
})
9. 发布文章-富文本编辑器
<div>
<label for="">内容:</label>
<!-- 富文本编辑器位置 -->
<div id="editor—wrapper">
<div id="toolbar-container"><!-- 工具栏 --></div>
<div id="editor-container"><!-- 编辑器 --></div>
</div>
<!-- 记录富文本内容-用于表单收集 -->
<textarea name="content" class="publish-content" hidden></textarea>
</div>
/* 富文本编辑器 */
#editor—wrapper {
border: 1px solid #ccc;
z-index: 100; /* 按需定义 */
}
#toolbar-container { border-bottom: 1px solid #ccc; }
#editor-container { height: 500px; }
// 富文本编辑器
// 创建编辑器函数,创建工具栏函数
const { createEditor, createToolbar } = window.wangEditor
// 编辑器配置对象
const editorConfig = {
// 占位提示文字
placeholder: '发布文章内容...',
// 编辑器变化时回调函数
onChange(editor) {
// 获取富文本内容
const html = editor.getHtml()
// 也可以同步到 <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'
})
10. 发布文章-频道列表
/**
* 目标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('')
document.querySelector('.form-select').innerHTML = htmlStr
}
// 网页运行后,默认调用一次
setChannleList()
11. 发布文章-封面设置
/**
* 目标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
})
// 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()
})
/**
12. 发布文章-收集并保存
动态创建还得用事件委托
/**
* 目标3:发布文章保存
* 3.1 基于 form-serialize 插件收集表单数据对象
* 3.2 基于 axios 提交到服务器保存
* 3.3 调用 Alert 警告框反馈结果给用户
* 3.4 重置表单并跳转到列表页
*/
// 3.1 基于 form-serialize 插件收集表单数据对象
document.querySelector('.send').addEventListener('click', async e => {
if (e.target.innerHTML !== '发布') return
const form = document.querySelector('.art-form')
const data = serialize(form, { hash: true, empty: true })
// 发布文章的时候,不需要 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) {
myAlert(false, error.response.data.message)
}
})
13. 内容管理-文章列表展示
/**
* 目标1:获取文章列表并展示
* 1.1 准备查询参数对象
* 1.2 获取文章列表数据
* 1.3 展示到指定的标签结构中
*/
// 1.1 准备查询参数对象
const queryObj = {
status: '', // 文章状态(1-待审核,2-审核通过)空字符串-全部
channel_id: '', // 文章频道 id,空字符串-全部
page: 1, // 当前页码
per_page: 2 // 当前页面条数
}
let totalCount = 0 // 保存文章总条数
// 获取并设置文章列表
async function setArtileList() {
// 1.2 获取文章列表数据
const res = await axios({
url: '/v1_0/mp/articles',
params: queryObj
})
// 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>
//如果说还有第三种状态,就得嵌套条件表达式,现在在模板字符串里面,${}里面只能写表达式,不能写if else语句
${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 data-id="${item.id}">
<i class="bi bi-pencil-square edit"></i>
<i class="bi bi-trash3 del"></i>
</td>
</tr>`).join('')
document.querySelector('.art-list').innerHTML = htmlStr
// 3.1 保存并设置文章总条数
totalCount = res.data.total_count
document.querySelector('.total-count').innerHTML = `共 ${totalCount} 条`
}
setArtileList()
14. 内容管理-筛选功能
/**
* 目标2:筛选文章列表
* 2.1 设置频道列表数据
* 2.2 监听筛选条件改变,保存查询信息到查询参数对象
* 2.3 点击筛选时,传递查询参数对象到服务器
* 2.4 获取匹配数据,覆盖到页面展示
*/
// 2.1 设置频道列表数据
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 => {
queryObj.status = e.target.value
})
})
// 筛选频道 id -> change事件 -> 绑定到查询参数对象上
document.querySelector('.form-select').addEventListener('change', e => {
queryObj.channel_id = e.target.value
})
// 2.3 点击筛选时,传递查询参数对象到服务器
document.querySelector('.sel-btn').addEventListener('click', () => {
// 2.4 获取匹配数据,覆盖到页面展示
setArtileList()
})
为什么不在点击筛选时,再收集这个查询条件,而是要单个的这种绑定,用户有可能再点击完筛选条件之后不去点击这个按钮,反而要直接去点击分页切换,为了避免这个bug,就在他切换时,就影响这个查询条件的对象,也是为了后续用vue做项目立刻把这个条件数据进行绑定的思想的锻炼
15. 内容管理-分页功能
let totalCount = 0 // 保存文章总条数
async function setArtileList() {
// 1.2 获取文章列表数据
const res = await axios({
url: '/v1_0/mp/articles',
params: queryObj
})
// 3.1 保存并设置文章总条数
totalCount = res.data.total_count
document.querySelector('.total-count').innerHTML = `共 ${totalCount} 条`
}
/**
* 目标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()
}
})
16. 内容管理-删除功能
/**
* 目标4:删除功能
* 4.1 关联文章 id 到删除图标
* 4.2 点击删除时,获取文章 id
* 4.3 调用删除接口,传递文章 id 到服务器
* 4.4 重新获取文章列表,并覆盖展示
* 4.5 删除最后一页的最后一条,需要自动向前翻页
*/
// 4.1 关联文章 id 到删除图标
<td data-id="${item.id}">
<i class="bi bi-pencil-square edit"></i>
<i class="bi bi-trash3 del"></i>
</td>
// 4.2 点击删除时,获取文章 id
//用事件委托,因为整个tr就是靠上边的数组循环生成的,而tbody网页打开就有了,所以给已存在的元素绑点击事件
document.querySelector('.art-list').addEventListener('click', async e => {
// 判断点击的是删除元素
if (e.target.classList.contains('del')) {
const delId = e.target.parentNode.dataset.id
// 4.3 调用删除接口,传递文章 id 到服务器
const res = await axios({
url: `/v1_0/mp/articles/${delId}`,
method: 'DELETE'
})
// 4.4 重新获取文章列表,并覆盖展示
setArtileList()
}
})
})
17. 内容管理-删除最后一条
// 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} 页`
}
18. 内容管理-编辑文章-回显
// 点击编辑时,获取文章 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]
}
})
}
})
})();
19. 内容管理-编辑文章-保存
/**
* 目标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 })
// 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)
}
})
20. 退出登录
/**
* 目标3:退出登录
* 3.1 绑定点击事件
* 3.2 清空本地缓存,跳转到登录页面
*/
// 3.1 绑定点击事件
document.querySelector('.quit').addEventListener('click', e => {
// 3.2 清空本地缓存,跳转到登录页面
localStorage.clear()
location.href = '../login/index.html'
})