黑马 springboot3+vue3(大事件)笔记分享(前端)

一、实战篇(前端)

在这里插入图片描述
JavaScript-导入导出
在这里插入图片描述
在这里插入图片描述

JS提供的导入导出机制,可以实现按需导入。
在这里插入图片描述
或者
在这里插入图片描述

在这里插入图片描述
导入和导出的时候, 可以使用 as 重命名:如complexMessage as cm
默认导出
在这里插入图片描述
在这里插入图片描述

1、Vue

Vue 是一款用于构建用户界面的渐进式的JavaScript框架。 (官方:https://cn.vuejs.org/
在这里插入图片描述
在这里插入图片描述
学习路径
在这里插入图片描述

1、快速入门

https://cn.vuejs.org
–准备
1、准备html页面,并引入Vue模块(官方提供)
2、创建Vue程序的应用实例
3、准备元素(div),被Vue控制
–构建用户界面
1、准备数据
2、通过插值表达式渲染页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app"><!-- 准备元素(div),被Vue控制 -->
        <h1>{{msg}}</h1><!-- 通过插值表达式渲染页面 -->
    </div>

    <div >
        <h1>{{msg}}</h1>
    </div>
    <!-- 引入vue模块 -->
    <script type="module">
        import {createApp} from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js';//引入Vue模块
        /* 创建vue的应用实例 */
        createApp({//创建Vue程序的应用实例
            data(){//准备数据
                return {
                    //定义数据
                    msg: 'hello vue3'
                }
            }

        }).mount("#app");
    </script>
</body>
</html>

2、常用指令

指令:HTML标签上带有 v-前缀的特殊属性,不同的指令具有不同的含义,可以实现不同的功能。
常用指令:
在这里插入图片描述

v-for

在这里插入图片描述
作用:列表渲染,遍历容器的元素或者对象的属性
语法: v-for = “(item,index) in items”
–参数说明:
items 为遍历的数组
item 为遍历出来的元素
index 为索引/下标,从0开始 ;可以省略,省略index语法: v-for = “item in items”
注意:遍历的数组,必须在data中定义; 要想让哪个标签循环展示多次,就在哪个标签上使用 v-for 指令。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>

    <div id="app">
        <table border="1 solid" colspa="0" cellspacing="0">
            <tr>
                <th>文章标题</th>
                <th>分类</th>
                <th>发表时间</th>
                <th>状态</th>
                <th>操作</th>
            </tr>
            <!-- 哪个元素要出现多次,v-for指令就添加到哪个元素上 -->
            <tr v-for="(article,index) in articleList">
                <td>{{article.title}}</td>
                <td>{{article.category}}</td>
                <td>{{article.time}}</td>
                <td>{{article.state}}</td>
                <td>
                    <button>编辑</button>
                    <button>删除</button>
                </td>
            </tr>
            <!-- <tr>
                <td>标题2</td>
                <td>分类2</td>
                <td>2000-01-01</td>
                <td>已发布</td>
                <td>
                    <button>编辑</button>
                    <button>删除</button>
                </td>
            </tr>
            <tr>
                <td>标题3</td>
                <td>分类3</td>
                <td>2000-01-01</td>
                <td>已发布</td>
                <td>
                    <button>编辑</button>
                    <button>删除</button>
                </td>
            </tr> -->
        </table>
    </div>

    <script type="module">
        //导入vue模块
        import { createApp} from 
                'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
        //创建应用实例
        createApp({
            data() {
                return {
                  //定义数据
                    articleList:[{
                                title:"医疗反腐绝非砍医护收入",
                                category:"时事",
                                time:"2023-09-5",
                                state:"已发布"
                            },
                            {
                                title:"中国男篮缘何一败涂地?",
                                category:"篮球",
                                time:"2023-09-5",
                                state:"草稿"
                            },
                            {
                                title:"华山景区已受大风影响阵风达7-8级,未来24小时将持续",
                                category:"旅游",
                                time:"2023-09-5",
                                state:"已发布"
                            }]  
                }
            }
        }).mount("#app")//控制页面元素
        
    </script>
</body>
</html>
v-bind

作用:动态为HTML标签绑定属性值,如设置href,src,style样式等。
语法:v-bind:属性名=“属性值”
简化::属性名=“属性值”
注意:v-bind所绑定的数据,必须在data中定义 。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <!-- <a v-bind:href="url">黑马官网</a> -->
        <a :href="url">黑马官网</a>
    </div>

    <script type="module">
        //引入vue模块
        import { createApp} from 
                'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
        //创建vue应用实例
        createApp({
            data() {
                return {
                    url: 'https://www.itheima.com'
                }
            }
        }).mount("#app")//控制html元素
    </script>
</body>
</html>
v-if & v-show

作用:这两类指令,都是用来控制元素的显示与隐藏的
v-if
语法:v-if=“表达式”,表达式值为 true,显示;false,隐藏
其它:可以配合 v-else-if / v-else 进行链式调用条件判断
原理:基于条件判断,来控制创建或移除元素节点(条件渲染)
场景:要么显示,要么不显示,不频繁切换的场景

v-show
语法:v-show=“表达式”,表达式值为 true,显示;false,隐藏
原理:基于CSS样式display来控制显示与隐藏
场景:频繁切换显示隐藏的场景

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <div id="app">

        手链价格为:  <span v-if="customer.level>=0 && customer.level<=1">9.9</span>  
                    <span v-else-if="customer.level>=2 && customer.level<=4">19.9</span> 
                    <span v-else>29.9</span>

        <br/>
        手链价格为:  <span v-show="customer.level>=0 && customer.level<=1">9.9</span>  
                    <span v-show="customer.level>=2 && customer.level<=4">19.9</span> 
                    <span v-show="customer.level>=5">29.9</span>

    </div>

    <script type="module">
        //导入vue模块
        import { createApp} from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'

        //创建vue应用实例
        createApp({
            data() {
                return {
                    customer:{
                        name:'张三',
                        level:2
                    }
                }
            }
        }).mount("#app")//控制html元素
    </script>
</body>

</html>
v-on

作用:为html标签绑定事件
语法:
v-on:事件名=“函数名”
简写为 @事件名=“函数名”
createApp({ data(){需要用到的数据}, methods:{需要用到的方法} })

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <button v-on:click="money">点我有惊喜</button> &nbsp;
        <button @click="love">再点更惊喜</button>
    </div>

    <script type="module">
        //导入vue模块
        import { createApp} from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'

        //创建vue应用实例
        createApp({
            data() {
                return {
                    //定义数据
                }
            },
            methods:{
                money: function(){
                    alert('送你钱100')
                },
                love: function(){
                    alert('爱你一万年')
                }
            }
        }).mount("#app");//控制html元素

    </script>
</body>
</html>
v-model

作用:在表单元素上使用,双向数据绑定。可以方便的 获取 或 设置 表单项数据
语法:v-model=“变量名”
在这里插入图片描述
在这里插入图片描述
注意:v-model 中绑定的变量,必须在data中定义。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <div id="app">

        文章分类: <input type="text" v-model="searchConditions.category"/> <span>{{searchConditions.category}}</span>

        发布状态: <input type="text" v-model="searchConditions.state"/> <span>{{searchConditions.state}}</span>

        <button>搜索</button>
        <button v-on:click="clear">重置</button>

        <br />
        <br />
        <table border="1 solid" colspa="0" cellspacing="0">
            <tr>
                <th>文章标题</th>
                <th>分类</th>
                <th>发表时间</th>
                <th>状态</th>
                <th>操作</th>
            </tr>
            <tr v-for="(article,index) in articleList">
                <td>{{article.title}}</td>
                <td>{{article.category}}</td>
                <td>{{article.time}}</td>
                <td>{{article.state}}</td>
                <td>
                    <button>编辑</button>
                    <button>删除</button>
                </td>
            </tr>
        </table>
    </div>
    <script type="module">
        //导入vue模块
        import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
        //创建vue应用实例
        createApp({
            data() {
                return {
                    //定义数据
                    searchConditions:{
                        category:'',
                        state:''
                    },

                    articleList: [{
                        title: "医疗反腐绝非砍医护收入",
                        category: "时事",
                        time: "2023-09-5",
                        state: "已发布"
                    },
                    {
                        title: "中国男篮缘何一败涂地?",
                        category: "篮球",
                        time: "2023-09-5",
                        state: "草稿"
                    },
                    {
                        title: "华山景区已受大风影响阵风达7-8级,未来24小时将持续",
                        category: "旅游",
                        time: "2023-09-5",
                        state: "已发布"
                    }]
                }
            }
            ,
            methods:{
                clear:function(){
                    //清空category以及state的数据
                    //在methods对应的方法里面,使用this就代表的是vue实例,可以使用this获取到vue实例中准备的数据
                    this.searchConditions.category='';
                    this.searchConditions.state='';
                }
            }
            ,
            mounted:function(){
                console.log('Vue挂载完毕,发送请求获取数据')
            }
        }).mount("#app")//控制html元素
    </script>
</body>

</html>

3、生命周期

生命周期:指一个对象从创建到销毁的整个过程。
生命周期的八个阶段:每个阶段会自动执行一个生命周期方法(钩子), 让开发者有机会在特定的阶段执行自己的代码
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Axios

介绍:Axios 对原生的Ajax进行了封装,简化书写,快速开发。
官网:https://www.axios-http.cn/

Axios使用步骤
引入Axios的js文件(参照官网)
使用Axios发送请求,并获取相应结果
在这里插入图片描述

Axios-请求方式别名
为了方便起见,Axios已经为所有支持的请求方法提供了别名
格式:axios.请求方式(url [, data [, config]])
在这里插入图片描述

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <!-- 引入axios的js文件 -->
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script>
        /* 发送请求 */
        /* axios({
            method:'get',
            url:'http://localhost:8080/article/getAll'
        }).then(result=>{
            //成功的回调
            //result代表服务器响应的所有的数据,包含了响应头,响应体. result.data 代表的是接口响应的核心数据
            console.log(result.data);
        }).catch(err=>{
            //失败的回调
            console.log(err);
        }); */
        let article = {
            title: '明天会更好',
            category: '生活',
            time: '2000-01-01',
            state: '草稿'
        }
        /*  axios({
             method:'post',
             url:'http://localhost:8080/article/add',
             data:article
         }).then(result=>{
             //成功的回调
             //result代表服务器响应的所有的数据,包含了响应头,响应体. result.data 代表的是接口响应的核心数据
             console.log(result.data);
         }).catch(err=>{
             //失败的回调
             console.log(err);
         }); */

        //别名的方式发送请求
        /* axios.get('http://localhost:8080/article/getAll').then(result => {
            //成功的回调
            //result代表服务器响应的所有的数据,包含了响应头,响应体. result.data 代表的是接口响应的核心数据
            console.log(result.data);
        }).catch(err => {
            //失败的回调
            console.log(err);
        }); */
        axios.post('http://localhost:8080/article/add', article).then(result => {
            //成功的回调
            //result代表服务器响应的所有的数据,包含了响应头,响应体. result.data 代表的是接口响应的核心数据
            console.log(result.data);
        }).catch(err => {
            //失败的回调
            console.log(err);
        });
    </script>
</body>

</html>
案例

使用表格展示所有文章的数据, 并完成条件搜索功能
在这里插入图片描述
钩子函数mounted中, 获取所有的文章数据
使用v-for指令,把数据渲染到表格上展示
使用v-model指令完成表单数据的双向绑定
使用v-on指令为搜索按钮绑定单击事件

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <div id="app">

        文章分类: <input type="text" v-model="searchConditions.category">

        发布状态: <input type="text"  v-model="searchConditions.state">

        <button v-on:click="search">搜索</button>

        <br />
        <br />
        <table border="1 solid" colspa="0" cellspacing="0">
            <tr>
                <th>文章标题</th>
                <th>分类</th>
                <th>发表时间</th>
                <th>状态</th>
                <th>操作</th>
            </tr>
            <tr v-for="(article,index) in articleList">
                <td>{{article.title}}</td>
                <td>{{article.category}}</td>
                <td>{{article.time}}</td>
                <td>{{article.state}}</td>
                <td>
                    <button>编辑</button>
                    <button>删除</button>
                </td>
            </tr>
            <!-- <tr>
                <td>标题2</td>
                <td>分类2</td>
                <td>2000-01-01</td>
                <td>已发布</td>
                <td>
                    <button>编辑</button>
                    <button>删除</button>
                </td>
            </tr>
            <tr>
                <td>标题3</td>
                <td>分类3</td>
                <td>2000-01-01</td>
                <td>已发布</td>
                <td>
                    <button>编辑</button>
                    <button>删除</button>
                </td>
            </tr> -->
        </table>
    </div>
    <!-- 导入axios的js文件 -->
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script type="module">
        //导入vue模块
        import {createApp} from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js';
        //创建vue应用实例
        createApp({
            data(){
                return {
                    articleList:[],
                    searchConditions:{
                        category:'',
                        state:''
                    }
                }
            },
            methods:{
                //声明方法
                search:function(){
                    //发送请求,完成搜索,携带搜索条件
                    axios.get('http://localhost:8080/article/search?category='+this.searchConditions.category+'&state='+this.searchConditions.state)
                    .then(result=>{
                        //成功回调 result.data
                        //把得到的数据赋值给articleList
                        this.articleList=result.data
                    }).catch(err=>{
                        console.log(err);
                    });
                }
            },
            //钩子函数mounted中,获取所有文章数据
            mounted:function(){
                //发送异步请求  axios
                axios.get('http://localhost:8080/article/getAll').then(result=>{
                    //成功回调
                    //console.log(result.data);
                    this.articleList=result.data;
                }).catch(err=>{
                    //失败回调
                    console.log(err);
                });
            }
        }).mount('#app');//控制html元素
    </script>
</body>

</html>

2、整站使用Vue(工程化)

1、环境准备

在这里插入图片描述

在这里插入图片描述

  1. 选择安装目录

选择安装到一个,没有中文,没有空格的目录下(新建一个文件夹NodeJS)


在这里插入图片描述

  1. 验证NodeJS环境变量

NodeJS 安装完毕后,会自动配置好环境变量,我们验证一下是否安装成功,通过: node -v

在这里插入图片描述

  1. 配置npm的全局安装路径

在这里插入图片描述

使用管理员身份运行命令行,在命令行中,执行如下指令:

npm config set prefix "D:\develop\NodeJS"

注意:D:\develop\NodeJS 这个目录是NodeJS的安装目录

5.更换安装包的源

设置

npm config set registry http://registry.npm.taobao.org/

检查

npm config get registry

2、Vue项目创建和启动

创建一个工程化的Vue项目,执行命令:npm init vue@latest
在这里插入图片描述
在这里插入图片描述
进入项目目录,执行命令安装当前项目的依赖:npm install
在这里插入图片描述
Vue项目-目录结构
在这里插入图片描述
Vue项目-启动
执行命令:npm run dev ,就可以启动vue项目了。
在这里插入图片描述
或者
在这里插入图片描述
访问项目:打开浏览器,在浏览器地址栏访问 http://127.0.0.1:5173 就可以访问到vue项目。
在这里插入图片描述

3、Vue项目开发流程

Vue项目-目录结构
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
.vue是Vue项目中的组件文件,在Vue项目中也称为单文件组件(SFC,Single-File Components)。Vue 的单文件组件会将一个组件的逻辑 (JS),模板 (HTML) 和样式 (CSS) 封装在同一个文件里(.vue) 。

<!-- <script>
  //写数据
  export default{
    data(){
      return {
        msg:'上海'
      }
    }
  }
</script> -->
<script setup>
  import {ref} from 'vue';
  //调用ref函数,定义响应式数据
  const msg = ref('西安');

  //导入 Api.vue文件
  import ApiVue from './Api.vue'
  //导入Article.vue文件
  import ArticleVue from './Article.vue'
</script>

<template>
  <!-- html -->
  <!-- <h1>北京</h1> -->
  <!-- <h1>{{ msg }}</h1>
  <br>
  <ApiVue/> -->

  <ArticleVue/>
</template>

<style scoped>
  /* 样式 */
  h1{
    color: red;
  }
</style>

4、API风格

Vue的组件有两种不同的风格:组合式API 和 选项式API
在这里插入图片描述
setup:是一个标识,告诉Vue需要进行一些处理,让我们可以更简洁的使用组合式API。
ref():接收一个内部值,返回一个响应式的ref对象,此对象只有一个指向内部值的属性 value。
onMounted():在组合式API中的钩子方法,注册一个回调函数,在组件挂载完成后执行。

<script setup>
    import {ref,onMounted} from 'vue'
    //声明响应式数据 ref  响应式对象有一个内部的属性value
    const count = ref(0); //在组合式api中,一般需要把数据定义为响应式数据
    //const count=0;

    //声明函数
    function increment(){
        count.value++;
    }

    //声明钩子函数 onMounted
    onMounted(()=>{
        console.log('vue 已经挂载完毕了...');
    });
</script>

<template>
    <!-- 写html元素 -->
    <button @click="increment">count: {{ count }}</button>
</template>

在这里插入图片描述
选项式API,可以用包含多个选项的对象来描述组件的逻辑,如:data,methods,mounted等。

5、案例

使用表格展示所有文章的数据, 并完成条件搜索功能
在这里插入图片描述
钩子函数mounted中, 获取所有的文章数据
使用v-for指令,把数据渲染到表格上展示
使用v-model指令完成表单数据的双向绑定
使用v-on指令为搜索按钮绑定单击事件
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在请求或响应被 then 或 catch 处理前拦截它们
在这里插入图片描述
Artcle.vue

<script setup>
    import {articleGetAllService,articleSearchService} from '@/api/article.js';
    import {ref} from 'vue';
    
    //定义响应式数据  ref
    const articleList = ref([]);

    //获取所有文章数据
    //同步获取articleGetAllService的返回结果  async await
    const getAllArticle=async function(){
        let data = await articleGetAllService();
        articleList.value=data;
    }

    getAllArticle();
   
    

    //定义响应式数据 searchConditions
    const searchConditions = ref({
        category:'',
        state:''
    })

    //声明search函数
    const search = async function(){
        //文章搜索
        let data = await articleSearchService({...searchConditions.value});
        articleList.value = data;
    }

</script>

<template>
    <!-- html元素 -->
    <div>

        文章分类: <input type="text" v-model="searchConditions.category">

        发布状态: <input type="text" v-model="searchConditions.state">

        <button v-on:click="search">搜索</button>

        <br />
        <br />
        <table border="1 solid" colspa="0" cellspacing="0">
            <tr>
                <th>文章标题</th>
                <th>分类</th>
                <th>发表时间</th>
                <th>状态</th>
                <th>操作</th>
            </tr>
            <tr v-for="(article,index) in articleList">
                <td>{{article.title}}</td>
                <td>{{article.category}}</td>
                <td>{{article.time}}</td>
                <td>{{article.state}}</td>
                <td>
                    <button>编辑</button>
                    <button>删除</button>
                </td>
            </tr>
        </table>
    </div>
</template>

artcle.js

/* //导入axios  npm install axios
import axios from 'axios';
//定义一个变量,记录公共的前缀  ,  baseURL
const baseURL = 'http://localhost:8080';
const instance = axios.create({baseURL}) */

import request from '@/util/request.js'

export function articleGetAllService() {
    return request.get('/article/getAll');

}

export function articleSearchService(conditions) {
    return request.get('/article/search', { params: conditions });
}

request.js

//定制请求的实例

//导入axios  npm install axios
import axios from 'axios';
//定义一个变量,记录公共的前缀  ,  baseURL
const baseURL = 'http://localhost:8080';
const instance = axios.create({baseURL})


//添加响应拦截器
instance.interceptors.response.use(
    result=>{
        return result.data;
    },
    err=>{
        alert('服务异常');
        return Promise.reject(err);//异步的状态转化成失败的状态
    }
)

export default instance;

3、Element Plus

Element:是饿了么团队研发的,基于 Vue 3,面向设计师和开发者的组件库。
组件:组成网页的部件,例如 超链接、按钮、图片、表格、表单、分页条等等。
官网:https://element-plus.org/zh-CN/#/zh-CN

1、快速入门

准备工作:
创建一个工程化的vue项目
参照官方文档,安装Element Plus组件库(在当前工程的目录下):npm install element-plus --save
在这里插入图片描述

main.js中引入Element Plus组件库(参照官方文档)

import { createApp } from 'vue'//导入vue
import ElementPlus from 'element-plus'//导入element-plus
import 'element-plus/dist/index.css'//导入element-plus的样式
import App from './App.vue'//导入app.vue
import locale from 'element-plus/dist/locale/zh-cn.js'

const app = createApp(App)//创建应用实例

app.use(ElementPlus,{locale})//使用element-plus
app.mount('#app')//控制html元素

制作组件:
访问Element官方文档,复制组件代码,调整
Button.vue

<script lang="ts" setup>
import {
    Check,
    Delete,
    Edit,
    Message,
    Search,
    Star,
} from '@element-plus/icons-vue'
</script>

<template>
    <el-row class="mb-4">
        <el-button>Default</el-button>
        <el-button type="primary" disabled="true">编辑</el-button>
        <el-button type="success" loading="true">查看</el-button>
        
    </el-row>

    <el-row class="mb-4">
        
        <el-button type="info" plain>Info</el-button>
        <el-button type="warning" plain>Warning</el-button>
        <el-button type="danger" plain>Danger</el-button>
    </el-row>

</template>

App.vue

<script setup>
  import ButtonVue from './Button.vue'
  import ArticleVue from './Article.vue'
</script>

<template>
  <!-- <ButtonVue/> -->
  <ArticleVue/> 
</template>

2、常用组件

在这里插入图片描述
Article.vue

<script lang="ts" setup>
import { reactive } from 'vue'

const formInline = reactive({
    user: '',
    region: '',
    date: '',
})

const onSubmit = () => {
    console.log('submit!')
}

import { ref } from 'vue'

const currentPage4 = ref(2)
const pageSize4 = ref(5)
const small = ref(false)
const background = ref(false)
const disabled = ref(false)
const total = ref(20)

const handleSizeChange = (val: number) => {
    console.log(`${val} items per page`)
}
const handleCurrentChange = (val: number) => {
    console.log(`current page: ${val}`)
}

import {
    Delete,
    Edit,
} from '@element-plus/icons-vue'

const tableData = [
    {
        title: '标题1',
        category: '时事',
        time: '2000-01-01',
        state: '已发布',
    },
    {
        title: '标题1',
        category: '时事',
        time: '2000-01-01',
        state: '已发布',
    },
    {
        title: '标题1',
        category: '时事',
        time: '2000-01-01',
        state: '已发布',
    },
    {
        title: '标题1',
        category: '时事',
        time: '2000-01-01',
        state: '已发布',
    },
    {
        title: '标题1',
        category: '时事',
        time: '2000-01-01',
        state: '已发布',
    }

]
</script>

<template>
    <el-card class="box-card">

        <div class="card-header">
            <span>文章管理</span>
            <el-button type="primary">发布文章</el-button>
        </div>

        <div style="margin-top: 20px;">
            <hr>
        </div>

        <el-form :inline="true" :model="formInline" class="demo-form-inline">

            <el-form-item label="文章分类:">
                <el-select v-model="formInline.region" placeholder="请选择" clearable>
                    <el-option label="时事" value="时事" />
                    <el-option label="篮球" value="篮球" />
                </el-select>
            </el-form-item>

            <el-form-item label="发布状态:">
                <el-select v-model="formInline.region" placeholder="请选择" clearable>
                    <el-option label="已发布" value="已发布" />
                    <el-option label="草稿" value="草稿" />
                </el-select>
            </el-form-item>

            <el-form-item>
                <el-button type="primary" @click="onSubmit">搜索</el-button>
            </el-form-item>
            <el-form-item>
                <el-button type="default" @click="onSubmit">重置</el-button>
            </el-form-item>
        </el-form>
        <el-table :data="tableData" style="width: 100%">
            <el-table-column prop="title" label="文章标题" />
            <el-table-column prop="category" label="分类" />
            <el-table-column prop="time" label="发表时间" />
            <el-table-column prop="state" label="状态" />
            <el-table-column label="操作" width="180">
                <el-row>
                    <el-button type="primary" :icon="Edit" circle />
                    <el-button type="danger" :icon="Delete" circle />
                </el-row>
            </el-table-column>
        </el-table>


        <el-pagination class="el-p" v-model:current-page="currentPage4" v-model:page-size="pageSize4"
            :page-sizes="[5, 10, 15, 20]" :small="small" :disabled="disabled" :background="background"
            layout="jumper,total, sizes, prev, pager, next" :total="total" @size-change="handleSizeChange"
            @current-change="handleCurrentChange" />
    </el-card>
</template>

<style scoped>
.el-p {
    margin-top: 20px;
    display: flex;
    justify-content: flex-end;
}

.card-header {
    display: flex;
    justify-content: space-between;
}
</style>

4、大事件

需求在这里插入图片描述

1. 环境准备

1.创建Vue工程
npm init vue@latest
2. 安装依赖
Element-Plus
npm install element-plus --save
Axios
npm install axios
Sass
npm install sass -D
3. 目录调整
4. 删除components下面自动生成的内容
新建目录api、utils、views
将资料中的静态资源拷贝到assets目录下
删除App.uve中自动生成的内容

在这里插入图片描述
在这里插入图片描述
main.js

import './assets/main.scss'

import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import router from '@/router'
import App from './App.vue'
import {createPinia} from 'pinia'
import { createPersistedState } from 'pinia-persistedstate-plugin'
import locale from 'element-plus/dist/locale/zh-cn.js'

const app = createApp(App);
const pinia = createPinia();
const persist = createPersistedState();
pinia.use(persist)
app.use(pinia)
app.use(router)
app.use(ElementPlus,{locale});
app.mount('#app')

2. 功能开发

注册登录

在这里插入图片描述
搭建页面
数据绑定:参考接口文档给属性起名
表单校验:
el-form标签上通过rules属性,绑定校验规则
el-form-item标签上通过prop属性,指定校验项

Login.vue

<script setup>
import { User, Lock } from '@element-plus/icons-vue'
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
//控制注册与登录表单的显示, 默认显示注册
const isRegister = ref(false)
//定义数据模型
const registerData = ref({
    username: '',
    password: '',
    rePassword: ''
})

//校验密码的函数
const checkRePassword = (rule, value, callback) => {
    if (value === '') {
        callback(new Error('请再次确认密码'))
    } else if (value !== registerData.value.password) {
        callback(new Error('请确保两次输入的密码一样'))
    } else {
        callback()
    }
}

//定义表单校验规则
const rules = {
    username: [
        { required: true, message: '请输入用户名', trigger: 'blur' },
        { min: 5, max: 16, message: '长度为5~16位非空字符', trigger: 'blur' }
    ],
    password: [
        { required: true, message: '请输入密码', trigger: 'blur' },
        { min: 5, max: 16, message: '长度为5~16位非空字符', trigger: 'blur' }
    ],
    rePassword: [
        { validator: checkRePassword, trigger: 'blur' }
    ]
}

//调用后台接口,完成注册
import { userRegisterService, userLoginService} from '@/api/user.js'
const register = async () => {
    //registerData是一个响应式对象,如果要获取值,需要.value
    let result = await userRegisterService(registerData.value);
    /* if (result.code === 0) {
        //成功了
        alert(result.msg ? result.msg : '注册成功');
    }else{
        //失败了
        alert('注册失败')
    } */
    //alert(result.msg ? result.msg : '注册成功');
    ElMessage.success(result.msg ? result.msg : '注册成功')
}

//绑定数据,复用注册表单的数据模型
//表单数据校验
//登录函数
import {useTokenStore} from '@/stores/token.js'
import {useRouter} from 'vue-router'
const router = useRouter()
const tokenStore = useTokenStore();
const login =async ()=>{
    //调用接口,完成登录
   let result =  await userLoginService(registerData.value);
   /* if(result.code===0){
    alert(result.msg? result.msg : '登录成功')
   }else{
    alert('登录失败')
   } */
   //alert(result.msg? result.msg : '登录成功')
   ElMessage.success(result.msg ? result.msg : '登录成功')
   //把得到的token存储到pinia中
   tokenStore.setToken(result.data)
   //跳转到首页 路由完成跳转
   router.push('/')
}

//定义函数,清空数据模型的数据
const clearRegisterData = ()=>{
    registerData.value={
        username:'',
        password:'',
        rePassword:''
    }
}
</script>

<template>
    <el-row class="login-page">
        <el-col :span="12" class="bg"></el-col>
        <el-col :span="6" :offset="3" class="form">
            <!-- 注册表单 -->
            <el-form ref="form" size="large" autocomplete="off" v-if="isRegister" :model="registerData" :rules="rules">
                <el-form-item>
                    <h1>注册</h1>
                </el-form-item>
                <el-form-item prop="username">
                    <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input>
                </el-form-item>
                <el-form-item prop="password">
                    <el-input :prefix-icon="Lock" type="password" placeholder="请输入密码"
                        v-model="registerData.password"></el-input>
                </el-form-item>
                <el-form-item prop="rePassword">
                    <el-input :prefix-icon="Lock" type="password" placeholder="请输入再次密码"
                        v-model="registerData.rePassword"></el-input>
                </el-form-item>
                <!-- 注册按钮 -->
                <el-form-item>
                    <el-button class="button" type="primary" auto-insert-space @click="register">
                        注册
                    </el-button>
                </el-form-item>
                <el-form-item class="flex">
                    <el-link type="info" :underline="false" @click="isRegister = false;clearRegisterData()">
                        ← 返回
                    </el-link>
                </el-form-item>
            </el-form>
            <!-- 登录表单 -->
            <el-form ref="form" size="large" autocomplete="off" v-else :model="registerData" :rules="rules">
                <el-form-item>
                    <h1>登录</h1>
                </el-form-item>
                <el-form-item prop="username">
                    <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input>
                </el-form-item>
                <el-form-item prop="password">
                    <el-input name="password" :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.password"></el-input>
                </el-form-item>
                <el-form-item class="flex">
                    <div class="flex">
                        <el-checkbox>记住我</el-checkbox>
                        <el-link type="primary" :underline="false">忘记密码?</el-link>
                    </div>
                </el-form-item>
                <!-- 登录按钮 -->
                <el-form-item>
                    <el-button class="button" type="primary" auto-insert-space @click="login">登录</el-button>
                </el-form-item>
                <el-form-item class="flex">
                    <el-link type="info" :underline="false" @click="isRegister = true;clearRegisterData()">
                        注册 →
                    </el-link>
                </el-form-item>
            </el-form>
        </el-col>
    </el-row>
</template>

<style lang="scss" scoped>
/* 样式 */
.login-page {
    height: 100vh;
    background-color: #fff;

    .bg {
        background: url('@/assets/logo2.png') no-repeat 60% center / 240px auto,
            url('@/assets/login_bg.jpg') no-repeat center / cover;
        border-radius: 0 20px 20px 0;
    }

    .form {
        display: flex;
        flex-direction: column;
        justify-content: center;
        user-select: none;

        .title {
            margin: 0 auto;
        }

        .button {
            width: 100%;
        }

        .flex {
            width: 100%;
            display: flex;
            justify-content: space-between;
        }
    }
}
</style>

api/user.js

//导入request.js请求工具
import request from '@/utils/request.js'

//提供调用注册接口的函数
export const userRegisterService = (registerData)=>{
    //借助于UrlSearchParams完成传递
    const params = new URLSearchParams()
    for(let key in registerData){
        params.append(key,registerData[key]);
    }
    return request.post('/user/register',params);
}

//提供调用登录接口的函数
export const userLoginService = (loginData)=>{
    const params = new URLSearchParams();
    for(let key in loginData){
        params.append(key,loginData[key])
    }
    return request.post('/user/login',params)
}


//获取用户详细信息
export const userInfoService = ()=>{
    return request.get('/user/userInfo')
}

//修改个人信息
export const userInfoUpdateService = (userInfoData)=>{
   return request.put('/user/update',userInfoData)
}

//修改头像
export const userAvatarUpdateService = (avatarUrl)=>{
    const params = new URLSearchParams();
    params.append('avatarUrl',avatarUrl)
    return request.patch('/user/updateAvatar',params)
}

跨域
由于浏览器的同源策略限制,向不同源(不同协议、不同域名、不同端口)发送ajax请求会失败
在这里插入图片描述
优化axios响应拦截器
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

request.js

//定制请求的实例

//导入axios  npm install axios
import axios from 'axios';

import { ElMessage } from 'element-plus'
//定义一个变量,记录公共的前缀  ,  baseURL
//const baseURL = 'http://localhost:8080';
const baseURL = '/api';
const instance = axios.create({ baseURL })

import {useTokenStore} from '@/stores/token.js'
//添加请求拦截器
instance.interceptors.request.use(
    (config)=>{
        //请求前的回调
        //添加token
        const tokenStore = useTokenStore();
        //判断有没有token
        if(tokenStore.token){
            config.headers.Authorization = tokenStore.token
        }
        return config;
    },
    (err)=>{
        //请求错误的回调
        Promise.reject(err)
    }
)

/* import {useRouter} from 'vue-router'
const router = useRouter(); */

import router from '@/router'
//添加响应拦截器
instance.interceptors.response.use(
    result => {
        //判断业务状态码
        if(result.data.code===0){
            return result.data;
        }

        //操作失败
        //alert(result.data.msg?result.data.msg:'服务异常')
        ElMessage.error(result.data.msg?result.data.msg:'服务异常')
        //异步操作的状态转换为失败
        return Promise.reject(result.data)
        
    },
    err => {
        //判断响应状态码,如果为401,则证明未登录,提示请登录,并跳转到登录页面
        if(err.response.status===401){
            ElMessage.error('请先登录')
            router.push('/login')
        }else{
            ElMessage.error('服务异常')
        }
       
        return Promise.reject(err);//异步的状态转化成失败的状态
    }
)

export default instance;

vite.config.js

import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'node:path'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
  ,
  server:{
    proxy:{
      '/api':{//获取路径中包含了/api的请求
          target:'http://localhost:8080',//后台服务所在的源
          changeOrigin:true,//修改源
          rewrite:(path)=>path.replace(/^\/api/,'')///api替换为''
      }
    }
  }
})
主页面布局

在这里插入图片描述
Layout.vue

<script setup>
import {
    Management,
    Promotion,
    UserFilled,
    User,
    Crop,
    EditPen,
    SwitchButton,
    CaretBottom
} from '@element-plus/icons-vue'
import avatar from '@/assets/default.png'

import {userInfoService} from '@/api/user.js'
import useUserInfoStore from '@/stores/userInfo.js'
import {useTokenStore} from '@/stores/token.js'
const tokenStore = useTokenStore();
const userInfoStore = useUserInfoStore();
//调用函数,获取用户详细信息
const getUserInfo = async()=>{
    //调用接口
    let result = await userInfoService();
    //数据存储到pinia中
    userInfoStore.setInfo(result.data);
}

getUserInfo();
//条目被点击后,调用的函数
import {useRouter} from 'vue-router'
const router = useRouter();
import {ElMessage,ElMessageBox} from 'element-plus'
const handleCommand = (command)=>{
    //判断指令
    if(command === 'logout'){
        //退出登录
        ElMessageBox.confirm(
        '您确认要退出吗?',
        '温馨提示',
        {
            confirmButtonText: '确认',
            cancelButtonText: '取消',
            type: 'warning',
        }
    )
        .then(async () => {
            //退出登录
            //1.清空pinia中存储的token以及个人信息
            tokenStore.removeToken()
            userInfoStore.removeInfo()

            //2.跳转到登录页面
            router.push('/login')
            ElMessage({
                type: 'success',
                message: '退出登录成功',
            })
            
        })
        .catch(() => {
            ElMessage({
                type: 'info',
                message: '用户取消了退出登录',
            })
        })
    }else{
        //路由
        router.push('/user/'+command)
    }
}
</script>

<template>
    <!-- element-plus中的容器 -->
    <el-container class="layout-container">
        <!-- 左侧菜单 -->
        <el-aside width="200px">
            <div class="el-aside__logo"></div>
            <!-- element-plus的菜单标签 -->
            <el-menu active-text-color="#ffd04b" background-color="#232323"  text-color="#fff"
                router>
                <el-menu-item index="/article/category">
                    <el-icon>
                        <Management />
                    </el-icon>
                    <span>文章分类</span>
                </el-menu-item>
                <el-menu-item index="/article/manage">
                    <el-icon>
                        <Promotion />
                    </el-icon>
                    <span>文章管理</span>
                </el-menu-item>
                <el-sub-menu >
                    <template #title>
                        <el-icon>
                            <UserFilled />
                        </el-icon>
                        <span>个人中心</span>
                    </template>
                    <el-menu-item index="/user/info">
                        <el-icon>
                            <User />
                        </el-icon>
                        <span>基本资料</span>
                    </el-menu-item>
                    <el-menu-item index="/user/avatar">
                        <el-icon>
                            <Crop />
                        </el-icon>
                        <span>更换头像</span>
                    </el-menu-item>
                    <el-menu-item index="/user/resetPassword">
                        <el-icon>
                            <EditPen />
                        </el-icon>
                        <span>重置密码</span>
                    </el-menu-item>
                </el-sub-menu>
            </el-menu>
        </el-aside>
        <!-- 右侧主区域 -->
        <el-container>
            <!-- 头部区域 -->
            <el-header>
                <div>黑马程序员:<strong>{{ userInfoStore.info.nickname }}</strong></div>
                <!-- 下拉菜单 -->
                <!-- command: 条目被点击后会触发,在事件函数上可以声明一个参数,接收条目对应的指令 -->
                <el-dropdown placement="bottom-end" @command="handleCommand">
                    <span class="el-dropdown__box">
                        <el-avatar :src="userInfoStore.info.userPic? userInfoStore.info.userPic:avatar" />
                        <el-icon>
                            <CaretBottom />
                        </el-icon>
                    </span>
                    <template #dropdown>
                        <el-dropdown-menu>
                            <el-dropdown-item command="info" :icon="User">基本资料</el-dropdown-item>
                            <el-dropdown-item command="avatar" :icon="Crop">更换头像</el-dropdown-item>
                            <el-dropdown-item command="resetPassword" :icon="EditPen">重置密码</el-dropdown-item>
                            <el-dropdown-item command="logout" :icon="SwitchButton">退出登录</el-dropdown-item>
                        </el-dropdown-menu>
                    </template>
                </el-dropdown>
            </el-header>
            <!-- 中间区域 -->
            <el-main>
                <!-- <div style="width: 1290px; height: 570px;border: 1px solid red;">
                    内容展示区
                </div> -->
                <router-view></router-view>
            </el-main>
            <!-- 底部区域 -->
            <el-footer>大事件 ©2023 Created by 黑马程序员</el-footer>
        </el-container>
    </el-container>
</template>

<style lang="scss" scoped>
.layout-container {
    height: 100vh;

    .el-aside {
        background-color: #232323;

        &__logo {
            height: 120px;
            background: url('@/assets/logo.png') no-repeat center / 120px auto;
        }

        .el-menu {
            border-right: none;
        }
    }

    .el-header {
        background-color: #fff;
        display: flex;
        align-items: center;
        justify-content: space-between;

        .el-dropdown__box {
            display: flex;
            align-items: center;

            .el-icon {
                color: #999;
                margin-left: 10px;
            }

            &:active,
            &:focus {
                outline: none;
            }
        }
    }

    .el-footer {
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 14px;
        color: #666;
    }
}
</style>
路由

路由,决定从起点到终点的路径的进程
在前端工程中,路由指的是根据不同的访问路径,展示不同组件的内容
Vue Router是Vue.js的官方路由
Vue Router
安装vue-router npm install vue-router@4
在src/router/index.js中创建路由器,并导出
在vue应用实例中使用vue-router
声明router-view标签,展示组件内容
在这里插入图片描述
App.vue
在这里插入图片描述
index.js
在这里插入图片描述

子路由

在这里插入图片描述
复制资料中提供好的五个组件
配置子路由
声明router-view标签
为菜单项 el-menu-item 设置index属性,设置点击后的路由路径
在这里插入图片描述

index.js

import { createRouter, createWebHistory } from 'vue-router'

//导入组件
import LoginVue from '@/views/Login.vue'
import LayoutVue from '@/views/Layout.vue'

import ArticleCategoryVue from '@/views/article/ArticleCategory.vue'
import ArticleManageVue from '@/views/article/ArticleManage.vue'
import UserAvatarVue from '@/views/user/UserAvatar.vue'
import UserInfoVue from '@/views/user/UserInfo.vue'
import UserResetPasswordVue from '@/views/user/UserResetPassword.vue'

//定义路由关系
const routes = [
    { path: '/login', component: LoginVue },
    {
        path: '/', component: LayoutVue,redirect:'/article/manage', children: [
            { path: '/article/category', component: ArticleCategoryVue },
            { path: '/article/manage', component: ArticleManageVue },
            { path: '/user/info', component: UserInfoVue },
            { path: '/user/avatar', component: UserAvatarVue },
            { path: '/user/resetPassword', component: UserResetPasswordVue }
        ]
    }
]

//创建路由器
const router = createRouter({
    history: createWebHistory(),
    routes: routes
})

//导出路由
export default router
文章分类

在这里插入图片描述
api/article.js

import request from '@/utils/request.js'
import { useTokenStore } from '@/stores/token.js'
//文章分类列表查询
export const articleCategoryListService = ()=>{
    //const tokenStore = useTokenStore();
    //在pinia中定义的响应式数据,都不需要.value
    //return request.get('/category',{headers:{'Authorization':tokenStore.token}})
    return request.get('/category')
}

//文章分类添加
export const articleCategoryAddService = (categoryData)=>{
    return request.post('/category',categoryData)
}

//文章分类修改
export const articleCategoryUpdateService = (categoryData)=>{
   return  request.put('/category',categoryData)
}

//文章分类删除
export const articleCategoryDeleteService = (id)=>{
    return request.delete('/category?id='+id)
}

//文章列表查询
export const articleListService = (params)=>{
   return  request.get('/article',{params:params})
}

//文章添加
export const articleAddService = (articleData)=>{
    return request.post('/article',articleData);

}

AticleCategory.vue

<script setup>
import {
    Edit,
    Delete
} from '@element-plus/icons-vue'
import { ref } from 'vue'
const categorys = ref([
    {
        "id": 3,
        "categoryName": "美食",
        "categoryAlias": "my",
        "createTime": "2023-09-02 12:06:59",
        "updateTime": "2023-09-02 12:06:59"
    },
    {
        "id": 4,
        "categoryName": "娱乐",
        "categoryAlias": "yl",
        "createTime": "2023-09-02 12:08:16",
        "updateTime": "2023-09-02 12:08:16"
    },
    {
        "id": 5,
        "categoryName": "军事",
        "categoryAlias": "js",
        "createTime": "2023-09-02 12:08:33",
        "updateTime": "2023-09-02 12:08:33"
    }
])
//声明一个异步的函数
import { articleCategoryListService, articleCategoryAddService, articleCategoryUpdateService,articleCategoryDeleteService } from '@/api/article.js'
const articleCategoryList = async () => {
    let result = await articleCategoryListService();
    categorys.value = result.data;

}
articleCategoryList();
//控制添加分类弹窗
const dialogVisible = ref(false)

//添加分类数据模型
const categoryModel = ref({
    categoryName: '',
    categoryAlias: ''
})
//添加分类表单校验
const rules = {
    categoryName: [
        { required: true, message: '请输入分类名称', trigger: 'blur' },
    ],
    categoryAlias: [
        { required: true, message: '请输入分类别名', trigger: 'blur' },
    ]
}


//调用接口,添加表单
import { ElMessage } from 'element-plus'
const addCategory = async () => {
    //调用接口
    let result = await articleCategoryAddService(categoryModel.value);
    ElMessage.success(result.msg ? result.msg : '添加成功')

    //调用获取所有文章分类的函数
    articleCategoryList();
    dialogVisible.value = false;
}

//定义变量,控制标题的展示
const title = ref('')

//展示编辑弹窗
const showDialog = (row) => {
    dialogVisible.value = true; title.value = '编辑分类'
    //数据拷贝
    categoryModel.value.categoryName = row.categoryName;
    categoryModel.value.categoryAlias = row.categoryAlias;
    //扩展id属性,将来需要传递给后台,完成分类的修改
    categoryModel.value.id = row.id
}

//编辑分类
const updateCategory = async () => {
    //调用接口
    let result = await articleCategoryUpdateService(categoryModel.value);

    ElMessage.success(result.msg ? result.msg : '修改成功')

    //调用获取所有分类的函数
    articleCategoryList();

    //隐藏弹窗
    dialogVisible.value = false;
}

//清空模型的数据
const clearData = () => {
    categoryModel.value.categoryName = '';
    categoryModel.value.categoryAlias = '';
}

//删除分类
import {ElMessageBox} from 'element-plus'
const deleteCategory = (row) => {
    //提示用户  确认框

    ElMessageBox.confirm(
        '你确认要删除该分类信息吗?',
        '温馨提示',
        {
            confirmButtonText: '确认',
            cancelButtonText: '取消',
            type: 'warning',
        }
    )
        .then(async () => {
            //调用接口
            let result = await articleCategoryDeleteService(row.id);
            ElMessage({
                type: 'success',
                message: '删除成功',
            })
            //刷新列表
            articleCategoryList();
        })
        .catch(() => {
            ElMessage({
                type: 'info',
                message: '用户取消了删除',
            })
        })
}
</script>
<template>
    <el-card class="page-container">
        <template #header>
            <div class="header">
                <span>文章分类</span>
                <div class="extra">
                    <el-button type="primary" @click="dialogVisible = true; title = '添加分类'; clearData()">添加分类</el-button>
                </div>
            </div>
        </template>
        <el-table :data="categorys" style="width: 100%">
            <el-table-column label="序号" width="100" type="index"> </el-table-column>
            <el-table-column label="分类名称" prop="categoryName"></el-table-column>
            <el-table-column label="分类别名" prop="categoryAlias"></el-table-column>
            <el-table-column label="操作" width="100">
                <template #default="{ row }">
                    <el-button :icon="Edit" circle plain type="primary" @click="showDialog(row)"></el-button>
                    <el-button :icon="Delete" circle plain type="danger" @click="deleteCategory(row)"></el-button>
                </template>
            </el-table-column>
            <template #empty>
                <el-empty description="没有数据" />
            </template>
        </el-table>

        <!-- 添加分类弹窗 -->
        <el-dialog v-model="dialogVisible" :title="title" width="30%">
            <el-form :model="categoryModel" :rules="rules" label-width="100px" style="padding-right: 30px">
                <el-form-item label="分类名称" prop="categoryName">
                    <el-input v-model="categoryModel.categoryName" minlength="1" maxlength="10"></el-input>
                </el-form-item>
                <el-form-item label="分类别名" prop="categoryAlias">
                    <el-input v-model="categoryModel.categoryAlias" minlength="1" maxlength="15"></el-input>
                </el-form-item>
            </el-form>
            <template #footer>
                <span class="dialog-footer">
                    <el-button @click="dialogVisible = false">取消</el-button>
                    <el-button type="primary" @click="title == '添加分类' ? addCategory() : updateCategory()"> 确认 </el-button>
                </span>
            </template>
        </el-dialog>
    </el-card>
</template>

<style lang="scss" scoped>
.page-container {
    min-height: 100%;
    box-sizing: border-box;

    .header {
        display: flex;
        align-items: center;
        justify-content: space-between;
    }
}
</style>
Pinia状态管理库

Pinia是Vue的专属状态管理库,它允许你跨组件或页面共享状态
在这里插入图片描述
安装pinia npm install pinia
在vue应用实例中使用pinia
在src/stores/token.js中定义store
在组件中使用store
在这里插入图片描述
在这里插入图片描述

token.js
在这里插入图片描述
Axios请求拦截器
在这里插入图片描述
在这里插入图片描述
Pinia持久化插件-persist
Pinia默认是内存存储,当刷新浏览器的时候会丢失数据。
Persist插件可以将pinia中的数据持久化的存储

安装persist npm install pinia-persistedstate-plugin
在pinia中使用persist
定义状态Store时指定持久化配置参数

main.js
在这里插入图片描述
在这里插入图片描述
未登录统一处理
在这里插入图片描述

文章管理

添加文章分类
在这里插入图片描述
添加分类弹窗页面

<!-- 添加分类弹窗 -->
<el-dialog v-model="dialogVisible" title="添加弹层" width="30%">
    <el-form :model="categoryModel" :rules="rules" label-width="100px" style="padding-right: 30px">
        <el-form-item label="分类名称" prop="categoryName">
            <el-input v-model="categoryModel.categoryName" minlength="1" maxlength="10"></el-input>
        </el-form-item>
        <el-form-item label="分类别名" prop="categoryAlias">
            <el-input v-model="categoryModel.categoryAlias" minlength="1" maxlength="15"></el-input>
        </el-form-item>
    </el-form>
    <template #footer>
        <span class="dialog-footer">
            <el-button @click="dialogVisible = false">取消</el-button>
            <el-button type="primary"> 确认 </el-button>
        </span>
    </template>
</el-dialog>

数据模型和校验规则

  //控制添加分类弹窗
    const dialogVisible = ref(false)
    
    //添加分类数据模型
    const categoryModel = ref({
        categoryName: '',
        categoryAlias: ''
    })
    //添加分类表单校验
    const rules = {
        categoryName: [
            { required: true, message: '请输入分类名称', trigger: 'blur' },
        ],
        categoryAlias: [
            { required: true, message: '请输入分类别名', trigger: 'blur' },
        ]
    }

添加分类按钮单击事件

 <el-button type="primary" @click="dialogVisible = true">添加分类</el-button>

接口调用

在article.js中提供添加分类的函数

//添加文章分类
export const articleCategoryAddService = (categoryModel) => {
    return request.post('/category', categoryModel)
}

在页面中调用接口

 //访问后台,添加文章分类
    const addCategory = async ()=>{
        let result = await articleCategoryAddService(categoryModel.value);
        ElMessage.success(result.message? result.message:'添加成功')
        //隐藏弹窗
        dialogVisible.value = false
        //再次访问后台接口,查询所有分类
        getAllCategory()
    }

    <el-button type="primary" @click="addCategory"> 确认 </el-button>

修改文章分类
在这里插入图片描述
修改分类弹窗页面

修改分类弹窗和新增文章分类弹窗长的一样,所以可以服用添加分类的弹窗

弹窗标题显示定义标题

//弹窗标题
const title=ref('')

在弹窗上绑定标题

 <el-dialog v-model="dialogVisible" :title="title" width="30%">

为添加分类按钮绑定事件

<el-button type="primary" @click="title='添加分类';dialogVisible = true">添加分类</el-button>

为修改分类按钮绑定事件

<el-button :icon="Edit" circle plain type="primary" @click="title='修改分类';dialogVisible=true"></el-button>

数据回显

当点击修改分类按钮时,需要把当前这一条数据的详细信息显示到修改分类的弹窗上,这个叫回显

通过插槽的方式得到被点击按钮所在行的数据

<template #default="{ row }">
                    <el-button :icon="Edit" circle plain type="primary" @click="updateCategoryEcho(row)"></el-button>
                    <el-button :icon="Delete" circle plain type="danger"></el-button>
                </template>

回显函数

 //修改分类回显
    const updateCategoryEcho = (row) => {
        title.value = '修改分类'
        dialogVisible.value = true
        //将row中的数据赋值给categoryModel
        categoryModel.value.categoryName=row.categoryName
        categoryModel.value.categoryAlias=row.categoryAlias
        //修改的时候必须传递分类的id,所以扩展一个id属性
        categoryModel.value.id=row.id
    }

接口调用

article.js中提供修改分类的函数

   //修改分类
    export const articleCategoryUpdateService = (categoryModel)=>{
        return request.put('/category',categoryModel)
    }

修改确定按钮的绑定事件

   <span class="dialog-footer">
                    <el-button @click="dialogVisible = false">取消</el-button>
                    <el-button type="primary" @click="title==='添加分类'? addCategory():updateCategory()"> 确认 </el-button>
                </span>

调用接口完成修改的函数

//修改分类
const updateCategory=async ()=>{
    let result = await articleCategoryUpdateService(categoryModel.value)
    ElMessage.success(result.message? result.message:'修改成功')
    //隐藏弹窗
    dialogVisible.value=false
    //再次访问后台接口,查询所有分类
    getAllCategory()
}

由于现在修改和新增共用了一个数据模型,所以在点击添加分类后,有时候会显示数据,此时可以将categoryModel中的数据清空

  //清空模型数据
    const clearCategoryModel = ()=>{
        categoryModel.value.categoryName='',
        categoryModel.value.categoryAlias=''
    }

修改添加按钮的点击事件

<el-button type="primary" @click="title = '添加分类'; dialogVisible = true;clearCategoryModel()">添加分类</el-button>

删除文章分类
在这里插入图片描述
确认框

//删除分类  给删除按钮绑定事件
const deleteCategory = (row) => {
    ElMessageBox.confirm(
        '你确认删除该分类信息吗?',
        '温馨提示',
        {
            confirmButtonText: '确认',
            cancelButtonText: '取消',
            type: 'warning',
        }
    )
        .then(() => {
            //用户点击了确认
            ElMessage({
                type: 'success',
                message: '删除成功',
            })
        })
        .catch(() => {
            //用户点击了取消
            ElMessage({
                type: 'info',
                message: '取消删除',
            })
        })
}

接口调用

article.js中提供删除分类的函数

   //删除分类
    export const articleCategoryDeleteService = (id) => {
        return request.delete('/category?id='+id)
    }

当用户点击确认后,调用接口删除分类

//删除分类
const deleteCategory = (row) => {
    ElMessageBox.confirm(
        '你确认删除该分类信息吗?',
        '温馨提示',
        {
            confirmButtonText: '确认',
            cancelButtonText: '取消',
            type: 'warning',
        }
    )
        .then(async () => {
            //用户点击了确认
            let result = await articleCategoryDeleteService(row.id)
            ElMessage.success(result.message?result.message:'删除成功')
            //再次调用getAllCategory,获取所有文章分类
            getAllCategory()
        })
        .catch(() => {
            //用户点击了取消
            ElMessage({
                type: 'info',
                message: '取消删除',
            })
        })
}

文章列表查询
在这里插入图片描述
文章列表页面组件

  <script setup>
    import {
        Edit,
        Delete
    } from '@element-plus/icons-vue'
    
    import { ref } from 'vue'
    
    //文章分类数据模型
    const categorys = ref([
        {
            "id": 3,
            "categoryName": "美食",
            "categoryAlias": "my",
            "createTime": "2023-09-02 12:06:59",
            "updateTime": "2023-09-02 12:06:59"
        },
        {
            "id": 4,
            "categoryName": "娱乐",
            "categoryAlias": "yl",
            "createTime": "2023-09-02 12:08:16",
            "updateTime": "2023-09-02 12:08:16"
        },
        {
            "id": 5,
            "categoryName": "军事",
            "categoryAlias": "js",
            "createTime": "2023-09-02 12:08:33",
            "updateTime": "2023-09-02 12:08:33"
        }
    ])
    
    //用户搜索时选中的分类id
    const categoryId=ref('')
    
    //用户搜索时选中的发布状态
    const state=ref('')
    
    //文章列表数据模型
    const articles = ref([
        {
            "id": 5,
            "title": "陕西旅游攻略",
            "content": "兵马俑,华清池,法门寺,华山...爱去哪去哪...",
            "coverImg": "https://big-event-gwd.oss-cn-beijing.aliyuncs.com/9bf1cf5b-1420-4c1b-91ad-e0f4631cbed4.png",
            "state": "草稿",
            "categoryId": 2,
            "createTime": "2023-09-03 11:55:30",
            "updateTime": "2023-09-03 11:55:30"
        },
        {
            "id": 5,
            "title": "陕西旅游攻略",
            "content": "兵马俑,华清池,法门寺,华山...爱去哪去哪...",
            "coverImg": "https://big-event-gwd.oss-cn-beijing.aliyuncs.com/9bf1cf5b-1420-4c1b-91ad-e0f4631cbed4.png",
            "state": "草稿",
            "categoryId": 2,
            "createTime": "2023-09-03 11:55:30",
            "updateTime": "2023-09-03 11:55:30"
        },
        {
            "id": 5,
            "title": "陕西旅游攻略",
            "content": "兵马俑,华清池,法门寺,华山...爱去哪去哪...",
            "coverImg": "https://big-event-gwd.oss-cn-beijing.aliyuncs.com/9bf1cf5b-1420-4c1b-91ad-e0f4631cbed4.png",
            "state": "草稿",
            "categoryId": 2,
            "createTime": "2023-09-03 11:55:30",
            "updateTime": "2023-09-03 11:55:30"
        },
    ])
    
    //分页条数据模型
    const pageNum = ref(1)//当前页
    const total = ref(20)//总条数
    const pageSize = ref(3)//每页条数
    
    //当每页条数发生了变化,调用此函数
    const onSizeChange = (size) => {
        pageSize.value = size
    }
    //当前页码发生变化,调用此函数
    const onCurrentChange = (num) => {
        pageNum.value = num
    }
    </script>
    <template>
        <el-card class="page-container">
            <template #header>
                <div class="header">
                    <span>文章管理</span>
                    <div class="extra">
                        <el-button type="primary">添加文章</el-button>
                    </div>
                </div>
            </template>
            <!-- 搜索表单 -->
            <el-form inline>
                <el-form-item label="文章分类:">
                    <el-select placeholder="请选择" v-model="categoryId">
                        <el-option 
                            v-for="c in categorys" 
                            :key="c.id" 
                            :label="c.categoryName"
                            :value="c.id">
                        </el-option>
                    </el-select>
                </el-form-item>
    
                <el-form-item label="发布状态:">
                    <el-select placeholder="请选择" v-model="state">
                        <el-option label="已发布" value="已发布"></el-option>
                        <el-option label="草稿" value="草稿"></el-option>
                    </el-select>
                </el-form-item>
                <el-form-item>
                    <el-button type="primary">搜索</el-button>
                    <el-button>重置</el-button>
                </el-form-item>
            </el-form>
            <!-- 文章列表 -->
            <el-table :data="articles" style="width: 100%">
                <el-table-column label="文章标题" width="400" prop="title"></el-table-column>
                <el-table-column label="分类" prop="categoryId"></el-table-column>
                <el-table-column label="发表时间" prop="createTime"> </el-table-column>
                <el-table-column label="状态" prop="state"></el-table-column>
                <el-table-column label="操作" width="100">
                    <template #default="{ row }">
                        <el-button :icon="Edit" circle plain type="primary"></el-button>
                        <el-button :icon="Delete" circle plain type="danger"></el-button>
                    </template>
                </el-table-column>
                <template #empty>
                    <el-empty description="没有数据" />
                </template>
            </el-table>
            <!-- 分页条 -->
            <el-pagination v-model:current-page="pageNum" v-model:page-size="pageSize" :page-sizes="[3, 5 ,10, 15]"
                layout="jumper, total, sizes, prev, pager, next" background :total="total" @size-change="onSizeChange"
                @current-change="onCurrentChange" style="margin-top: 20px; justify-content: flex-end" />
        </el-card>
    </template>
    <style lang="scss" scoped>
    .page-container {
        min-height: 100%;
        box-sizing: border-box;
    
        .header {
            display: flex;
            align-items: center;
            justify-content: space-between;
        }
    }
    </style>

使用中文语言包,解决分页条中文问题, 在main.js中完成

 import locale from 'element-plus/dist/locale/zh-cn.js'
    
    app.use(ElementPlus,{locale})

文章分类数据回显

ArticleMange.vue

//文章列表查询
import { articleCategoryListService } from '@/api/article.js'
const getArticleCategoryList = async () => {
    //获取所有分类
    let resultC = await articleCategoryListService();
    categorys.value = resultC.data
}
getArticleCategoryList();

文章列表接口调用

article.js中提供获取文章列表数据的函数

//文章列表查询
export const articleListService = (params) => {
    return request.get('/article', { params: params })
}

ArticleManage.vue中,调用接口获取数据

 //文章列表查询
    import { articleListService } from '@/api/article.js'
    const getArticles = async () => {
        let params = {
            pageNum: pageNum.value,
            pageSize: pageSize.value,
            categoryId: categoryId.value ? categoryId.value : null,
            state: state.value ? state.value : null
        }
        let result = await articleListService(params);
        //渲染列表数据
        articles.value = result.data.items
        //为列表中添加categoryName属性
        for(let i=0;i<articles.value.length;i++){
            let article = articles.value[i];
            for(let j=0;j<categorys.value.length;j++){
                if(article.categoryId===categorys.value[j].id){
                    article.categoryName=categorys.value[j].categoryName
                }
            }
        }
        //渲染总条数
        total.value=result.data.total
    }
    getArticles()

当分页条的当前页和每页条数发生变化,重新发送请求获取数据

//当每页条数发生了变化,调用此函数
const onSizeChange = (size) => {
    pageSize.value = size
    getArticles()
}
//当前页码发生变化,调用此函数
const onCurrentChange = (num) => {
    pageNum.value = num
    getArticles()
}

搜索和重置

为搜索按钮绑定单击事件,调用getArticles函数即可

<el-button type="primary" @click="getArticles">搜索</el-button>

为重置按钮绑定单击事件,清除categoryId和state的之即可

 <el-button @click="categoryId='';state=''">重置</el-button>

添加文章
在这里插入图片描述
添加文章抽屉组件

import {Plus} from '@element-plus/icons-vue'
//控制抽屉是否显示
const visibleDrawer = ref(false)
//添加表单数据模型
const articleModel = ref({
    title: '',
    categoryId: '',
    coverImg: '',
    content:'',
    state:''
})

<!-- 抽屉 -->
        <el-drawer v-model="visibleDrawer" title="添加文章" direction="rtl" size="50%">
            <!-- 添加文章表单 -->
            <el-form :model="articleModel" label-width="100px" >
                <el-form-item label="文章标题" >
                    <el-input v-model="articleModel.title" placeholder="请输入标题"></el-input>
                </el-form-item>
                <el-form-item label="文章分类">
                    <el-select placeholder="请选择" v-model="articleModel.categoryId">
                        <el-option v-for="c in categorys" :key="c.id" :label="c.categoryName" :value="c.id">
                        </el-option>
                    </el-select>
                </el-form-item>
                <el-form-item label="文章封面">

                    <el-upload class="avatar-uploader" :auto-upload="false" :show-file-list="false">
                        <img v-if="articleModel.coverImg" :src="articleModel.coverImg" class="avatar" />
                        <el-icon v-else class="avatar-uploader-icon">
                            <Plus />
                        </el-icon>
                    </el-upload>
                </el-form-item>
                <el-form-item label="文章内容">
                    <div class="editor">富文本编辑器</div>
                </el-form-item>
                <el-form-item>
                    <el-button type="primary">发布</el-button>
                    <el-button type="info">草稿</el-button>
                </el-form-item>
            </el-form>
        </el-drawer>

/* 抽屉样式 */
.avatar-uploader {
    :deep() {
        .avatar {
            width: 178px;
            height: 178px;
            display: block;
        }

        .el-upload {
            border: 1px dashed var(--el-border-color);
            border-radius: 6px;
            cursor: pointer;
            position: relative;
            overflow: hidden;
            transition: var(--el-transition-duration-fast);
        }

        .el-upload:hover {
            border-color: var(--el-color-primary);
        }

        .el-icon.avatar-uploader-icon {
            font-size: 28px;
            color: #8c939d;
            width: 178px;
            height: 178px;
            text-align: center;
        }
    }
}
.editor {
  width: 100%;
  :deep(.ql-editor) {
    min-height: 200px;
  }
}

为添加文章按钮添加单击事件,展示抽屉

<el-button type="primary" @click="visibleDrawer = true">添加文章</el-button>

富文本编辑器

文章内容需要使用到富文本编辑器,这里咱们使用一个开源的富文本编辑器 Quill

官网地址: https://vueup.github.io/vue-quill/

安装:

npm install @vueup/vue-quill@latest --save

导入组件和样式:

  import { QuillEditor } from '@vueup/vue-quill'
    import '@vueup/vue-quill/dist/vue-quill.snow.css'

页面长使用quill组件:

<quill-editor
              theme="snow"
              v-model:content="articleModel.content"
              contentType="html"
              >
</quill-editor>

样式美化:

 .editor {
      width: 100%;
      :deep(.ql-editor) {
        min-height: 200px;
      }
    }

文章封面图片上传

将来当点击+图标,选择本地图片后,el-upload这个组件会自动发送请求,把图片上传到指定的服务器上,而不需要我们自己使用axios发送异步请求,所以需要给el-upload标签添加一些属性,控制请求的发送

auto-upload:是否自动上传

action: 服务器接口路径

name: 上传的文件字段名

headers: 设置上传的请求头

on-success: 上传成功的回调函数

  import {
        Plus
    } from '@element-plus/icons-vue'
    
    <el-form-item label="文章封面">
        <el-upload class="avatar-uploader" 
                   :show-file-list="false" 
                   >
            <img v-if="articleModel.coverImg" :src="articleModel.coverImg" class="avatar" />
            <el-icon v-else class="avatar-uploader-icon">
                <Plus />
            </el-icon>
        </el-upload>
    </el-form-item>

注意:

  1. 由于这个请求时el-upload自动发送的异步请求,并没有使用咱们的request.js请求工具,所以在请求的路ing上,需要加上/api, 这个时候请求代理才能拦截到这个请求,转发到后台服务器上

  2. 要携带请求头,还需要导入pinia状态才可以使用

  import { useTokenStore } from '@/stores/token.js'
  const tokenStore = useTokenStore();
  1. 在成功的回调函数中,可以拿到服务器响应的数据,其中有一个属性为data,对应的就是图片在阿里云oss上存储的访问地址,需要把它赋值给articleModel的coverImg属性,这样img标签就能显示这张图片了,因为img标签上通过src属性绑定了articleModel.coverImg
   //上传图片成功回调
      const uploadSuccess = (img) => {
          //img就是后台响应的数据,格式为:{code:状态码,message:提示信息,data: 图片的存储地址}
          articleModel.value.coverImg=img.data
      }
  

添加文章接口调用

article.js中提供添加文章函数

//添加文章
export const articleAddService = (articleModel)=>{
    return request.post('/article',articleModel)
}

为已发布和草稿按钮绑定事件

<el-form-item>
    <el-button type="primary" @click="addArticle('已发布')">发布</el-button>
    <el-button type="info" @click="addArticle('草稿')">草稿</el-button>
</el-form-item>

ArticleManage.vue中提供addArticle函数完成添加文章接口的调用

 //添加文章
    const addArticle=async (state)=>{
        articleModel.value.state = state
        let result = await articleAddService(articleModel.value);
        ElMessage.success(result.message? result.message:'添加成功')
        //再次调用getArticles,获取文章
        getArticles()
        //隐藏抽屉
        visibleDrawer.value=false
    }

顶部导航栏信息显示
在这里插入图片描述
在Layout.vue中,页面加载完就发送请求,获取个人信息展示,并存储到pinia中,因为将来在个人中心中修改信息的时候还需要使用

user.js中提供获取个人信息的函数

//获取个人信息
export const userInfoGetService = ()=>{
    return request.get('/user/userInfo');
}

src/stores/user.js中,定义个人中心状态

 import { defineStore } from "pinia"
    import {ref} from 'vue'
    
    export const useUserInfoStore = defineStore('userInfo',()=>{
        //1.定义用户信息
        const info = ref({})
        //2.定义修改用户信息的方法
        const setInfo = (newInfo)=>{
            info.value = newInfo
        }
        //3.定义清空用户信息的方法
        const removeInfo = ()=>{
            info.value={}
        }
    
        return{info,setInfo,removeInfo}
    },{
        persist:true
    })

Layout.vue中获取个人信息,并存储到pinia中

   //导入接口函数
    import {userInfoGetService} from '@/api/user.js'
    //导入pinia
    import {useUserInfoStore} from '@/stores/user.js'
    const userInfoStore = useUserInfoStore();
    import {ref} from 'vue'
    
    //获取个人信息
    const getUserInf = async ()=>{
        let result = await userInfoGetService();
        //存储pinia
        userInfoStore.info =result.data;
    }
    getUserInf()

Layout.vue的顶部导航栏中,展示昵称和头像

<div>黑马程序员:<strong>{{ userInfoStore.info.nickname ? userInfoStore.info.nickname : userInfoStore.info.usrename }}</strong></div>



<el-avatar :src="userInfoStore.info.userPic ? userInfoStore.info.userPic : avatar" />

下拉菜单功能
在这里插入图片描述
el-dropdown中功能实现

在el-dropdown中有四个子条目,分别是:

  • 基本资料
  • 更换头像
  • 重置密码
  • 退出登录

其中其三个起到路由功能,跟左侧菜单中【个人中心】下面的二级菜单是同样的功能,退出登录需要删除本地pinia中存储的token以及userInfo

路由实现:

在el-dropdown-item标签上添加command属性,属性值和路由表中/user/xxx保持一致

<el-dropdown-menu>
    <el-dropdown-item command="info" :icon="User">基本资料</el-dropdown-item>
    <el-dropdown-item command="avatar" :icon="Crop">更换头像</el-dropdown-item>
    <el-dropdown-item command="password" :icon="EditPen">重置密码</el-dropdown-item>
    <el-dropdown-item command="logout" :icon="SwitchButton">退出登录</el-dropdown-item>
</el-dropdown-menu>

在el-dropdown标签上绑定command事件,当有条目被点击后,会触发这个事件

<el-dropdown placement="bottom-end" @command="handleCommand">

提供handleCommand函数,参数为点击条目的command属性值

 //dropDown条目被点击后,回调的函数
    import {useRouter} from 'vue-router'
    const router = useRouter()
    const handleCommand = (command)=>{
        if(command==='logout'){
            //退出登录
            alert('退出登录')
        }else{
            //路由
            router.push('/user/'+command)
        }
    }

退出登录实现:

  import {ElMessage,ElMessageBox} from 'element-plus'
    import { useTokenStore } from '@/stores/token.js'
    const tokenStore = useTokenStore()
    const handleCommand = (command) => {
        if (command === 'logout') {
            //退出登录
            ElMessageBox.confirm(
                '你确认退出登录码?',
                '温馨提示',
                {
                    confirmButtonText: '确认',
                    cancelButtonText: '取消',
                    type: 'warning',
                }
            )
                .then(async () => {
                    //用户点击了确认
                    //清空pinia中的token和个人信息
                    userInfoStore.info={}
                    tokenStore.token=''
                    //跳转到登录页
                    router.push('/login')
                })
                .catch(() => {
                    //用户点击了取消
                    ElMessage({
                        type: 'info',
                        message: '取消退出',
                    })
                })
        } else {
            //路由
            router.push('/user/' + command)
        }
    }
个人中心

基本资料修改
在这里插入图片描述
基本资料页面组件

 <script setup>
    import { ref } from 'vue'
    const userInfo = ref({
        id: 0,
        username: 'zhangsan',
        nickname: 'zs',
        email: 'zs@163.com',
    })
    const rules = {
        nickname: [
            { required: true, message: '请输入用户昵称', trigger: 'blur' },
            {
                pattern: /^\S{2,10}$/,
                message: '昵称必须是2-10位的非空字符串',
                trigger: 'blur'
            }
        ],
        email: [
            { required: true, message: '请输入用户邮箱', trigger: 'blur' },
            { type: 'email', message: '邮箱格式不正确', trigger: 'blur' }
        ]
    }
    </script>
    <template>
        <el-card class="page-container">
            <template #header>
                <div class="header">
                    <span>基本资料</span>
                </div>
            </template>
            <el-row>
                <el-col :span="12">
                    <el-form :model="userInfo" :rules="rules" label-width="100px" size="large">
                        <el-form-item label="登录名称">
                            <el-input v-model="userInfo.username" disabled></el-input>
                        </el-form-item>
                        <el-form-item label="用户昵称" prop="nickname">
                            <el-input v-model="userInfo.nickname"></el-input>
                        </el-form-item>
                        <el-form-item label="用户邮箱" prop="email">
                            <el-input v-model="userInfo.email"></el-input>
                        </el-form-item>
                        <el-form-item>
                            <el-button type="primary">提交修改</el-button>
                        </el-form-item>
                    </el-form>
                </el-col>
            </el-row>
        </el-card>
    </template>

表单数据回显

个人信息之前已经存储到了pinia中,只需要从pinia中获取个人信息,替换模板数据即可

   import { useUserInfoStore } from '@/stores/user.js';
    const userInfoStore = useUserInfoStore()
    const userInfo = ref({...userInfoStore.info})

接口调用

在src/api/user.js中提供修改基本资料的函数

//修改个人信息
export const userInfoUpdateService = (userInfo)=>{
    return request.put('/user/update',userInfo)
}

为修改按钮绑定单击事件

  <el-button type="primary" @click="updateUserInfo">提交修改</el-button>

提供updateUserInfo函数

  //修改用户信息
    import {userInfoUpdateService} from '@/api/user.js'
    import { ElMessage } from 'element-plus';
    const updateUserInfo = async ()=>{
        let result = await userInfoUpdateService(userInfo.value)
        ElMessage.success(result.message? result.message:'修改成功')
        //更新pinia中的数据
        userInfoStore.info.nickname=userInfo.value.nickname
        userInfoStore.info.email = userInfo.value.email
    }

用户头像修改
在这里插入图片描述
修改头像页面组件

<script setup>
import { Plus, Upload } from '@element-plus/icons-vue'
import {ref} from 'vue'
import avatar from '@/assets/default.png'
const uploadRef = ref()

//用户头像地址
const imgUrl= avatar

</script>

<template>
    <el-card class="page-container">
        <template #header>
            <div class="header">
                <span>更换头像</span>
            </div>
        </template>
        <el-row>
            <el-col :span="12">
                <el-upload 
                    ref="uploadRef"
                    class="avatar-uploader" 
                    :show-file-list="false"
                    >
                    <img v-if="imgUrl" :src="imgUrl" class="avatar" />
                    <img v-else src="avatar" width="278" />
                </el-upload>
                <br />
                <el-button type="primary" :icon="Plus" size="large"  @click="uploadRef.$el.querySelector('input').click()">
                    选择图片
                </el-button>
                <el-button type="success" :icon="Upload" size="large">
                    上传头像
                </el-button>
            </el-col>
        </el-row>
    </el-card>
</template>

<style lang="scss" scoped>
.avatar-uploader {
    :deep() {
        .avatar {
            width: 278px;
            height: 278px;
            display: block;
        }

        .el-upload {
            border: 1px dashed var(--el-border-color);
            border-radius: 6px;
            cursor: pointer;
            position: relative;
            overflow: hidden;
            transition: var(--el-transition-duration-fast);
        }

        .el-upload:hover {
            border-color: var(--el-color-primary);
        }

        .el-icon.avatar-uploader-icon {
            font-size: 28px;
            color: #8c939d;
            width: 278px;
            height: 278px;
            text-align: center;
        }
    }
}
</style>

头像回显

从pinia中读取用户的头像数据

 //读取用户信息
    import {ref} from 'vue'
    import {useUserInfoStore} from '@/stores/user.js'
    const userInfoStore = useUserInfoStore()
    const imgUrl=ref(userInfoStore.info.userPic)

img标签上绑定图片地址

<img v-if="imgUrl" :src="imgUrl" class="avatar" />
<img v-else src="@/assets/avatar.jpg" width="278" />

头像上传

为el-upload指定属性值,分别有:

  • ​ action: 服务器接口路径
  • ​ headers: 设置请求头,需要携带token
  • ​ on-success: 上传成功的回调函数
  • ​ name: 上传图片的字段名称
<el-upload 
           class="avatar-uploader" 
           :show-file-list="false"
           :auto-upload="true"
           action="/api/upload"
           name="file"
           :headers="{'Authorization':tokenStore.token}"
           :on-success="uploadSuccess"
           >
    <img v-if="imgUrl" :src="imgUrl" class="avatar" />
    <img v-else src="@/assets/avatar.jpg" width="278" />
</el-upload>

提供上传成功的回调函数

  //读取token信息
    import {useTokenStore} from '@/stores/token.js'
    const tokenStore = useTokenStore()
    
    //图片上传成功的回调
    const uploadSuccess = (result)=>{
        //回显图片
        imgUrl.value = result.data
    }

外部触发图片选择

​ 需要获取到el-upload组件,然后再通过$el.querySelector(‘input’)获取到el-upload对应的元素,触发click事件

   //获取el-upload元素
    const uploadRef = ref()
    
    
    <el-button type="primary" :icon="Plus" size="large"  @click="uploadRef.$el.querySelector('input').click()">
        选择图片
    </el-button>

接口调用

在user.js中提供修改头像的函数

//修改头像
export const userAvatarUpdateService=(avatarUrl)=>{
    let params = new URLSearchParams();
    params.append('avatarUrl',avatarUrl)
    return request.patch('/user/updateAvatar',params)
}

为【上传头像】按钮绑定单击事件

<el-button type="success" :icon="Upload" size="large" @click="updateAvatar">
    上传头像
</el-button>

提供updateAvatar函数,完成头像更新

//调用接口,更新头像url
import {userAvatarUpdateService} from '@/api/user.js'
import {ElMessage} from 'element-plus'
const updateAvatar = async ()=>{
    let result = await userAvatarUpdateService(imgUrl.value)
    ElMessage.success(result.message? result.message:'修改成功')
    //更新pinia中的数据
    userInfoStore.info.userPic=imgUrl.value
}
  
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值