黑马程序员视频地址:
前言
功能:
1.登录和权限判断
2.查看文章内容列表(筛选,分页)
3.编辑文章(数据回显)
4.删除文章
5.发布文章(图片上传,富文本编辑器)
技术:1.基于Bootstrap 搭建网站标签和样式
2.集成wangEditor 插件实现富文本编辑器
3.使用原生JS 完成增删改查等业务
4.基于axios 与黑马头条线上接口交互
5.使用axios 拦截器进行权限判断
项目准备:准备配套的素材代码
包含:html,css,js,静态图片,第三方插件等等
欢迎使用 - B站-AJAX和黑马头条-数据管理平台
目录管理:• assets:资源文件夹(图片,字体等)
• lib:资料文件夹(第三方插件,例如:form-serialize)
• page:页面文件夹
• utils:实用程序文件夹(工具插件)
验证码登录
1.在utils/request.js中配置基地址
javascript
// axios 公共配置
// 基地址
axios.defaults.baseURL = "http://geek.itheima.net"
2.登录按钮代码
javascript
document.querySelector(".btn").addEventListener("click", () => {
const form = document.querySelector(".login-form")
const data = serialize(form, {hash: true, empty: true})
axios({
url: "/v1_0/authorizations",
method: "POST",
data
}).then(result => {
myAlert(true, "登录成功!")
}).catch(error => {
myAlert(false, error.response.data.message)
})
})
token
token 的介绍
![](https://i-blog.csdnimg.cn/direct/945f14a1bba64dd7bceb2539efa2f097.png)
token 的使用
- 在utils/auth.js 中判断无 token 令牌字符串,则强制跳转到登录页(手动修改地址栏测试)
javascript
/**
* 目标1:访问权限控制
* 1.1 判断无 token 令牌字符串,则强制跳转到登录页
* 1.2 登录成功后,保存 token 令牌字符串到本地,并跳转到内容列表页面
*/
const token = localStorage.getItem("token")
if(!token)
{
alert("请先登录!")
setTimeout(() => {
location.href = "../login/index.html"
}, 1500)
}
- 在登录成功后,保存token 令牌字符串到本地,再跳转到首页(手动修改地址栏测试)
javascript
then(result => {
//存入token
localStorage.setItem("token", result.data.data.token)
//提示框
myAlert(true, "登录成功!")
//1.5s后跳转新页面
setTimeout(() => {
location.href = "../content/index.html"
}, 1500)
})
axios 请求拦截器和个人信息设置
- 什么是axios 请求拦截器?
✓ 发起请求之前,调用的一个函数,对请求参数进行设置
- axios 请求拦截器,什么时候使用?
✓ 有公共配置和设置时,统一设置在请求拦截器中
官方文档:
1.在utils/request.js文件中配置
javascript
// 添加请求拦截器
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.在utils/auth.js 中请求个人信息并设置到页面
javascript
/**
* 目标2:设置个人信息
* 2.1 在 utils/request.js 设置请求拦截器,统一携带 token
* 2.2 请求个人信息并设置到页面
*/
axios({
url: "/v1_0/user/profile"
}).then(result => {
const username = result.data.data.name
document.querySelector(".nick-name").innerText = username
}).catch(error => {
console.log(error)
})
axios 响应拦截器和身份验证失败
- 什么是axios 响应拦截器?
✓ 响应回到then/catch 之前,触发的拦截函数,对响应结果统一处理
- axios 响应拦截器,什么时候触发成功/失败的回调函数?
✓ 状态为2xx触发成功回调,其他则触发失败的回调函数
javascript
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
response = response.data
//⭐️数据优化,得到的response是服务器数据+axios修饰后的代码,
//而response.data是服务器返回的数据,将服务器返回的数据直接返回给then中
//如上文代码中的result.data.data.name,如果此处返回response,则调用result.data.data.name
//如果此处返回response.data,则调用result.data.name
return response //⭐️response会传进.then(result => {})中,由result接收
}, function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
⭐️if(error?.response?.status === 401)
{
alert("身份验证过期,请重新登录!")
localStorage.clear()
location.href = "../login/index.html"
}
return Promise.reject(error) //⭐️error会传进.catch(error => {})中,由error接收
})
发布文章
富文本编辑器
官方手册:
wangEditor 5https://www.wangeditor.com/v5/getting-started.htmlhttps://www.wangeditor.com/v5/getting-started.htmlhttps://www.wangeditor.com/v5/getting-started.htmlhttps://www.wangeditor.com/v5/getting-started.htmlhttps://www.wangeditor.com/v5/getting-started.htmlhttps://www.wangeditor.com/v5/getting-started.html
https://www.wangeditor.com/v5/getting-started.html手册中有完整步骤
1.引入CSS:在相应页面(publish/index.css)引入CSS
css
/* 富文本编辑器 */
#editor---wrapper {
border: 1px solid #ccc;
z-index: 100; /* 按需定义 */
}
#toolbar-container {
border-bottom: 1px solid #ccc;
}
#editor-container {
height: 500px;
}
2.定义HTML:在需要部署的页面(publish/index.html)放置HTML代码
html
<!-- 富文本编辑器位置 -->
<div id="editor---wrapper">
<div id="toolbar-container"><!-- 工具栏 --></div>
<div id="editor-container"><!-- 编辑器 --></div>
</div>
html
<!-- 记录富文本内容-用于表单收集 -->
<textarea name="content" class="publish-content" hidden></textarea>
3.引入 JS 创建编辑器:在utils/editor.js中放入代码
javascript
// 富文本编辑器
// 创建编辑器函数,创建工具栏函数
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'
})
4.引入文件:在publish/index.html中引入script(黑马程序员已经帮我们引入过了)
html
<!--引入 CSS 定义样式-->
<link
href="https://unpkg.com/@wangeditor/editor@latest/dist/css/style.css"
rel="stylesheet"
/>
<!--引入 JS 创建编辑器-->
<script src="https://unpkg.com/@wangeditor/editor@latest/dist/index.js"></script>
频道列表
目标1:设置频道下拉菜单
1.1 获取频道列表数据
1.2 展示到下拉菜单中
javascript
//publish/index.js
async function getChannels()
{
//请求数据
const channelsData = await axios({url: "/v1_0/channels"})
//拼接数据
const channelsHTML = `<option value="" selected="">请选择文章频道</option>` + channelsData.data.data.channels.map(item => `<option value="${item.id}" >${item.name}</option>`).join("")
//渲染数据
document.querySelector(".form-select").innerHTML = channelsHTML
}
getChannels()
封面设置
目标2:文章封面设置
2.1 准备标签结构和样式
2.2 选择文件并保存在 FormData
2.3 单独上传图片并得到图片 URL 网址
2.4 回显并切换 img 标签展示(隐藏 + 号上传标签)
javascript
document.querySelector(".img-file").addEventListener("change", async e => {
//获取本地上传照片数据
const file = e.target.files[0]
//实例化表单对象
const fd = new FormData()
//添加image属性,并放入图片数据
fd.append("image", file)
//上传数据,返回照片地址
const res = await axios({
url: "/v1_0/upload",
method: "POST",
data: fd
})
const imgUrl = res.data.data.url
//渲染到本地
document.querySelector(".rounded").src = imgUrl //照片回显
document.querySelector(".rounded").classList.add("show") //显示照片盒子
document.querySelector(".place").classList.add("hide") //隐藏加号盒子
})
//⭐️点击图片,模拟点击上传图片按钮,从而再次调用代码
document.querySelector(".rounded").addEventListener("click", () => {
document.querySelector(".img-file").click()
})
收集并保存
目标3:发布文章保存
3.1 基于 form-serialize 插件收集表单数据对象
3.2 基于 axios 提交到服务器保存
3.3 调用 Alert 警告框反馈结果给用户
3.4 重置表单并跳转到列表页
javascript
document.querySelector(".send").addEventListener("click", async e => {
//⭐️编辑功能留空1
//获取表单
const form = document.querySelector(".art-form")
//获取表单数据
const data = serialize(form, {hash: true, empty: true})
//⭐️删除id字段数据
delete data.id
//获取图片数据
data.cover = {type: 1, images: [document.querySelector(".rounded").src]}
//上传数据
try{
const result = await axios({
url: "/v1_0/mp/articles",
method: "POST",
data
})
//调用 Alert 警告框反馈结果给用户
myAlert(true, "发布成功!")
//清除表单
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)
{
//调用 Alert 警告框反馈结果给用户
myAlert(false, error.response.data.message)
}
})
内容管理
文章列表展示
目标1:获取文章列表并展示
1.1 准备查询参数对象
1.2 获取文章列表数据
1.3 展示到指定的标签结构中
标⭐️为代码留空区域,在对应功能模块编写时会添加
javascript
// 1.准备查询参数对象
const queryObj = {
status: '', // 文章状态(1-待审核,2-审核通过)空字符串-全部
channel_id: '', // 文章频道 id,空字符串-全部
page: 1, // 当前页码
per_page: 10 // 当前页面条数
}
//⭐️分页功能留空1
async function getArtileList() {
//2.获取文章列表数据
const res = await axios({
url: '/v1_0/mp/articles',
params: queryObj
})
//3.展示到指定的标签结构中
const htmlStr = res.data.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 "⭐️删除功能留空1">
<i class="bi bi-pencil-square edit"></i>
<i class="bi bi-trash3 del"></i>
</td>
</tr>`).join('')
document.querySelector('.art-list').innerHTML = htmlStr
//⭐️分页功能留空2
}
getArtileList()
筛选功能
目标2:筛选文章列表
2.1 设置频道列表数据
2.2 监听筛选条件改变,保存查询信息到查询参数对象
2.3 点击筛选时,传递查询参数对象到服务器
2.4 获取匹配数据,覆盖到页面展示
javascript
//1.设置频道列表数据
async function getChannels()
{
//请求数据
const channelsData = await axios({url: "/v1_0/channels"})
//拼接数据
const channelsHTML = `<option value="" selected="">请选择文章频道</option>` + channelsData.data.data.channels.map(item => `<option value="${item.id}" >${item.name}</option>`).join("")
//渲染数据
document.querySelector(".form-select").innerHTML = channelsHTML
}
getChannels()
//2.监听筛选条件改变,保存查询信息到查询参数对象
//状态
document.querySelectorAll(".form-check").forEach(item => {
item.addEventListener("change", e => {
queryObj.status = e.target.value
})
})
//频道
document.querySelector(".form-select").addEventListener("change", e => {
queryObj.channel_id = e.target.value
})
//3.点击筛选时,传递查询参数对象到服务器,并获取匹配数据,覆盖到页面展示
document.querySelector(".sel-btn").addEventListener("click", () => {
getArtileList()
})
分页功能
目标3:分页功能
3.1 保存并设置文章总条数
3.2 点击下一页,做临界值判断,并切换页码参数并请求最新数据
3.3 点击上一页,做临界值判断,并切换页码参数并请求最新数据
javascript
//------------------------------------------------------------------------1.代码写在文章列表展示中------------------------------------------------------------------------------------------
//1.1保存文章总条数(对应留空1)
let totalCount = 0
//1.2获取文章总条数(对应留空2)
totalCount = res.data.total_count
document.querySelector('.total-count').innerHTML = `共 ${totalCount} 条`
//------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
//2.点击下一页
document.querySelector(".next").addEventListener("click", () => {
//判断是否可以进行下一页:当前页码小于总页码(总页码=(总条数/每一页最大条数)向上取整)
if(queryObj.page < Math.ceil(totalCount / queryObj.per_page))
{
//页码自增
queryObj.page++
//页面显示当前页码
document.querySelector(".page-now").innerHTML = `第 ${queryObj.page} 页`
//更新数据
getArtileList()
}
})
//3.点击上一页
document.querySelector('.last').addEventListener('click', e => {
// 大于 1 的时候,才能翻到上一页
if (queryObj.page > 1) {
//页码自减
queryObj.page--
//页面显示当前页码
document.querySelector('.page-now').innerHTML = `第 ${queryObj.page} 页`
//更新数据
getArtileList()
}
})
删除功能
目标4:删除功能
4.1 关联文章 id 到删除图标
4.2 点击删除时,获取文章 id
4.3 调用删除接口,传递文章 id 到服务器
4.4 删除最后一页的最后一条,需要自动向前翻页
4.5 重新获取文章列表,并覆盖展示
javascript
//------------------------------------------------------------------------1.关联文章 id------------------------------------------------------------------------
//留空1补自定义属性
data-id="${item.id}"
//------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
//2.点击删除时,获取文章 id
document.querySelector(".art-list").addEventListener("click", async e => {
if(e.target.classList.contains("del"))
{
//2.获取id
const delId = e.target.parentNode.dataset.id
//3.调用删除接口,传递文章 id 到服务器
const res = await axios({
url: `/v1_0/mp/articles/${delId}`,
method: "DELETE"
})
//4.删除最后一页的最后一条,需要自动向前翻页
//获取子元素数组
const children = document.querySelector(".art-list").children
//判断子元素数组长度等于1并且当前页码数不等于1
if(children.length === 1 && queryObj.page !== 1)
{
//页码自减
queryObj.page--
//页面显示当前页码
document.querySelector('.page-now').innerHTML = `第 ${queryObj.page} 页`
}
//5.重新获取并渲染数据
getArtileList()
}
})
编辑功能
回显文章
目标4:编辑-回显文章
4.1 页面跳转传参(URL 查询参数方式)
4.2 发布文章页面接收参数判断(共用同一套表单)
4.3 修改标题和按钮文字
4.4 获取文章详情数据并回显表单
javascript
//content/index.js
//1.点击编辑时,获取文章 id,跳转到发布文章页面传递文章 id 过去
document.querySelector(".art-list").addEventListener("click", e => {
//判断是否点击编辑按钮
if(e.target.classList.contains("edit"))
{
//获取id
const editId = e.target.parentNode.dataset.id
//跳转到发布页面,并传递id
location.href = `../publish/index.html?id=${editId}`
}
})
javascript
//publish/index.js
//使用立即执行函数(IIFE)创建一个独立的作用域,避免变量污染全局作用域
;(function(){
//⭐️获取网页中携带的参数
const paramsStr = location.search
//⭐️实例化内置对象(用来处理?后面内容的对象)
const params = new URLSearchParams(paramsStr)
params.forEach(async (value, key) => {
//判断是否含有id参数
if(key === "id")
{
//修改页面文字
document.querySelector(".title span").innerHTML = "修改文章"
document.querySelector(".send").innerHTML = "修改"
//获取文章详情
const res = await axios({url: `/v1_0/mp/articles/${value}`})
//整理需要用的值
//方法一:解构
// const {channel_id, title, cover:{images:[image]}, content, id} = res.data.data
//方法二:创立新对象挨个赋值(本篇选择此方法)
const dataObj = {
channel_id: res.data.data.channel_id,
title: res.data.data.title,
image: res.data.data.cover.images[0],
content: res.data.data.content,
id: res.data.data.id
}
console.log(dataObj)
//数据渲染
Object.keys(dataObj).forEach(key => {
if(key === "image")
{
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]
}
})
}
})
})();
保存文章
目标5:编辑-保存文章
5.1 判断按钮文字,区分业务(因为共用一套表单)
5.2 调用编辑文章接口,保存信息到服务器
5.3 基于 Alert 反馈结果消息给用户
javascript
//区分发布按钮与修改按钮
//------------------------------------------------------------------发布文章/收集并保存中的留空1代码------------------------------------------------------------------------------------
//判断是否为发布按钮
if(e.target.innerHTML != "发布") return
//------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
javascript
document.querySelector(".send").addEventListener("click", async e => {
//判断是否为修改按钮
if(e.target.innerHTML !== "修改") return
//获取表单对象
const form = document.querySelector(".art-form")
//使用serialize插件获取表单数据
const data = serialize(form, {hash: true, empty: true})
//提交数据
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]
}
}
})
//成功弹窗
myAlert(true, "修改成功!")
//返回内容管理页面
setTimeout(() =>{
location.href = "../content/index.html"
}, 1500)
}
catch(error)
{
//异常弹窗
myAlert(false, error.response.data.message)
}
})
退出登录
目标3:退出登录
3.1 绑定点击事件
3.2 清空本地缓存,跳转到登录页面
javascript
//utils/auth.js
//绑定事件
document.querySelector(".quit").addEventListener("click", () => {
//清空本地缓存
localStorage.clear()
//提示框
myAlert(true, "退出成功!即将返回登陆页面!")
//跳转登录页面
setTimeout(() => {
location.href ="../login/index.html"
}, 1500)
})