本文介绍: /控制注册登录表单的显示, 默认显示注册//定义数据模型})– 注册表单 –>注册

目录

一、环境准备

1、创建Vue工程

2、安装依赖 

2.1 安装项目所需要的vue依赖 

2.2 安装element-plus依赖


前言:本项目的终结篇,是一名vue3完成本次课程前端页面

npm init vue@latest

# 项目名称spring3vue3project

进入创建项目目录

# 项目需要vue依赖
npm install

# 安装element-plus依赖
npm install element-plus --save

打开项目 

code .

修改src/main.js 

import './assets/main.scss'

import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'


import App from './App.vue'

const app = createApp(App)
app.use(ElementPlus)

app.mount('#app')

npm install axios

# 安装sass依赖 好像是关于cssnpm install sass -D

直接进到项目文件夹操作

// 定制请求实例

//导入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;

如需,关注,发私信

删除app.vue自动生成内容

<script setup&gt;

</script&gt;

<template>
春天的菠菜

</template>


<style scoped>

</style>
npm run dev

 

<script setup>
import { User, Lock } from '@element-plus/icons-vue'
import { ref } from 'vue'
//控制注册与登录表单的显示, 默认显示注册
const isRegister = ref(false)
</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">
                <el-form-item>
                    <h1>注册</h1>
                </el-form-item>
                <el-form-item>
                    <el-input :prefix-icon="User" placeholder="请输入用户名"></el-input>
                </el-form-item>
                <el-form-item>
                    <el-input :prefix-icon="Lock" type="password" placeholder="请输入密码"></el-input>
                </el-form-item>
                <el-form-item>
                    <el-input :prefix-icon="Lock" type="password" placeholder="请输入再次密码"></el-input>
                </el-form-item>
                <!-- 注册按钮 -->
                <el-form-item>
                    <el-button class="button" type="primary" auto-insert-space>
                        注册
                    </el-button>
                </el-form-item>
                <el-form-item class="flex">
                    <el-link type="info" :underline="false" @click="isRegister = false">
                        ← 返回
                    </el-link>
                </el-form-item>
            </el-form>
            <!-- 登录表单 -->
            <el-form ref="form" size="large" autocomplete="off" v-else>
                <el-form-item>
                    <h1>登录</h1>
                </el-form-item>
                <el-form-item>
                    <el-input :prefix-icon="User" placeholder="请输入用户名"></el-input>
                </el-form-item>
                <el-form-item>
                    <el-input name="password" :prefix-icon="Lock" type="password" placeholder="请输入密码"></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>登录</el-button>
                </el-form-item>
                <el-form-item class="flex">
                    <el-link type="info" :underline="false" @click="isRegister = true">
                        注册 →
                    </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>
<script setup>
import LoginVue from '@/views/Login.vue'

</script>

<template>
<LoginVue/>
</template>


<style scoped>

</style>

<script setup>
import { User, Lock } from '@element-plus/icons-vue'
import { ref } from 'vue'
//控制注册与登录表单的显示, 默认显示注册
const isRegister = ref(false)

//定义数据模型

const registerData = ref({
    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">
                <el-form-item>
                    <h1>注册</h1>
                </el-form-item>
                <el-form-item>
                    <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input>
                </el-form-item>
                <el-form-item>
                    <el-input :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.password"></el-input>
                </el-form-item>
                <el-form-item>
                    <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>
                        注册
                    </el-button>
                </el-form-item>
                <el-form-item class="flex">
                    <el-link type="info" :underline="false" @click="isRegister = false">
                        ← 返回
                    </el-link>
                </el-form-item>
            </el-form>
            <!-- 登录表单 -->
            <el-form ref="form" size="large" autocomplete="off" v-else>
                <el-form-item>
                    <h1>登录</h1>
                </el-form-item>
                <el-form-item>
                    <el-input :prefix-icon="User" placeholder="请输入用户名"></el-input>
                </el-form-item>
                <el-form-item>
                    <el-input name="password" :prefix-icon="Lock" type="password" placeholder="请输入密码"></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>登录</el-button>
                </el-form-item>
                <el-form-item class="flex">
                    <el-link type="info" :underline="false" @click="isRegister = true">
                        注册 →
                    </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>

表单校验可看官网

<script setup>
import { User, Lock } from '@element-plus/icons-vue'
import { ref } from 'vue'
//控制注册与登录表单的显示, 默认显示注册
const isRegister = ref(false)

//2.1定义数据模型

const registerData = ref({
    username:'',
    password:'',
    rePassword:''
})

//2.2自定义rePassword需要自定义校验规则校验规则的函数,三个参rule 规则,value 值,callback 回调函数
const checkRePassword = (rule,value,callback)=>{
    if(value === '' ){
        callback(new Error('请再次确认密码!'))
    }else if(value !== registerData.value.password){
        callback(new Error('请确保两次密码输入一致性!'))
    }else{
        callback()  //校验通过
    }
}

//2.2 定义表单校验规则,rePassword需要自定义校验规则
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'}
    ]
}



</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">  <!-- 2.1  :model="registerData"表单声明属性 --> <!-- 2.2  :rules="rules"绑定校验 -->
                <el-form-item>
                    <h1>注册</h1>
                </el-form-item>
                <el-form-item prop="username">  <!-- 2.2 prop="username"绑定校验 -->
                    <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input>  <!-- 2.1 v-model="registerData.username"绑定属性 -->
                </el-form-item>
                <el-form-item prop="password"> <!-- 2.2 prop="password"绑定校验 -->
                    <el-input :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.password"></el-input>  <!-- 2.1 v-model="registerData.password"绑定属性 -->
                </el-form-item>
                <el-form-item prop="rePassword">  <!-- 2.2 prop="rePassword"绑定校验 -->
                    <el-input :prefix-icon="Lock" type="password" placeholder="请输入再次密码" v-model="registerData.rePassword"></el-input>  <!-- 2.1 v-model="registerData.rePassword"绑定属性 -->
                </el-form-item>
                <!-- 注册按钮 -->
                <el-form-item>
                    <el-button class="button" type="primary" auto-insert-space>
                        注册
                    </el-button>
                </el-form-item>
                <el-form-item class="flex">
                    <el-link type="info" :underline="false" @click="isRegister = false">
                        ← 返回
                    </el-link>
                </el-form-item>
            </el-form>
            <!-- 登录表单 -->
            <el-form ref="form" size="large" autocomplete="off" v-else>
                <el-form-item>
                    <h1>登录</h1>
                </el-form-item>
                <el-form-item>
                    <el-input :prefix-icon="User" placeholder="请输入用户名"></el-input>
                </el-form-item>
                <el-form-item>
                    <el-input name="password" :prefix-icon="Lock" type="password" placeholder="请输入密码"></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>登录</el-button>
                </el-form-item>
                <el-form-item class="flex">
                    <el-link type="info" :underline="false" @click="isRegister = true">
                        注册 →
                    </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>

启动前面章节后端服务以及redis 

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

// 提供调用注册接口的函数
export const userRegisterService = (registerData) =>{
    //借助于URLSearchParam完成参数传递
    const params = new URLSearchParams();
    for(let key in registerData){
        params.append(key,registerData[key]);
    }
    return request.post('/user/register',params);
    
}
<script setup>
import { User, Lock } from '@element-plus/icons-vue'
import { ref } from 'vue'
// 3.3 导入注册的接口
import { userRegisterService} from '@/api/user.js'

//控制注册与登录表单的显示, 默认显示注册
const isRegister = ref(false)

//2.1定义数据模型

const registerData = ref({
    username:'',
    password:'',
    rePassword:''
})

//2.2自定义rePassword需要自定义校验规则校验规则的函数,三个参rule 规则,value 值,callback 回调函数
const checkRePassword = (rule,value,callback)=>{
    if(value === '' ){
        callback(new Error('请再次确认密码!'))
    }else if(value !== registerData.value.password){
        callback(new Error('请确保两次密码输入一致性!'))
    }else{
        callback()  //校验通过
    }
}

//2.2 定义表单校验规则,rePassword需要自定义校验规则
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'}
    ]
}

//3.3 调用后台接口,完成注册
const  register = async()=>{
    // registerData 是一个响应对象,如果要获取需要.value
    let result = await userRegisterService(registerData.value);
    if(result.code ===1){
        //注册成功
        alert(result.msg?result.msg : '注册成功')

    }else{
        // 注册失败
        alert('注册失败')

    }
}

</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">  <!-- 2.1  :model="registerData"表单声明属性 --> <!-- 2.2  :rules="rules"绑定校验 -->
                <el-form-item>
                    <h1>注册</h1>
                </el-form-item>
                <el-form-item prop="username">  <!-- 2.2 prop="username"绑定校验 -->
                    <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input>  <!-- 2.1 v-model="registerData.username"绑定属性 -->
                </el-form-item>
                <el-form-item prop="password"> <!-- 2.2 prop="password"绑定校验 -->
                    <el-input :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.password"></el-input>  <!-- 2.1 v-model="registerData.password"绑定属性 -->
                </el-form-item>
                <el-form-item prop="rePassword">  <!-- 2.2 prop="rePassword"绑定校验 -->
                    <el-input :prefix-icon="Lock" type="password" placeholder="请输入再次密码" v-model="registerData.rePassword"></el-input>  <!-- 2.1 v-model="registerData.rePassword"绑定属性 -->
                </el-form-item>
                <!-- 注册按钮 -->
                <el-form-item>
                    <el-button class="button" type="primary" auto-insert-space @click="register"> <!-- 3.3  @click="register"  绑定注册单击事件 -->
                        注册
                    </el-button>
                </el-form-item>
                <el-form-item class="flex">
                    <el-link type="info" :underline="false" @click="isRegister = false">
                        ← 返回
                    </el-link>
                </el-form-item>
            </el-form>
            <!-- 登录表单 -->
            <el-form ref="form" size="large" autocomplete="off" v-else>
                <el-form-item>
                    <h1>登录</h1>
                </el-form-item>
                <el-form-item>
                    <el-input :prefix-icon="User" placeholder="请输入用户名"></el-input>
                </el-form-item>
                <el-form-item>
                    <el-input name="password" :prefix-icon="Lock" type="password" placeholder="请输入密码"></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>登录</el-button>
                </el-form-item>
                <el-form-item class="flex">
                    <el-link type="info" :underline="false" @click="isRegister = true">
                        注册 →
                    </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>

验证,存在跨域问题

// 定制请求实例

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

//定义一个变量记录公共的前缀baseURL
// const baseURL = 'http://localhost:8080'  
const baseURL = '/api'          // 注释上面代码,解决跨域问题
const instance = axios.create({baseURL})

// 添加响应拦截器
instance.interceptors.response.use(
    result=>{
        return result.data;

    },
    err=>{
        alert('服务异常')
        return Promise.reject(err);//异步状态转化成失败的状态
    }
)

export default instance;
import { fileURLToPath, URL } from 'node:url'

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

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

 

<script setup>
import { User, Lock } from '@element-plus/icons-vue'
import { ref } from 'vue'
//二 注册 3.3 导入注册的接口
import { userRegisterService} from '@/api/user.js'

//控制注册与登录表单的显示, 默认显示注册
const isRegister = ref(false)

//二 注册 2.1定义数据模型

const registerData = ref({
    username:'',
    password:'',
    rePassword:''
})

//二 注册  2.2自定义rePassword需要自定义校验规则校验规则的函数,三个参rule 规则,value 值,callback 回调函数
const checkRePassword = (rule,value,callback)=>{
    if(value === '' ){
        callback(new Error('请再次确认密码!'))
    }else if(value !== registerData.value.password){
        callback(new Error('请确保两次密码输入一致性!'))
    }else{
        callback()  //校验通过
    }
}

//二 注册  2.2 定义表单校验规则,rePassword需要自定义校验规则
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'}
    ]
}

// 二 注册  3.3 调用后台接口,完成注册
const  register = async()=>{
    // registerData 是一个响应式对象,如果要获取值需要.value
    let result = await userRegisterService(registerData.value);
    if(result.code ===1){
        //注册成功
        alert(result.msg?result.msg : '注册成功')

    }else{
        // 注册失败
        alert('注册失败')

    }
}

// 三 登录 1 绑定数据 复用注册表单的数据模型

</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">  <!-- 二 注册2.1  :model="registerData"表单声明属性 --> <!-- 二 注册2.2  :rules="rules"绑定校验 -->
                <el-form-item>
                    <h1>注册</h1>
                </el-form-item>
                <el-form-item prop="username">  <!--二 注册 2.2 prop="username"绑定校验 -->
                    <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input>  <!--二 注册 2.1 v-model="registerData.username"绑定属性 -->
                </el-form-item>
                <el-form-item prop="password"> <!-- 二 注册2.2 prop="password"绑定校验 -->
                    <el-input :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.password"></el-input>  <!-- 二 注册2.1 v-model="registerData.password"绑定属性 -->
                </el-form-item>
                <el-form-item prop="rePassword">  <!-- 2.2 prop="rePassword"绑定校验 -->
                    <el-input :prefix-icon="Lock" type="password" placeholder="请输入再次密码" v-model="registerData.rePassword"></el-input>  <!-- 二 注册2.1 v-model="registerData.rePassword"绑定属性 -->
                </el-form-item>
                <!-- 注册按钮 -->
                <el-form-item>
                    <el-button class="button" type="primary" auto-insert-space @click="register"> <!-- 二 注册3.3  @click="register"  绑定注册单击事件 -->
                        注册
                    </el-button>
                </el-form-item>
                <el-form-item class="flex">
                    <el-link type="info" :underline="false" @click="isRegister = false">
                        ← 返回
                    </el-link>
                </el-form-item>
            </el-form>
            <!-- 登录表单 -->
            <el-form ref="form" size="large" autocomplete="off" v-else model="registerDate">  <!-- 三 登录 3.1  :model="registerData"表单声明属性,复用注册 -->
                <el-form-item>
                    <h1>登录</h1>
                </el-form-item>
                <el-form-item>
                    <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input> <!--三 登录 3.1 v-model="registerData.username"绑定属性 -->
                </el-form-item>
                <el-form-item>
                    <el-input name="password" :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.password"></el-input> <!--三 登录 3.1 v-model="registerData.password"绑定属性 -->
                </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>登录</el-button>
                </el-form-item>
                <el-form-item class="flex">
                    <el-link type="info" :underline="false" @click="isRegister = true">
                        注册 →
                    </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>
<script setup>
import { User, Lock } from '@element-plus/icons-vue'
import { ref } from 'vue'
//二 注册 3.3 导入注册的接口
import { userRegisterService} from '@/api/user.js'

//控制注册与登录表单的显示, 默认显示注册
const isRegister = ref(false)

//二 注册 2.1定义数据模型

const registerData = ref({
    username:'',
    password:'',
    rePassword:''
})

//二 注册  2.2自定义rePassword需要自定义校验规则校验规则的函数,三个参rule 规则,value 值,callback 回调函数
const checkRePassword = (rule,value,callback)=>{
    if(value === '' ){
        callback(new Error('请再次确认密码!'))
    }else if(value !== registerData.value.password){
        callback(new Error('请确保两次密码输入一致性!'))
    }else{
        callback()  //校验通过
    }
}

//二 注册  2.2 定义表单校验规则,rePassword需要自定义校验规则
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'}
    ]
}

// 二 注册  3.3 调用后台接口,完成注册
const  register = async()=>{
    // registerData 是一个响应式对象,如果要获取值需要.value
    let result = await userRegisterService(registerData.value);
    if(result.code ===1){
        //注册成功
        alert(result.msg?result.msg : '注册成功')

    }else{
        // 注册失败
        alert('注册失败')

    }
}

// 三 登录 1 绑定数据 复用注册表单的数据模型
// 三 登录 2  数据校验 复用注册表单的数据校验

</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">  <!-- 二 注册2.1  :model="registerData"表单声明属性 --> <!-- 二 注册2.2  :rules="rules"绑定校验 -->
                <el-form-item>
                    <h1>注册</h1>
                </el-form-item>
                <el-form-item prop="username">  <!--二 注册 2.2 prop="username"绑定校验 -->
                    <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input>  <!--二 注册 2.1 v-model="registerData.username"绑定属性 -->
                </el-form-item>
                <el-form-item prop="password"> <!-- 二 注册2.2 prop="password"绑定校验 -->
                    <el-input :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.password"></el-input>  <!-- 二 注册2.1 v-model="registerData.password"绑定属性 -->
                </el-form-item>
                <el-form-item prop="rePassword">  <!-- 2.2 prop="rePassword"绑定校验 -->
                    <el-input :prefix-icon="Lock" type="password" placeholder="请输入再次密码" v-model="registerData.rePassword"></el-input>  <!-- 二 注册2.1 v-model="registerData.rePassword"绑定属性 -->
                </el-form-item>
                <!-- 注册按钮 -->
                <el-form-item>
                    <el-button class="button" type="primary" auto-insert-space @click="register"> <!-- 二 注册3.3  @click="register"  绑定注册单击事件 -->
                        注册
                    </el-button>
                </el-form-item>
                <el-form-item class="flex">
                    <el-link type="info" :underline="false" @click="isRegister = false">
                        ← 返回
                    </el-link>
                </el-form-item>
            </el-form>
            <!-- 登录表单 -->
            <el-form ref="form" size="large" autocomplete="off" v-else model="registerDate" rules="rules">  <!-- 三 登录 3.1  :model="registerData"表单声明属性,复用注册 --> <!-- 三 登录 3.2  :rules="rules"绑定校验 -->
                <el-form-item>
                    <h1>登录</h1>
                </el-form-item>
                <el-form-item prop="username"> <!--三 登录 3.2  prop="username"绑定校验 -->
                    <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input> <!--三 登录 3.1 v-model="registerData.username"绑定属性 -->
                </el-form-item>
                <el-form-item prop="username"> <!--三 登录 3.2  prop="username"绑定校验 -->
                    <el-input name="password" :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.password"></el-input> <!--三 登录 3.1 v-model="registerData.password"绑定属性 -->
                </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>登录</el-button>
                </el-form-item>
                <el-form-item class="flex">
                    <el-link type="info" :underline="false" @click="isRegister = true">
                        注册 →
                    </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>

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

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

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

}

<script setup>
import { User, Lock } from '@element-plus/icons-vue'
import { ref } from 'vue'
//二 注册 3.3 导入注册的接口 //三 登录 3.3 导入登录的接口
import { userRegisterService,userLoginService} from '@/api/user.js'



//控制注册与登录表单的显示, 默认显示注册
const isRegister = ref(false)

//二 注册 2.1定义数据模型

const registerData = ref({
    username:'',
    password:'',
    rePassword:''
})

//二 注册  2.2自定义rePassword需要自定义校验规则校验规则的函数,三个参rule 规则,value 值,callback 回调函数
const checkRePassword = (rule,value,callback)=>{
    if(value === '' ){
        callback(new Error('请再次确认密码!'))
    }else if(value !== registerData.value.password){
        callback(new Error('请确保两次密码输入一致性!'))
    }else{
        callback()  //校验通过
    }
}

//二 注册  2.2 定义表单校验规则,rePassword需要自定义校验规则
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'}
    ]
}

// 二 注册  3.3 调用后台接口,完成注册
const  register = async()=>{
    // registerData 是一个响应式对象,如果要获取值需要.value
    let result = await userRegisterService(registerData.value);
    if(result.code ===1){
        //注册成功
        alert(result.msg?result.msg : '注册成功')

    }else{
        // 注册失败
        alert('注册失败')

    }
}

// 三 登录 1 绑定数据 复用注册表单的数据模型
// 三 登录 2  数据校验 复用注册表单的数据校验
// 三 登录 3  登录函数
const login  = async()=>{ 
    //调用接口完成登录
    let result = await userLoginService(registerData.value);
    if(result.code ===1){
        //登录成功
        alert(result.msg?result.msg : '登录成功')

    }else{
        // 登录失败
        alert('登录失败')

    }

}


</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">  <!-- 二 注册2.1  :model="registerData"表单声明属性 --> <!-- 二 注册2.2  :rules="rules"绑定校验 -->
                <el-form-item>
                    <h1>注册</h1>
                </el-form-item>
                <el-form-item prop="username">  <!--二 注册 2.2 prop="username"绑定校验 -->
                    <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input>  <!--二 注册 2.1 v-model="registerData.username"绑定属性 -->
                </el-form-item>
                <el-form-item prop="password"> <!-- 二 注册2.2 prop="password"绑定校验 -->
                    <el-input :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.password"></el-input>  <!-- 二 注册2.1 v-model="registerData.password"绑定属性 -->
                </el-form-item>
                <el-form-item prop="rePassword">  <!-- 2.2 prop="rePassword"绑定校验 -->
                    <el-input :prefix-icon="Lock" type="password" placeholder="请输入再次密码" v-model="registerData.rePassword"></el-input>  <!-- 二 注册2.1 v-model="registerData.rePassword"绑定属性 -->
                </el-form-item>
                <!-- 注册按钮 -->
                <el-form-item>
                    <el-button class="button" type="primary" auto-insert-space @click="register"> <!-- 二 注册3.3  @click="register"  绑定注册单击事件 -->
                        注册
                    </el-button>
                </el-form-item>
                <el-form-item class="flex">
                    <el-link type="info" :underline="false" @click="isRegister = false">
                        ← 返回
                    </el-link>
                </el-form-item>
            </el-form>
            <!-- 登录表单 -->
            <el-form ref="form" size="large" autocomplete="off" v-else model="registerDate" rules="rules">  <!-- 三 登录 3.1  :model="registerData"表单声明属性,复用注册 --> <!-- 三 登录 3.2  :rules="rules"绑定校验 -->
                <el-form-item>
                    <h1>登录</h1>
                </el-form-item>
                <el-form-item prop="username"> <!--三 登录 3.2  prop="username"绑定校验 -->
                    <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input> <!--三 登录 3.1 v-model="registerData.username"绑定属性 -->
                </el-form-item>
                <el-form-item prop="username"> <!--三 登录 3.2  prop="username"绑定校验 -->
                    <el-input name="password" :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.password"></el-input> <!--三 登录 3.1 v-model="registerData.password"绑定属性 -->
                </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">
                        注册 →
                    </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>

 

<script setup>
import { User, Lock } from '@element-plus/icons-vue'
import { ref } from 'vue'
//二 注册 3.3 导入注册的接口 //三 登录 3.3 导入登录的接口
import { userRegisterService,userLoginService} from '@/api/user.js'



//控制注册与登录表单的显示, 默认显示注册
const isRegister = ref(false)

//二 注册 2.1定义数据模型

const registerData = ref({
    username:'',
    password:'',
    rePassword:''
})

//二 注册  2.2自定义rePassword需要自定义校验规则校验规则的函数,三个参rule 规则,value 值,callback 回调函数
const checkRePassword = (rule,value,callback)=>{
    if(value === '' ){
        callback(new Error('请再次确认密码!'))
    }else if(value !== registerData.value.password){
        callback(new Error('请确保两次密码输入一致性!'))
    }else{
        callback()  //校验通过
    }
}

//二 注册  2.2 定义表单校验规则,rePassword需要自定义校验规则
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'}
    ]
}

// 二 注册  3.3 调用后台接口,完成注册
const  register = async()=>{
    // registerData 是一个响应式对象,如果要获取值需要.value
    let result = await userRegisterService(registerData.value);
    if(result.code ===1){
        //注册成功
        alert(result.msg?result.msg : '注册成功')

    }else{
        // 注册失败
        alert('注册失败')

    }
}

// 三 登录 1 绑定数据 复用注册表单的数据模型
// 三 登录 2  数据校验 复用注册表单的数据校验
// 三 登录 3  登录函数
const login  = async()=>{ 
    //调用接口完成登录
    let result = await userLoginService(registerData.value);
    if(result.code ===1){
        //登录成功
        alert(result.msg?result.msg : '登录成功')

    }else{
        // 登录失败
        alert('登录失败')

    }

}
// 三 登录 4 定义函数清空数据模型数据(将数据清空)
const clearRegisterDate = ()=>{
    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">  <!-- 二 注册2.1  :model="registerData"表单声明属性 --> <!-- 二 注册2.2  :rules="rules"绑定校验 -->
                <el-form-item>
                    <h1>注册</h1>
                </el-form-item>
                <el-form-item prop="username">  <!--二 注册 2.2 prop="username"绑定校验 -->
                    <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input>  <!--二 注册 2.1 v-model="registerData.username"绑定属性 -->
                </el-form-item>
                <el-form-item prop="password"> <!-- 二 注册2.2 prop="password"绑定校验 -->
                    <el-input :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.password"></el-input>  <!-- 二 注册2.1 v-model="registerData.password"绑定属性 -->
                </el-form-item>
                <el-form-item prop="rePassword">  <!-- 2.2 prop="rePassword"绑定校验 -->
                    <el-input :prefix-icon="Lock" type="password" placeholder="请输入再次密码" v-model="registerData.rePassword"></el-input>  <!-- 二 注册2.1 v-model="registerData.rePassword"绑定属性 -->
                </el-form-item>
                <!-- 注册按钮 -->
                <el-form-item>
                    <el-button class="button" type="primary" auto-insert-space @click="register"> <!-- 二 注册3.3  @click="register"  绑定注册单击事件 -->
                        注册
                    </el-button>
                </el-form-item>
                <el-form-item class="flex">
                    <el-link type="info" :underline="false" @click="isRegister = false;clearRegisterDate()"> <!--三 登录 4  调用函数clearRegisterDate() 清空数据 -->
                        ← 返回
                    </el-link>
                </el-form-item>
            </el-form>
            <!-- 登录表单 -->
            <el-form ref="form" size="large" autocomplete="off" v-else model="registerDate" rules="rules">  <!-- 三 登录 3.1  :model="registerData"表单声明属性,复用注册 --> <!-- 三 登录 3.2  :rules="rules"绑定校验 -->
                <el-form-item>
                    <h1>登录</h1>
                </el-form-item>
                <el-form-item prop="username"> <!--三 登录 3.2  prop="username"绑定校验 -->
                    <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input> <!--三 登录 3.1 v-model="registerData.username"绑定属性 -->
                </el-form-item>
                <el-form-item prop="username"> <!--三 登录 3.2  prop="username"绑定校验 -->
                    <el-input name="password" :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.password"></el-input> <!--三 登录 3.1 v-model="registerData.password"绑定属性 -->
                </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;clearRegisterDate()">  <!--三 登录 4  调用函数clearRegisterDate() 清空数据 -->
                        注册 →
                    </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>

(上图右侧的文件错误

// 定制请求实例

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

//定义一个变量记录公共的前缀baseURL
// const baseURL = 'http://localhost:8080'  
const baseURL = '/api'          // 注释上面代码,解决跨域问题
const instance = axios.create({baseURL})

// 添加响应拦截器
instance.interceptors.response.use(
    result=>{
        // 判断业务状态码
        if(result.data.code === 1){
            //成功,正常返回数据
            return result.data;
        }
        // 操作失败
        alert(result.data.msg?result.data.msg : '服务异常')
        // 异步操作的状态转换为失败
        return Promise.reject(result.data)

    },
    err=>{
        alert('服务异常')
        return Promise.reject(err);//异步的状态转化成失败的状态
    }
)

export default instance;
<script setup>
import { User, Lock } from '@element-plus/icons-vue'
import { ref } from 'vue'
//二 注册 3.3 导入注册的接口 //三 登录 3.3 导入登录的接口
import { userRegisterService,userLoginService} from '@/api/user.js'



//控制注册与登录表单的显示, 默认显示注册
const isRegister = ref(false)

//二 注册 2.1定义数据模型

const registerData = ref({
    username:'',
    password:'',
    rePassword:''
})

//二 注册  2.2自定义rePassword需要自定义校验规则校验规则的函数,三个参rule 规则,value 值,callback 回调函数
const checkRePassword = (rule,value,callback)=>{
    if(value === '' ){
        callback(new Error('请再次确认密码!'))
    }else if(value !== registerData.value.password){
        callback(new Error('请确保两次密码输入一致性!'))
    }else{
        callback()  //校验通过
    }
}

//二 注册  2.2 定义表单校验规则,rePassword需要自定义校验规则
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'}
    ]
}

// 二 注册  3.3 调用后台接口,完成注册
const  register = async()=>{
    // registerData 是一个响应式对象,如果要获取值需要.value
    let result = await userRegisterService(registerData.value);
    /* 使用axios的 request.js 统一处理了,优化这里代码
    if(result.code ===1){
        //注册成功
        alert(result.msg?result.msg : '注册成功')

    }else{
        // 注册失败
        alert('注册失败')

    } */
    alert(result.msg?result.msg : '注册成功')
}

// 三 登录 1 绑定数据 复用注册表单的数据模型
// 三 登录 2  数据校验 复用注册表单的数据校验
// 三 登录 3  登录函数
const login  = async()=>{ 
    //调用接口完成登录
    let result = await userLoginService(registerData.value);
      /* 使用axios的 request.js 统一处理了,优化这里代码
    if(result.code ===1){
        //登录成功
        alert(result.msg?result.msg : '登录成功')

    }else{
        // 登录失败
        alert('登录失败')

    }*/
    alert(result.msg?result.msg : '登录成功')

}
// 三 登录 4 定义函数清空数据模型数据(将数据清空)
const clearRegisterDate = ()=>{
    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">  <!-- 二 注册2.1  :model="registerData"表单声明属性 --> <!-- 二 注册2.2  :rules="rules"绑定校验 -->
                <el-form-item>
                    <h1>注册</h1>
                </el-form-item>
                <el-form-item prop="username">  <!--二 注册 2.2 prop="username"绑定校验 -->
                    <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input>  <!--二 注册 2.1 v-model="registerData.username"绑定属性 -->
                </el-form-item>
                <el-form-item prop="password"> <!-- 二 注册2.2 prop="password"绑定校验 -->
                    <el-input :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.password"></el-input>  <!-- 二 注册2.1 v-model="registerData.password"绑定属性 -->
                </el-form-item>
                <el-form-item prop="rePassword">  <!-- 2.2 prop="rePassword"绑定校验 -->
                    <el-input :prefix-icon="Lock" type="password" placeholder="请输入再次密码" v-model="registerData.rePassword"></el-input>  <!-- 二 注册2.1 v-model="registerData.rePassword"绑定属性 -->
                </el-form-item>
                <!-- 注册按钮 -->
                <el-form-item>
                    <el-button class="button" type="primary" auto-insert-space @click="register"> <!-- 二 注册3.3  @click="register"  绑定注册单击事件 -->
                        注册
                    </el-button>
                </el-form-item>
                <el-form-item class="flex">
                    <el-link type="info" :underline="false" @click="isRegister = false;clearRegisterDate()"> <!--三 登录 4  调用函数clearRegisterDate() 清空数据 -->
                        ← 返回
                    </el-link>
                </el-form-item>
            </el-form>
            <!-- 登录表单 -->
            <el-form ref="form" size="large" autocomplete="off" v-else model="registerDate" rules="rules">  <!-- 三 登录 3.1  :model="registerData"表单声明属性,复用注册 --> <!-- 三 登录 3.2  :rules="rules"绑定校验 -->
                <el-form-item>
                    <h1>登录</h1>
                </el-form-item>
                <el-form-item prop="username"> <!--三 登录 3.2  prop="username"绑定校验 -->
                    <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input> <!--三 登录 3.1 v-model="registerData.username"绑定属性 -->
                </el-form-item>
                <el-form-item prop="username"> <!--三 登录 3.2  prop="username"绑定校验 -->
                    <el-input name="password" :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.password"></el-input> <!--三 登录 3.1 v-model="registerData.password"绑定属性 -->
                </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;clearRegisterDate()">  <!--三 登录 4  调用函数clearRegisterDate() 清空数据 -->
                        注册 →
                    </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>
// 定制请求实例

//导入axios  npm install axios
import axios from 'axios';
// 四 4.2 element-plus封装组件
import { ElMessage } from 'element-plus';

//定义一个变量记录公共的前缀baseURL
// const baseURL = 'http://localhost:8080'  
const baseURL = '/api'          // 注释上面代码,解决跨域问题
const instance = axios.create({baseURL})

// 添加响应拦截器
instance.interceptors.response.use(
    result=>{
        // 四  4.1 判断业务状态码
        if(result.data.code === 1){
            //成功,正常返回数据
            return result.data;
        }
        // 操作失败
        // alert(result.data.msg?result.data.msg : '服务异常')  
        // 四 4.2 优化alert
        ElMessage.error(result.data.msg?result.data.msg : '服务异常')
        // 异步操作的状态转换为失败
        return Promise.reject(result.data)

    },
    err=>{
        alert('服务异常')
        return Promise.reject(err);//异步的状态转化成失败的状态
    }
)

export default instance;
<script setup>
import { User, Lock } from '@element-plus/icons-vue'
import { ref } from 'vue'
//二 注册 3.3 导入注册的接口 //三 登录 3.3 导入登录的接口
import { userRegisterService,userLoginService} from '@/api/user.js'

// 四 4.2 element-plus封装组件
import { ElMessage } from 'element-plus';

//控制注册与登录表单的显示, 默认显示注册
const isRegister = ref(false)

//二 注册 2.1定义数据模型

const registerData = ref({
    username:'',
    password:'',
    rePassword:''
})

//二 注册  2.2自定义rePassword需要自定义校验规则校验规则的函数,三个参rule 规则,value 值,callback 回调函数
const checkRePassword = (rule,value,callback)=>{
    if(value === '' ){
        callback(new Error('请再次确认密码!'))
    }else if(value !== registerData.value.password){
        callback(new Error('请确保两次密码输入一致性!'))
    }else{
        callback()  //校验通过
    }
}

//二 注册  2.2 定义表单校验规则,rePassword需要自定义校验规则
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'}
    ]
}

// 二 注册  3.3 调用后台接口,完成注册
const  register = async()=>{
    // registerData 是一个响应式对象,如果要获取值需要.value
    let result = await userRegisterService(registerData.value);
    /* 使用axios的 request.js 统一处理了,优化这里代码
    if(result.code ===1){
        //注册成功
        alert(result.msg?result.msg : '注册成功')

    }else{
        // 注册失败
        alert('注册失败')

    } */
    // 四 4.2 优化alert
    // alert(result.msg?result.msg : '注册成功')
    ElMessage.success(result.msg?result.msg : '注册成功')
}

// 三 登录 1 绑定数据 复用注册表单的数据模型
// 三 登录 2  数据校验 复用注册表单的数据校验
// 三 登录 3  登录函数
const login  = async()=>{ 
    //调用接口完成登录
    let result = await userLoginService(registerData.value);
      /* 使用axios的 request.js 统一处理了,优化这里代码
    if(result.code ===1){
        //登录成功
        alert(result.msg?result.msg : '登录成功')

    }else{
        // 登录失败
        alert('登录失败')

    }*/
    // 四 4.2 优化alert
    // alert(result.msg?result.msg : '登录成功')
    ElMessage.success(result.msg?result.msg : '登录成功')

}
// 三 登录 4 定义函数清空数据模型数据(将数据清空)
const clearRegisterDate = ()=>{
    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">  <!-- 二 注册2.1  :model="registerData"表单声明属性 --> <!-- 二 注册2.2  :rules="rules"绑定校验 -->
                <el-form-item>
                    <h1>注册</h1>
                </el-form-item>
                <el-form-item prop="username">  <!--二 注册 2.2 prop="username"绑定校验 -->
                    <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input>  <!--二 注册 2.1 v-model="registerData.username"绑定属性 -->
                </el-form-item>
                <el-form-item prop="password"> <!-- 二 注册2.2 prop="password"绑定校验 -->
                    <el-input :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.password"></el-input>  <!-- 二 注册2.1 v-model="registerData.password"绑定属性 -->
                </el-form-item>
                <el-form-item prop="rePassword">  <!-- 2.2 prop="rePassword"绑定校验 -->
                    <el-input :prefix-icon="Lock" type="password" placeholder="请输入再次密码" v-model="registerData.rePassword"></el-input>  <!-- 二 注册2.1 v-model="registerData.rePassword"绑定属性 -->
                </el-form-item>
                <!-- 注册按钮 -->
                <el-form-item>
                    <el-button class="button" type="primary" auto-insert-space @click="register"> <!-- 二 注册3.3  @click="register"  绑定注册单击事件 -->
                        注册
                    </el-button>
                </el-form-item>
                <el-form-item class="flex">
                    <el-link type="info" :underline="false" @click="isRegister = false;clearRegisterDate()"> <!--三 登录 4  调用函数clearRegisterDate() 清空数据 -->
                        ← 返回
                    </el-link>
                </el-form-item>
            </el-form>
            <!-- 登录表单 -->
            <el-form ref="form" size="large" autocomplete="off" v-else model="registerDate" rules="rules">  <!-- 三 登录 3.1  :model="registerData"表单声明属性,复用注册 --> <!-- 三 登录 3.2  :rules="rules"绑定校验 -->
                <el-form-item>
                    <h1>登录</h1>
                </el-form-item>
                <el-form-item prop="username"> <!--三 登录 3.2  prop="username"绑定校验 -->
                    <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input> <!--三 登录 3.1 v-model="registerData.username"绑定属性 -->
                </el-form-item>
                <el-form-item prop="username"> <!--三 登录 3.2  prop="username"绑定校验 -->
                    <el-input name="password" :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.password"></el-input> <!--三 登录 3.1 v-model="registerData.password"绑定属性 -->
                </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;clearRegisterDate()">  <!--三 登录 4  调用函数clearRegisterDate() 清空数据 -->
                        注册 →
                    </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>

<script setup>
import {
    Management,
    Promotion,
    UserFilled,
    User,
    Crop,
    EditPen,
    SwitchButton,
    CaretBottom
} from '@element-plus/icons-vue'
import avatar from '@/assets/default.png'
</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 >
                    <el-icon>
                        <Management />
                    </el-icon>
                    <span>文章分类</span>
                </el-menu-item>
                <el-menu-item >
                    <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 >
                        <el-icon>
                            <User />
                        </el-icon>
                        <span>基本资料</span>
                    </el-menu-item>
                    <el-menu-item >
                        <el-icon>
                            <Crop />
                        </el-icon>
                        <span>更换头像</span>
                    </el-menu-item>
                    <el-menu-item >
                        <el-icon>
                            <EditPen />
                        </el-icon>
                        <span>重置密码</span>
                    </el-menu-item>
                </el-sub-menu>
            </el-menu>
        </el-aside>
        <!-- 右侧主区域 -->
        <el-container>
            <!-- 头部区域 -->
            <el-header>
                <div>编码集中营:<strong>春天的菠菜</strong></div>
                <el-dropdown placement="bottom-end">
                    <span class="el-dropdown__box">
                        <el-avatar :src="avatar" />
                        <el-icon>
                            <CaretBottom />
                        </el-icon>
                    </span>
                    <template #dropdown>
                        <el-dropdown-menu>
                            <el-dropdown-item command="profile" :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>
                    </template>
                </el-dropdown>
            </el-header>
            <!-- 中间区域 -->
            <el-main>
                <div style="width: 1290px; height: 570px;border: 1px solid red;">
                    内容展示区
                </div>
            </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>
<script setup>
import LoginVue from '@/views/Login.vue'
import LayoutVue from '@/views/Layout.vue';

</script>

<template>
<LoginVue/>
<!-- 五 LayoutVue-->
<LayoutVue/>
</template>


<style scoped>

</style>

 

npm install vue-router@4

src下新增router文件夹,在router下新增index.js

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

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



//定义路由关系
const routes = [
    { path: '/login', component: LoginVue },
    { path: '/', component: LayoutVue }  
]

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

//导出路由
export default router
import './assets/main.scss'  // 本项目使用sass

import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
//五 2.3 
import router from '@/router'


import App from './App.vue'


const app = createApp(App)
//五 2.3 
app.use(router)


app.use(ElementPlus)
app.mount('#app')
<script setup>
// 五  2.4
// import LoginVue from '@/views/Login.vue'
// import LayoutVue from '@/views/Layout.vue';

</script>

<template>
    <!-- 五  2.4 -->
    <router-view></router-view>
    <!-- 五  2.4 -->
<!-- <LoginVue/> -->
<!-- 五 1.1 LayoutVue-->
<LayoutVue/>
</template>


<style scoped>

</style>
<script setup>
import { User, Lock } from '@element-plus/icons-vue'
import { ref } from 'vue'
//二 注册 3.3 导入注册的接口 //三 登录 3.3 导入登录的接口
import { userRegisterService,userLoginService} from '@/api/user.js'

// 四 4.2 element-plus封装组件
import { ElMessage } from 'element-plus';

// 五 2.5 导入路由
import {useRouter} from 'vue-router'

// 五 2.5 调用路由
const router = useRouter()

//控制注册与登录表单的显示, 默认显示注册
const isRegister = ref(false)

//二 注册 2.1定义数据模型

const registerData = ref({
    username:'',
    password:'',
    rePassword:''
})

//二 注册  2.2自定义rePassword需要自定义校验规则校验规则的函数,三个参rule 规则,value 值,callback 回调函数
const checkRePassword = (rule,value,callback)=>{
    if(value === '' ){
        callback(new Error('请再次确认密码!'))
    }else if(value !== registerData.value.password){
        callback(new Error('请确保两次密码输入一致性!'))
    }else{
        callback()  //校验通过
    }
}

//二 注册  2.2 定义表单校验规则,rePassword需要自定义校验规则
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'}
    ]
}

// 二 注册  3.3 调用后台接口,完成注册
const  register = async()=>{
    // registerData 是一个响应式对象,如果要获取值需要.value
    let result = await userRegisterService(registerData.value);
    /* 使用axios的 request.js 统一处理了,优化这里代码
    if(result.code ===1){
        //注册成功
        alert(result.msg?result.msg : '注册成功')

    }else{
        // 注册失败
        alert('注册失败')

    } */
    // 四 4.2 优化alert
    // alert(result.msg?result.msg : '注册成功')
    ElMessage.success(result.msg?result.msg : '注册成功')
}

// 三 登录 1 绑定数据 复用注册表单的数据模型
// 三 登录 2  数据校验 复用注册表单的数据校验
// 三 登录 3  登录函数
const login  = async()=>{ 
    //调用接口完成登录
    let result = await userLoginService(registerData.value);
      /* 使用axios的 request.js 统一处理了,优化这里代码
    if(result.code ===1){
        //登录成功
        alert(result.msg?result.msg : '登录成功')

    }else{
        // 登录失败
        alert('登录失败')

    }*/
    // 四 4.2 优化alert
    // alert(result.msg?result.msg : '登录成功')
    ElMessage.success(result.msg?result.msg : '登录成功')
    // 五 2.5 跳转首页 借助于路由
    router.push('/')

}
// 三 登录 4 定义函数清空数据模型数据(将数据清空)
const clearRegisterDate = ()=>{
    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">  <!-- 二 注册2.1  :model="registerData"表单声明属性 --> <!-- 二 注册2.2  :rules="rules"绑定校验 -->
                <el-form-item>
                    <h1>注册</h1>
                </el-form-item>
                <el-form-item prop="username">  <!--二 注册 2.2 prop="username"绑定校验 -->
                    <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input>  <!--二 注册 2.1 v-model="registerData.username"绑定属性 -->
                </el-form-item>
                <el-form-item prop="password"> <!-- 二 注册2.2 prop="password"绑定校验 -->
                    <el-input :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.password"></el-input>  <!-- 二 注册2.1 v-model="registerData.password"绑定属性 -->
                </el-form-item>
                <el-form-item prop="rePassword">  <!-- 2.2 prop="rePassword"绑定校验 -->
                    <el-input :prefix-icon="Lock" type="password" placeholder="请输入再次密码" v-model="registerData.rePassword"></el-input>  <!-- 二 注册2.1 v-model="registerData.rePassword"绑定属性 -->
                </el-form-item>
                <!-- 注册按钮 -->
                <el-form-item>
                    <el-button class="button" type="primary" auto-insert-space @click="register"> <!-- 二 注册3.3  @click="register"  绑定注册单击事件 -->
                        注册
                    </el-button>
                </el-form-item>
                <el-form-item class="flex">
                    <el-link type="info" :underline="false" @click="isRegister = false;clearRegisterDate()"> <!--三 登录 4  调用函数clearRegisterDate() 清空数据 -->
                        ← 返回
                    </el-link>
                </el-form-item>
            </el-form>
            <!-- 登录表单 -->
            <el-form ref="form" size="large" autocomplete="off" v-else model="registerDate" rules="rules">  <!-- 三 登录 3.1  :model="registerData"表单声明属性,复用注册 --> <!-- 三 登录 3.2  :rules="rules"绑定校验 -->
                <el-form-item>
                    <h1>登录</h1>
                </el-form-item>
                <el-form-item prop="username"> <!--三 登录 3.2  prop="username"绑定校验 -->
                    <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input> <!--三 登录 3.1 v-model="registerData.username"绑定属性 -->
                </el-form-item>
                <el-form-item prop="username"> <!--三 登录 3.2  prop="username"绑定校验 -->
                    <el-input name="password" :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.password"></el-input> <!--三 登录 3.1 v-model="registerData.password"绑定属性 -->
                </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;clearRegisterDate()">  <!--三 登录 4  调用函数clearRegisterDate() 清空数据 -->
                        注册 →
                    </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>

登录页面:   http://127.0.0.1:5173/login

 

 

<template>
    文章分类
</template>
<template>
    文章管理
</template>
<template>
    更换头像
</template>
<template>
    基本资料
</template>
<template>
    重置密码
</template>
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
<script setup>
import {
    Management,
    Promotion,
    UserFilled,
    User,
    Crop,
    EditPen,
    SwitchButton,
    CaretBottom
} from '@element-plus/icons-vue'
import avatar from '@/assets/default.png'
</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">  <!-- 五 3.3  -->
                    <el-icon>
                        <Management />
                    </el-icon>
                    <span>文章分类</span>
                </el-menu-item>
                <el-menu-item index="/article/manage">  <!-- 五 3.3  -->
                    <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">  <!-- 五 3.3  -->
                        <el-icon>
                            <User />
                        </el-icon>
                        <span>基本资料</span>
                    </el-menu-item>
                    <el-menu-item index="/user/avatar">  <!-- 五 3.3  -->
                        <el-icon>
                            <Crop />
                        </el-icon>
                        <span>更换头像</span>
                    </el-menu-item>
                    <el-menu-item index="/user/resetPassword">  <!-- 五 3.3  -->
                        <el-icon>
                            <EditPen />
                        </el-icon>
                        <span>重置密码</span>
                    </el-menu-item>
                </el-sub-menu>
            </el-menu>
        </el-aside>
        <!-- 右侧主区域 -->
        <el-container>
            <!-- 头部区域 -->
            <el-header>
                <div>编码集中营:<strong>春天的菠菜</strong></div>
                <el-dropdown placement="bottom-end">
                    <span class="el-dropdown__box">
                        <el-avatar :src="avatar" />
                        <el-icon>
                            <CaretBottom />
                        </el-icon>
                    </span>
                    <template #dropdown>
                        <el-dropdown-menu>
                            <el-dropdown-item command="profile" :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>
                    </template>
                </el-dropdown>
            </el-header>
            <!-- 中间区域 -->
            <el-main>
                <!-- 五 3.3  -->
                <!-- <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>

api下新建article.js

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

// 六  文章分类列表查询函数  
export const articleCategoryListService = (registerData) =>{    
    return request.get('/category');
    
}
<script setup>
// 六 1.2 声明一个异步函数
import {articleCategoryListService} from '@/api/article.js'

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"
    }
])

// 六 1.2 声明一个异步函数
const articleCategoryList = async()=>{
    let result = await articleCategoryListService()
    categorys.value = result.data;

}
articleCategoryList();

</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-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" ></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-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>

此时页面访问存在token问题

npm install pinia

在main.js中,引入pinia,创建pinia实例,并调用vue应用实例的use方法使用pinia

import './assets/main.scss'  // 本项目使用sass

import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
//五 2.3 
import router from '@/router'


import App from './App.vue'
//六 2.2
import {createPinia} from 'pinia'

const app = createApp(App)
//六 2.2
const pinia = createPinia();
app.use(pinia)
//五 2.3 
app.use(router)


app.use(ElementPlus)
app.mount('#app')

src/stores目录下定义token.js

//定义store
import {defineStore} from 'pinia'
import {ref} from 'vue'
/* 
    第一个参数:名字,唯一性
    第二个参数:函数,函数的内部可以定义状态的所有内容

    返回值: 函数
*/
export const useTokenStore = defineStore('token',()=>{
    //定义状态的内容

    //1.响应式变量
    const token = ref('')

    //2.定义一个函数,修改token的值
    const setToken = (newToken)=>{
        token.value = newToken
    }

    //3.函数,移除token的值
    const removeToken = ()=>{
        token.value=''
    }

    return {
        token,setToken,removeToken
    }
});

在需要使用状态的地方,导入@/stores/*.js , 使用即可

在Login.vue中导入@/stores/token.js, 并且当用户登录成功后,将token保存pinia中

<script setup>
import { User, Lock } from '@element-plus/icons-vue'
import { ref } from 'vue'
//二 注册 3.3 导入注册的接口 //三 登录 3.3 导入登录的接口
import { userRegisterService,userLoginService} from '@/api/user.js'

// 四 4.2 element-plus封装的组件
import { ElMessage } from 'element-plus';

// 五 2.5 导入路由
import {useRouter} from 'vue-router'

// 六  2.4 导入token状态
import { useTokenStore } from '@/stores/token.js'

// 五 2.5 调用路由
const router = useRouter()

// 六  2.4   调用useTokenStore得到状态
const tokenStore = useTokenStore();

//控制注册与登录表单的显示, 默认显示注册
const isRegister = ref(false)

//二 注册 2.1定义数据模型

const registerData = ref({
    username:'',
    password:'',
    rePassword:''
})

//二 注册  2.2自定义rePassword需要自定义校验规则校验规则的函数,三个参rule 规则,value 值,callback 回调函数
const checkRePassword = (rule,value,callback)=>{
    if(value === '' ){
        callback(new Error('请再次确认密码!'))
    }else if(value !== registerData.value.password){
        callback(new Error('请确保两次密码输入一致性!'))
    }else{
        callback()  //校验通过
    }
}

//二 注册  2.2 定义表单校验规则,rePassword需要自定义校验规则
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'}
    ]
}

// 二 注册  3.3 调用后台接口,完成注册
const  register = async()=>{
    // registerData 是一个响应式对象,如果要获取值需要.value
    let result = await userRegisterService(registerData.value);
    /* 使用axios的 request.js 统一处理了,优化这里代码
    if(result.code ===1){
        //注册成功
        alert(result.msg?result.msg : '注册成功')

    }else{
        // 注册失败
        alert('注册失败')

    } */
    // 四 4.2 优化alert
    // alert(result.msg?result.msg : '注册成功')
    ElMessage.success(result.msg?result.msg : '注册成功')
}

// 三 登录 1 绑定数据 复用注册表单的数据模型
// 三 登录 2  数据校验 复用注册表单的数据校验
// 三 登录 3  登录函数
const login  = async()=>{ 
    //调用接口完成登录
    let result = await userLoginService(registerData.value);
      /* 使用axios的 request.js 统一处理了,优化这里代码
    if(result.code ===1){
        //登录成功
        alert(result.msg?result.msg : '登录成功')

    }else{
        // 登录失败
        alert('登录失败')

    }*/
    // 四 4.2 优化alert
    // alert(result.msg?result.msg : '登录成功')
    ElMessage.success(result.msg?result.msg : '登录成功')

    // 六  2.4  保存token存储到pinia
    tokenStore.setToken(result.data)
    
    // 五 2.5 跳转首页 借助于路由
    router.push('/')

}
// 三 登录 4 定义函数清空数据模型数据(将数据清空)
const clearRegisterDate = ()=>{
    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">  <!-- 二 注册2.1  :model="registerData"表单声明属性 --> <!-- 二 注册2.2  :rules="rules"绑定校验 -->
                <el-form-item>
                    <h1>注册</h1>
                </el-form-item>
                <el-form-item prop="username">  <!--二 注册 2.2 prop="username"绑定校验 -->
                    <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input>  <!--二 注册 2.1 v-model="registerData.username"绑定属性 -->
                </el-form-item>
                <el-form-item prop="password"> <!-- 二 注册2.2 prop="password"绑定校验 -->
                    <el-input :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.password"></el-input>  <!-- 二 注册2.1 v-model="registerData.password"绑定属性 -->
                </el-form-item>
                <el-form-item prop="rePassword">  <!-- 2.2 prop="rePassword"绑定校验 -->
                    <el-input :prefix-icon="Lock" type="password" placeholder="请输入再次密码" v-model="registerData.rePassword"></el-input>  <!-- 二 注册2.1 v-model="registerData.rePassword"绑定属性 -->
                </el-form-item>
                <!-- 注册按钮 -->
                <el-form-item>
                    <el-button class="button" type="primary" auto-insert-space @click="register"> <!-- 二 注册3.3  @click="register"  绑定注册单击事件 -->
                        注册
                    </el-button>
                </el-form-item>
                <el-form-item class="flex">
                    <el-link type="info" :underline="false" @click="isRegister = false;clearRegisterDate()"> <!--三 登录 4  调用函数clearRegisterDate() 清空数据 -->
                        ← 返回
                    </el-link>
                </el-form-item>
            </el-form>
            <!-- 登录表单 -->
            <el-form ref="form" size="large" autocomplete="off" v-else model="registerDate" rules="rules">  <!-- 三 登录 3.1  :model="registerData"表单声明属性,复用注册 --> <!-- 三 登录 3.2  :rules="rules"绑定校验 -->
                <el-form-item>
                    <h1>登录</h1>
                </el-form-item>
                <el-form-item prop="username"> <!--三 登录 3.2  prop="username"绑定校验 -->
                    <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input> <!--三 登录 3.1 v-model="registerData.username"绑定属性 -->
                </el-form-item>
                <el-form-item prop="username"> <!--三 登录 3.2  prop="username"绑定校验 -->
                    <el-input name="password" :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.password"></el-input> <!--三 登录 3.1 v-model="registerData.password"绑定属性 -->
                </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;clearRegisterDate()">  <!--三 登录 4  调用函数clearRegisterDate() 清空数据 -->
                        注册 →
                    </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>

在article.js中导入@/stores/token.js, 从pinia中获取到存储的token,在发起查询文章分类列表的时候把token通过请求头的形式携带给服务器

//导入request.js请求工具
import request from '@/utils/request.js'
// 六 2.4 导入@/stores/token.js
import { useTokenStore } from '@/stores/token.js'

// 六  文章分类列表查询函数  
export const articleCategoryListService = (registerData) =>{    
    // 六 2.4获取token状态
    const tokenStore = useTokenStore()
    //通过请求头Authorization携带token
    return request.get('/category', { headers: { 'Authorization': tokenStore.token } });
    
}

进入主页后,将来要与后台交互,都需要携带token,如果每次请求都写这样的代码,将会比较繁琐,此时可以将携带token的代码通过请求拦截器统一处理

src/util/request.js中

// 定制请求实例

//导入axios  npm install axios
import axios from 'axios';
// 四 4.2 element-plus封装的组件
import { ElMessage } from 'element-plus';

// 六  3 导入token状态
import { useTokenStore } from '@/stores/token.js';

//定义一个变量记录公共的前缀baseURL
// const baseURL = 'http://localhost:8080'  
const baseURL = '/api'          // 注释上面代码,解决跨域问题
const instance = axios.create({baseURL})
//六  3 添加请求拦截器
instance.interceptors.request.use(
    (config)=>{
        //在发送请求之前做什么
        let tokenStore = useTokenStore()
        //如果token中有值,在携带
        if(tokenStore.token){
            config.headers.Authorization=tokenStore.token
        }
        return config
    },
    (err)=>{
        //如果请求错误做什么
        Promise.reject(err)
    }
)

// 添加响应拦截器
instance.interceptors.response.use(
    result=>{
        // 四  4.1 判断业务状态码
        if(result.data.code === 1){
            //成功,正常返回数据
            return result.data;
        }
        // 操作失败
        // alert(result.data.msg?result.data.msg : '服务异常')  
        // 四 4.2 优化alert
        ElMessage.error(result.data.msg?result.data.msg : '服务异常')
        // 异步操作的状态转换为失败
        return Promise.reject(result.data)

    },
    err=>{
        alert('服务异常')
        return Promise.reject(err);//异步的状态转化成失败的状态
    }
)

export default instance;
//导入request.js请求工具
import request from '@/utils/request.js'
// 六 2.4 导入@/stores/token.js
import { useTokenStore } from '@/stores/token.js'

// 六  文章分类列表查询函数  
export const articleCategoryListService = (registerData) =>{    
    // 六 2.4获取token状态
    // 六 3.2
    // const tokenStore = useTokenStore()
    //通过请求头Authorization携带token
    // return request.get('/category', { headers: { 'Authorization': tokenStore.token } });
    
    return request.get('/category');
    
}

npm install pinia-persistedstate-plugin

在main.js中

import './assets/main.scss'  // 本项目使用sass

import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
//五 2.3 
import router from '@/router'


import App from './App.vue'
//六 2.2
import {createPinia} from 'pinia'

// 六  4.2 导入持久化插件
import {createPersistedState} from'pinia-persistedstate-plugin'

const app = createApp(App)
//六 2.2
const pinia = createPinia();
// 六  4.2
const persist = createPersistedState()
//六  4.2 pinia使用持久化插件
pinia.use(persist)
app.use(pinia)
//五 2.3 
app.use(router)


app.use(ElementPlus)
app.mount('#app')

在src/stores/token.js中

//定义store
import {defineStore} from 'pinia'
import {ref} from 'vue'
/* 
    第一个参数:名字,唯一性
    第二个参数:函数,函数的内部可以定义状态的所有内容

    返回值: 函数
*/
export const useTokenStore = defineStore('token',()=>{
    //定义状态的内容

    //1.响应式变量
    const token = ref('')

    //2.定义一个函数,修改token的值
    const setToken = (newToken)=>{
        token.value = newToken
    }

    //3.函数,移除token的值
    const removeToken = ()=>{
        token.value=''
    }

    return {
        token,setToken,removeToken
    }
},{
    persist:true//  六 4.3持久化存储
});

在后续访问接口时,如果没有登录,则前端不携带token,后台服务器会返回响应状态码401,代表未登录,此时可以在axios的响应拦截器中,统一对未登录的情况做处理request.js

// 定制请求实例

//导入axios  npm install axios
import axios from 'axios';
// 四 4.2 element-plus封装的组件
import { ElMessage } from 'element-plus';

// 六  3.1 导入token状态
import { useTokenStore } from '@/stores/token.js';

// 六 5  导入路由跳转到首页
import router from '@/router'

//定义一个变量记录公共的前缀baseURL
// const baseURL = 'http://localhost:8080'  
const baseURL = '/api'          // 注释上面代码,解决跨域问题
const instance = axios.create({baseURL})
//六  3 添加请求拦截器
instance.interceptors.request.use(
    (config)=>{
        //在发送请求之前做什么
        let tokenStore = useTokenStore()
        //如果token中有值,在携带
        if(tokenStore.token){
            config.headers.Authorization=tokenStore.token
        }
        return config
    },
    (err)=>{
        //如果请求错误做什么
        Promise.reject(err)
    }
)

// 添加响应拦截器
instance.interceptors.response.use(
    result=>{
        // 四  4.1 判断业务状态码
        if(result.data.code === 1){
            //成功,正常返回数据
            return result.data;
        }
        // 操作失败
        // alert(result.data.msg?result.data.msg : '服务异常')  
        // 四 4.2 优化alert
        // ElMessage.error(result.data.msg?result.data.msg : '服务异常')
        ElMessage.error(result.data.msg || '服务异常')
        // 异步操作的状态转换为失败
        return Promise.reject(result.data)

    },
    err=>{
       // 六 5  如果响应状态码时401,代表未登录,给出对应的提示,并跳转到登录页
       if(err.response.status===401){
        ElMessage.error('请先登录!')
        router.push('/login')
    }else{
        ElMessage.error('服务异常');
    }
    return Promise.reject(err);//异步的状态转化成失败的状态
}
)

export default instance;

添加弹窗分类、数据模型和校验规则、添加分类按钮单击事件,确认按钮单击事件、在页面中调用接口

<script setup>

// 六 6.2 element-plus封装的组件
import { ElMessage } from 'element-plus';
// 六 1.2 articleCategoryListService声明一个异步函数  6.2articleCategoryAddService
import {articleCategoryListService,articleCategoryAddService} from '@/api/article.js'

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"
    }
])

// 六 1.2 声明一个异步函数
const articleCategoryList = async()=>{
    let result = await articleCategoryListService()
    categorys.value = result.data;

}
articleCategoryList();

//  六  6.1  控制添加分类弹窗
const dialogVisible = ref(false)

//六  6.1 添加分类数据模型
const categoryModel = ref({
    categoryName: '',
    categoryAlias: ''
})
//六  6.1   添加分类表单校验
const rules = {
    categoryName: [
        { required: true, message: '请输入分类名称', trigger: 'blur' },
    ],
    categoryAlias: [
        { required: true, message: '请输入分类别名', trigger: 'blur' },
    ]
}
// 六 6.1 调用接口添加表单
const addCategory = async ()=>{
    let result = await articleCategoryAddService(categoryModel.value);
    ElMessage.success(result.message? result.message:'添加成功')
    //隐藏弹窗
    dialogVisible.value = false
    //再次访问后台接口,查询所有分类
    articleCategoryList()
}

</script>
<template>
    <el-card class="page-container">
        <template #header>
            <div class="header">
                <span>文章分类</span>
                <div class="extra">
                    <el-button type="primary" @click="dialogVisible = true">添加分类</el-button>  <!--  六 6.1  @click="dialogVisible = true"绑定点击事件-->
                </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="创建时间" prop="createTime"></el-table-column>
            <el-table-column label="更新时间" prop="updateTime"></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>
        <!--  六  6.1 添加分类弹窗 -->
        <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" @click="addCategory"> 确认 </el-button> <!--  六 6.1 @click="addCategory"绑定点击事件-->
                </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>

//导入request.js请求工具
import request from '@/utils/request.js'
// 六 2.4 导入@/stores/token.js
import { useTokenStore } from '@/stores/token.js'

// 六  文章分类列表查询函数  
export const articleCategoryListService = (registerData) =>{    
    // 六 2.4获取token状态
    // 六 3.2
    // const tokenStore = useTokenStore()
    //通过请求头Authorization携带token
    // return request.get('/category', { headers: { 'Authorization': tokenStore.token } });
    
    return request.get('/category');
    
}

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

分析编辑与新增的弹窗页面可以复用,优化实施

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

定义标题

//弹窗标题
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>

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

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

<script setup>

// 六 6.2 element-plus封装的组件
import { ElMessage } from 'element-plus';
// 六 1.2 articleCategoryListService声明一个异步函数  6.2articleCategoryAddService
import {articleCategoryListService,articleCategoryAddService} from '@/api/article.js'

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"
    }
])

// 六 1.2 声明一个异步函数
const articleCategoryList = async()=>{
    let result = await articleCategoryListService()
    categorys.value = result.data;

}
articleCategoryList();

//  六  6.1  控制添加分类弹窗
const dialogVisible = ref(false)

//六  6.1 添加分类数据模型
const categoryModel = ref({
    categoryName: '',
    categoryAlias: ''
})
//六  6.1   添加分类表单校验
const rules = {
    categoryName: [
        { required: true, message: '请输入分类名称', trigger: 'blur' },
    ],
    categoryAlias: [
        { required: true, message: '请输入分类别名', trigger: 'blur' },
    ]
}
// 六 6.1 调用接口添加表单
const addCategory = async ()=>{
    let result = await articleCategoryAddService(categoryModel.value);
    ElMessage.success(result.message? result.message:'添加成功')
    //隐藏弹窗
    dialogVisible.value = false
    //再次访问后台接口,查询所有分类
    articleCategoryList()
}

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

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

</script>
<template>
    <el-card class="page-container">
        <template #header>
            <div class="header">
                <span>文章分类</span>
                <div class="extra">
                    <el-button type="primary" @click="title='添加分类';dialogVisible = true">添加分类</el-button>  <!--  六 6.1  @click="dialogVisible = true"绑定点击事件   六7.1.3 title='添加分类';--> 
                </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="创建时间" prop="createTime"></el-table-column>
            <el-table-column label="更新时间" prop="updateTime"></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>  <!--  六 7.1.4 @click="title='修改分类';dialogVisible=true"绑定事件   六 7.2 showDialog(row)-->
                    <el-button :icon="Delete" circle plain type="danger"></el-button>
                </template>
            </el-table-column>
            <template #empty>
                <el-empty description="没有数据" />
            </template>
        </el-table>
        <!--  六  6.1 添加分类弹窗 -->
        <el-dialog v-model="dialogVisible" :title="title" width="30%">   <!--  六 7.1.2 :title="title"绑定标题-->
            <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="addCategory"> 确认 </el-button> <!--  六 6.1 @click="addCategory"绑定点击事件-->
                </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>

//导入request.js请求工具
import request from '@/utils/request.js'
// 六 2.4 导入@/stores/token.js
import { useTokenStore } from '@/stores/token.js'

// 六  文章分类列表查询函数  
export const articleCategoryListService = (registerData) =>{    
    // 六 2.4获取token状态
    // 六 3.2
    // const tokenStore = useTokenStore()
    //通过请求头Authorization携带token
    // return request.get('/category', { headers: { 'Authorization': tokenStore.token } });
    
    return request.get('/category');
    
}

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


//六 7.3.1   修改分类
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>
<script setup>

// 六 6.2 element-plus封装的组件
import { ElMessage } from 'element-plus';
// 六 1.2 articleCategoryListService声明一个异步函数  6.2articleCategoryAddService  7.3.3 articleCategoryUpdateService
import {articleCategoryListService,articleCategoryAddService,articleCategoryUpdateService} from '@/api/article.js'

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"
    }
])

// 六 1.2 声明一个异步函数
const articleCategoryList = async()=>{
    let result = await articleCategoryListService()
    categorys.value = result.data;

}
articleCategoryList();

//  六  6.1  控制添加分类弹窗
const dialogVisible = ref(false)

//六  6.1 添加分类数据模型
const categoryModel = ref({
    categoryName: '',
    categoryAlias: ''
})
//六  6.1   添加分类表单校验
const rules = {
    categoryName: [
        { required: true, message: '请输入分类名称', trigger: 'blur' },
    ],
    categoryAlias: [
        { required: true, message: '请输入分类别名', trigger: 'blur' },
    ]
}
// 六 6.1 调用接口添加表单
const addCategory = async ()=>{
    let result = await articleCategoryAddService(categoryModel.value);
    ElMessage.success(result.message? result.message:'添加成功')
    //隐藏弹窗
    dialogVisible.value = false
    //再次访问后台接口,查询所有分类
    articleCategoryList()
}

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

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

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

</script>
<template>
    <el-card class="page-container">
        <template #header>
            <div class="header">
                <span>文章分类</span>
                <div class="extra">
                    <el-button type="primary" @click="title='添加分类';dialogVisible = true">添加分类</el-button>  <!--  六 6.1  @click="dialogVisible = true"绑定点击事件   六7.1.3 title='添加分类';--> 
                </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="创建时间" prop="createTime"></el-table-column>
            <el-table-column label="更新时间" prop="updateTime"></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>  <!--  六 7.1.4 @click="title='修改分类';dialogVisible=true"绑定事件   六 7.2 showDialog(row)-->
                    <el-button :icon="Delete" circle plain type="danger"></el-button>
                </template>
            </el-table-column>
            <template #empty>
                <el-empty description="没有数据" />
            </template>
        </el-table>
        <!--  六  6.1 添加分类弹窗 -->
        <el-dialog v-model="dialogVisible" :title="title" width="30%">   <!--  六 7.1.2 :title="title"绑定标题-->
            <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> <!--  六 6.1 @click="addCategory"绑定点击事件  六 7.3.1 @click="title==='添加分类'? addCategory():updateCategory()"-->
                </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>

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

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

// 六 6.2 element-plus封装的组件
import { ElMessage } from 'element-plus';
// 六 1.2 articleCategoryListService声明一个异步函数  6.2articleCategoryAddService  7.3.3 articleCategoryUpdateService
import {articleCategoryListService,articleCategoryAddService,articleCategoryUpdateService} from '@/api/article.js'

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"
    }
])

// 六 1.2 声明一个异步函数
const articleCategoryList = async()=>{
    let result = await articleCategoryListService()
    categorys.value = result.data;

}
articleCategoryList();

//  六  6.1  控制添加分类弹窗
const dialogVisible = ref(false)

//六  6.1 添加分类数据模型
const categoryModel = ref({
    categoryName: '',
    categoryAlias: ''
})
//六  6.1   添加分类表单校验
const rules = {
    categoryName: [
        { required: true, message: '请输入分类名称', trigger: 'blur' },
    ],
    categoryAlias: [
        { required: true, message: '请输入分类别名', trigger: 'blur' },
    ]
}
// 六 6.1 调用接口添加表单
const addCategory = async ()=>{
    let result = await articleCategoryAddService(categoryModel.value);
    ElMessage.success(result.message? result.message:'添加成功')
    //隐藏弹窗
    dialogVisible.value = false
    //再次访问后台接口,查询所有分类
    articleCategoryList()
}

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

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

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

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

</script>
<template>
    <el-card class="page-container">
        <template #header>
            <div class="header">
                <span>文章分类</span>
                <div class="extra">
                    <el-button type="primary" @click="title='添加分类';dialogVisible = true;clearCategoryModel()">添加分类</el-button>  <!--  六 6.1  @click="dialogVisible = true"绑定点击事件   六7.1.3 title='添加分类'; 六 7.3.4 清空模型数据 clearCategoryModel()--> 
                </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="创建时间" prop="createTime"></el-table-column>
            <el-table-column label="更新时间" prop="updateTime"></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>  <!--  六 7.1.4 @click="title='修改分类';dialogVisible=true"绑定事件   六 7.2 showDialog(row)-->
                    <el-button :icon="Delete" circle plain type="danger"></el-button>
                </template>
            </el-table-column>
            <template #empty>
                <el-empty description="没有数据" />
            </template>
        </el-table>
        <!--  六  6.1 添加分类弹窗 -->
        <el-dialog v-model="dialogVisible" :title="title" width="30%">   <!--  六 7.1.2 :title="title"绑定标题-->
            <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> <!--  六 6.1 @click="addCategory"绑定点击事件  六 7.3.1 @click="title==='添加分类'? addCategory():updateCategory()"-->
                </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>

//导入request.js请求工具
import request from '@/utils/request.js'
// 六 2.4 导入@/stores/token.js
import { useTokenStore } from '@/stores/token.js'

// 六  文章分类列表查询函数  
export const articleCategoryListService = (registerData) =>{    
    // 六 2.4获取token状态
    // 六 3.2
    // const tokenStore = useTokenStore()
    //通过请求头Authorization携带token
    // return request.get('/category', { headers: { 'Authorization': tokenStore.token } });
    
    return request.get('/category');
    
}

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


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


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

//删除分类  给删除按钮绑定事件
const deleteCategory = (row) => {
    ElMessageBox.confirm(
        '你确认删除该分类信息吗?',
        '温馨提示',
        {
            confirmButtonText: '确认',
            cancelButtonText: '取消',
            type: 'warning',
        }
    )
        .then(() => {
            //用户点击了确认
            ElMessage({
                type: 'success',
                message: '删除成功',
            })
        })
        .catch(() => {
            //用户点击了取消
            ElMessage({
                type: 'info',
                message: '取消删除',
            })
        })
}
<template #default="{ row }">
                    <el-button :icon="Edit" circle plain type="primary" @click="showDialog(row)"></el-button>  <!--  六 7.1.4 @click="title='修改分类';dialogVisible=true"绑定事件   六 7.2 showDialog(row)-->
                    <el-button :icon="Delete" circle plain type="danger" @click="deleteCategory(row)"></el-button> <!--  六 8.3 @click="deleteCategory(row)" 绑定删除事件-->
                </template>
<script setup>

// 六 6.2  ElMessage  element-plus封装的组件 六 8.2 ElMessageBox 
import { ElMessage,ElMessageBox } from 'element-plus';
// 六 1.2 articleCategoryListService声明一个异步函数  6.2articleCategoryAddService  7.3.3 articleCategoryUpdateService 8.2 articleCategoryDeleteService
import {articleCategoryListService,articleCategoryAddService,articleCategoryUpdateService,articleCategoryDeleteService} from '@/api/article.js'

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"
    }
])

// 六 1.2 声明一个异步函数
const articleCategoryList = async()=>{
    let result = await articleCategoryListService()
    categorys.value = result.data;

}
articleCategoryList();

//  六  6.1  控制添加分类弹窗
const dialogVisible = ref(false)

//六  6.1 添加分类数据模型
const categoryModel = ref({
    categoryName: '',
    categoryAlias: ''
})
//六  6.1   添加分类表单校验
const rules = {
    categoryName: [
        { required: true, message: '请输入分类名称', trigger: 'blur' },
    ],
    categoryAlias: [
        { required: true, message: '请输入分类别名', trigger: 'blur' },
    ]
}
// 六 6.1 调用接口添加表单
const addCategory = async ()=>{
    let result = await articleCategoryAddService(categoryModel.value);
    ElMessage.success(result.message? result.message:'添加成功')
    //隐藏弹窗
    dialogVisible.value = false
    //再次访问后台接口,查询所有分类
    articleCategoryList()
}

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

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

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

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

// 六 8.2 删除分类  给删除按钮绑定事件
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="title='添加分类';dialogVisible = true;clearCategoryModel()">添加分类</el-button>  <!--  六 6.1  @click="dialogVisible = true"绑定点击事件   六7.1.3 title='添加分类'; 六 7.3.4 清空模型数据 clearCategoryModel()--> 
                </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="创建时间" prop="createTime"></el-table-column>
            <el-table-column label="更新时间" prop="updateTime"></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>  <!--  六 7.1.4 @click="title='修改分类';dialogVisible=true"绑定事件   六 7.2 showDialog(row)-->
                    <el-button :icon="Delete" circle plain type="danger" @click="deleteCategory(row)"></el-button> <!--  六 8.3 @click="deleteCategory(row)" 绑定删除事件-->
                </template>
            </el-table-column>
            <template #empty>
                <el-empty description="没有数据" />
            </template>
        </el-table>
        <!--  六  6.1 添加分类弹窗 -->
        <el-dialog v-model="dialogVisible" :title="title" width="30%">   <!--  六 7.1.2 :title="title"绑定标题-->
            <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> <!--  六 6.1 @click="addCategory"绑定点击事件  六 7.3.1 @click="title==='添加分类'? addCategory():updateCategory()"-->
                </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>

<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>
import './assets/main.scss'  // 本项目使用sass

import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
// 七 1.2 解决分页控件显示英文问题
import locale from  'element-plus/dist/locale/zh-cn.js'
//五 2.3 
import router from '@/router'


import App from './App.vue'
//六 2.2
import {createPinia} from 'pinia'

// 六  4.2 导入持久化插件
import {createPersistedState} from 'pinia-persistedstate-plugin'

const app = createApp(App)
//六 2.2
const pinia = createPinia();
// 六  4.2
const persist = createPersistedState()
//六  4.2 pinia使用持久化插件
pinia.use(persist)
app.use(pinia)
//五 2.3 
app.use(router)
// 七 1.2 解决分页控件显示英文问题
app.use(ElementPlus,{locale})
app.mount('#app')

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

//导入request.js请求工具
import request from '@/utils/request.js'
// 六 2.4 导入@/stores/token.js
import { useTokenStore } from '@/stores/token.js'

// 六  文章分类列表查询函数  
export const articleCategoryListService = (registerData) =>{    
    // 六 2.4获取token状态
    // 六 3.2
    // const tokenStore = useTokenStore()
    //通过请求头Authorization携带token
    // return request.get('/category', { headers: { 'Authorization': tokenStore.token } });
    
    return request.get('/category');
    
}

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


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


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

// 八 1.4.1 文章列表查询
export const articleListService = (params) => {
    return request.get('/article', { params: params })
   }
<script setup>
import {
    Edit,
    Delete
} from '@element-plus/icons-vue'

import { ref } from 'vue'


// 八 1.3 articleCategoryListService搜索查询条件 文章列表查询 导入   八 1.4.2 articleListService
import { articleCategoryListService ,articleListService} from '@/api/article.js'

//文章分类数据模型
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
}



// 八 1.3 搜索查询条件 文章列表查询
const getArticleCategoryList = async () => {
    //获取所有分类
    let resultC = await articleCategoryListService();
        categorys.value = resultC.data
}


// 八 1.4.2  获取文章列表数据
const articleList = 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);

    //渲染视图
    total.value = result.data.total;
    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;
            }
        }
    }
}

    

// 八  1.3 搜索查询条件 文章列表查询 调用函数getArticleCategoryList
getArticleCategoryList();
// 八 1.4.2  获取文章列表数据
articleList();


</script>
<template>
    <el-card class="page-container">
        <!-- 1 标题 -->
        <template #header>
            <div class="header">
                <span>文章管理</span>
                <div class="extra">
                    <el-button type="primary">添加文章</el-button>
                </div>
            </div>
        </template>
        <!-- 2 搜索表单 -->
        <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>
        <!-- 3 文章列表 -->
        <el-table :data="articles" style="width: 100%">
            <el-table-column label="文章标题" width="400" prop="title"></el-table-column>
            <el-table-column label="分类" prop="categoryName"></el-table-column>
            <el-table-column label="发表时间" prop="createTime"> </el-table-column>
            <el-table-column label="更新时间" prop="updateTime"> </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>
        <!-- 4 分页条 -->
        <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>

 

//当每页条数发生了变化,调用此函数
const onSizeChange = (size) => {
    pageSize.value = size
    articleList()  //七  1.4.3 **当分页条的当前页和每页条数发生变化,重新发送请求获取数据**
}
//当前页码发生变化,调用此函数
const onCurrentChange = (num) => {
    pageNum.value = num
    articleList()  //七  1.4.3 **当分页条的当前页和每页条数发生变化,重新发送请求获取数据**
}

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

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

<script setup>
import {
    Edit,
    Delete
} from '@element-plus/icons-vue'

import { ref } from 'vue'


// 七  1.3 articleCategoryListService搜索查询条件 文章列表查询 导入   八 1.4.2 articleListService
import { articleCategoryListService ,articleListService} from '@/api/article.js'

//文章分类数据模型
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
    articleList()  //七  1.4.3 **当分页条的当前页和每页条数发生变化,重新发送请求获取数据**
}
//当前页码发生变化,调用此函数
const onCurrentChange = (num) => {
    pageNum.value = num
    articleList()  //七  1.4.3 **当分页条的当前页和每页条数发生变化,重新发送请求获取数据**
}



// 七  1.3 搜索查询条件 文章列表查询
const getArticleCategoryList = async () => {
    //获取所有分类
    let resultC = await articleCategoryListService();
        categorys.value = resultC.data
}


// 七  1.4.2  获取文章列表数据
const articleList = 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);

    //渲染视图
    total.value = result.data.total;
    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;
            }
        }
    }
}
    

//七  1.3 搜索查询条件 文章列表查询 调用函数getArticleCategoryList
getArticleCategoryList();
// 七  1.4.2  获取文章列表数据
articleList();


</script>
<template>
    <el-card class="page-container">
        <!-- 1 标题 -->
        <template #header>
            <div class="header">
                <span>文章管理</span>
                <div class="extra">
                    <el-button type="primary">添加文章</el-button>
                </div>
            </div>
        </template>
        <!-- 2 搜索表单 -->
        <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" @click="articleList">搜索</el-button>  <!-- 七 2  绑定查询事件-->
                <el-button @click="categoryId='';state='';articleList()">重置</el-button>  <!-- 七 2  绑定重置事件-->
            </el-form-item>
        </el-form>
        <!-- 3 文章列表 -->
        <el-table :data="articles" style="width: 100%">
            <el-table-column label="文章标题" width="400" prop="title"></el-table-column>
            <el-table-column label="分类" prop="categoryName"></el-table-column>
            <el-table-column label="发表时间" prop="createTime"> </el-table-column>
            <el-table-column label="更新时间" prop="updateTime"> </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>
        <!-- 4 分页条 -->
        <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>

import {Plus} from '@element-plus/icons-vue'
//控制抽屉是否显示
const visibleDrawer = ref(false)
//添加表单数据模型
const articleModel = ref({
    title: '',
    categoryId: '',
    coverImg: '',
    content:'',
    sta
<!-- 抽屉 -->
        <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;
  }
}
<script setup>
import {
    Edit,
    Delete
} from '@element-plus/icons-vue'

import { ref } from 'vue'


// 七  1.3 articleCategoryListService搜索查询条件 文章列表查询 导入   八 1.4.2 articleListService
import { articleCategoryListService ,articleListService} from '@/api/article.js'

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

//文章分类数据模型
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
    articleList()  //七  1.4.3 **当分页条的当前页和每页条数发生变化,重新发送请求获取数据**
}
//当前页码发生变化,调用此函数
const onCurrentChange = (num) => {
    pageNum.value = num
    articleList()  //七  1.4.3 **当分页条的当前页和每页条数发生变化,重新发送请求获取数据**
}



// 七  1.3 搜索查询条件 文章列表查询
const getArticleCategoryList = async () => {
    //获取所有分类
    let resultC = await articleCategoryListService();
        categorys.value = resultC.data
}


// 七  1.4.2  获取文章列表数据
const articleList = 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);

    //渲染视图
    total.value = result.data.total;
    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;
            }
        }
    }
}
    

//七  1.3 搜索查询条件 文章列表查询 调用函数getArticleCategoryList
getArticleCategoryList();
// 七  1.4.2  获取文章列表数据
articleList();


</script>
<template>
    <el-card class="page-container">
        <!-- 1 标题 -->
        <template #header>
            <div class="header">
                <span>文章管理</span>
                <div class="extra">
                    <el-button type="primary" @click="visibleDrawer = true">添加文章</el-button>  <!-- 七 3.2 @click="visibleDrawer = true" 绑定查询事件-->
                </div>
            </div>
        </template>
        <!-- 2 搜索表单 -->
        <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" @click="articleList">搜索</el-button>  <!-- 七 2  绑定查询事件-->
                <el-button @click="categoryId='';state='';articleList()">重置</el-button>  <!-- 七 2  绑定重置事件-->
            </el-form-item>
        </el-form>
        <!-- 3 文章列表 -->
        <el-table :data="articles" style="width: 100%">
            <el-table-column label="文章标题" width="400" prop="title"></el-table-column>
            <el-table-column label="分类" prop="categoryName"></el-table-column>
            <el-table-column label="发表时间" prop="createTime"> </el-table-column>
            <el-table-column label="更新时间" prop="updateTime"> </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>
        <!-- 4 分页条 -->
        <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" />


        <!-- 七 3.1 抽屉 -->
        <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>
    </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;
    }
}

/* 七 3.1 抽屉样式 */
.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;
  }
}
</style>

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

官网地址VueQuill | Rich Text Editor Component for Vue 3

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

import { QuillEditor } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css'
<quill-editor
              theme="snow"
              v-model:content="articleModel.content"
              contentType="html"
              >
</quill-editor>
​
.editor {
  width: 100%;
  :deep(.ql-editor) {
    min-height: 200px;
  }
}
<script setup>
import {
    Edit,
    Delete
} from '@element-plus/icons-vue'

import { ref } from 'vue'


// 七  1.3 articleCategoryListService搜索查询条件 文章列表查询 导入   八 1.4.2 articleListService
import { articleCategoryListService ,articleListService} from '@/api/article.js'

// 七 3.1 导入添加抽屉组件
import {Plus} from '@element-plus/icons-vue'

// 七 3.3.2 导入组件和样式
import { QuillEditor } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css'


// 七 3.1 控制抽屉是否显示
const visibleDrawer = ref(false)
// 七 3.1 添加表单数据模型
const articleModel = ref({
    title: '',
    categoryId: '',
    coverImg: '',
    content:'',
    state:''
})

//文章分类数据模型
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
    articleList()  //七  1.4.3 **当分页条的当前页和每页条数发生变化,重新发送请求获取数据**
}
//当前页码发生变化,调用此函数
const onCurrentChange = (num) => {
    pageNum.value = num
    articleList()  //七  1.4.3 **当分页条的当前页和每页条数发生变化,重新发送请求获取数据**
}



// 七  1.3 搜索查询条件 文章列表查询
const getArticleCategoryList = async () => {
    //获取所有分类
    let resultC = await articleCategoryListService();
        categorys.value = resultC.data
}


// 七  1.4.2  获取文章列表数据
const articleList = 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);

    //渲染视图
    total.value = result.data.total;
    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;
            }
        }
    }
}
    

//七  1.3 搜索查询条件 文章列表查询 调用函数getArticleCategoryList
getArticleCategoryList();
// 七  1.4.2  获取文章列表数据
articleList();


</script>
<template>
    <el-card class="page-container">
        <!-- 1 标题 -->
        <template #header>
            <div class="header">
                <span>文章管理</span>
                <div class="extra">
                    <el-button type="primary" @click="visibleDrawer = true">添加文章</el-button>  <!-- 七 3.2 @click="visibleDrawer = true" 绑定查询事件-->
                </div>
            </div>
        </template>
        <!-- 2 搜索表单 -->
        <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" @click="articleList">搜索</el-button>  <!-- 七 2  绑定查询事件-->
                <el-button @click="categoryId='';state='';articleList()">重置</el-button>  <!-- 七 2  绑定重置事件-->
            </el-form-item>
        </el-form>
        <!-- 3 文章列表 -->
        <el-table :data="articles" style="width: 100%">
            <el-table-column label="文章标题" width="400" prop="title"></el-table-column>
            <el-table-column label="分类" prop="categoryName"></el-table-column>
            <el-table-column label="发表时间" prop="createTime"> </el-table-column>
            <el-table-column label="更新时间" prop="updateTime"> </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>
        <!-- 4 分页条 -->
        <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" />


        <!-- 七 3.1 抽屉 -->
        <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">
                        <quill-editor theme="snow" v-model:content="articleModel.content" contentType="html"> <!-- 七 3.3.3页面使用quill组件 -->
                        </quill-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>
    </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;
    }
}

/* 七 3.1 抽屉样式 */
.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;
  }
}
// 七 3.3.4 样式
.editor {
    width: 100%;  
    :deep(.ql-editor) {
        min-height: 200px;
  }
}
</style>

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

auto-upload:是否自动上传

action: 服务器接口路径

name: 上传文件字段

headers: 设置上传的请求头

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

</el-form-item>
                <el-form-item label="文章封面">
                    <!-- 
                        auto-upload:是否自动上传
                        action: 服务器接口路径
                        name: 上传文件字段名
                        headers: 设置上传的请求头
                        on-success: 上传成功的回调函数 -->

                    <el-upload class="avatar-uploader" :auto-upload="true" :show-file-list="false"
                    action="/api/upload"
                    name="file"
                    :headers="{Authorization:tokenStore.token}"
                    :on-success="uploadSuccess"
                    >
                        <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();
    <script setup>
    import {
        Edit,
        Delete
    } from '@element-plus/icons-vue'
    
    import { ref } from 'vue'
    
    
    // 七  1.3 articleCategoryListService搜索查询条件 文章列表查询 导入   八 1.4.2 articleListService
    import { articleCategoryListService ,articleListService} from '@/api/article.js'
    
    // 七 3.1 导入添加抽屉组件
    import {Plus} from '@element-plus/icons-vue'
    
    // 七 3.3.2 导入组件和样式
    import { QuillEditor } from '@vueup/vue-quill'
    import '@vueup/vue-quill/dist/vue-quill.snow.css'
    
    
    // 七 3.4 文章封面上传 导入token
    import { useTokenStore } from '@/stores/token.js'
    const tokenStore = useTokenStore();
    // 七 3.4 上传图片成功回调
    const uploadSuccess = (img) => {
        //img就是后台响应的数据,格式为:{code:状态码,message:提示信息,data: 图片的存储地址}
        articleModel.value.coverImg=img.data
    }
    
    // 七 3.1 控制抽屉是否显示
    const visibleDrawer = ref(false)
    // 七 3.1 添加表单数据模型
    const articleModel = ref({
        title: '',
        categoryId: '',
        coverImg: '',
        content:'',
        state:''
    })
    
    //文章分类数据模型
    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
        articleList()  //七  1.4.3 **当分页条的当前页和每页条数发生变化,重新发送请求获取数据**
    }
    //当前页码发生变化,调用此函数
    const onCurrentChange = (num) => {
        pageNum.value = num
        articleList()  //七  1.4.3 **当分页条的当前页和每页条数发生变化,重新发送请求获取数据**
    }
    
    
    
    // 七  1.3 搜索查询条件 文章列表查询
    const getArticleCategoryList = async () => {
        //获取所有分类
        let resultC = await articleCategoryListService();
            categorys.value = resultC.data
    }
    
    
    // 七  1.4.2  获取文章列表数据
    const articleList = 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);
    
        //渲染视图
        total.value = result.data.total;
        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;
                }
            }
        }
    }
        
    
    //七  1.3 搜索查询条件 文章列表查询 调用函数getArticleCategoryList
    getArticleCategoryList();
    // 七  1.4.2  获取文章列表数据
    articleList();
    
    
    </script>
    <template>
        <el-card class="page-container">
            <!-- 1 标题 -->
            <template #header>
                <div class="header">
                    <span>文章管理</span>
                    <div class="extra">
                        <el-button type="primary" @click="visibleDrawer = true">添加文章</el-button>  <!-- 七 3.2 @click="visibleDrawer = true" 绑定查询事件-->
                    </div>
                </div>
            </template>
            <!-- 2 搜索表单 -->
            <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" @click="articleList">搜索</el-button>  <!-- 七 2  绑定查询事件-->
                    <el-button @click="categoryId='';state='';articleList()">重置</el-button>  <!-- 七 2  绑定重置事件-->
                </el-form-item>
            </el-form>
            <!-- 3 文章列表 -->
            <el-table :data="articles" style="width: 100%">
                <el-table-column label="文章标题" width="400" prop="title"></el-table-column>
                <el-table-column label="分类" prop="categoryName"></el-table-column>
                <el-table-column label="发表时间" prop="createTime"> </el-table-column>
                <el-table-column label="更新时间" prop="updateTime"> </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>
            <!-- 4 分页条 -->
            <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" />
    
    
            <!-- 七 3.1 抽屉 -->
            <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="文章封面">
                        <!-- 
                            auto-upload:是否自动上传
                            action: 服务器接口路径
                            name: 上传的文件字段名
                            headers: 设置上传的请求头
                            on-success: 上传成功的回调函数 -->
    
                        <el-upload class="avatar-uploader" :auto-upload="true" :show-file-list="false"
                        action="/api/upload"
                        name="file"
                        :headers="{Authorization:tokenStore.token}"
                        :on-success="uploadSuccess"
                        >
                            <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">
                            <quill-editor theme="snow" v-model:content="articleModel.content" contentType="html"> <!-- 七 3.3.3页面使用quill组件 -->
                            </quill-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>
        </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;
        }
    }
    
    /* 七 3.1 抽屉样式 */
    .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;
      }
    }
    // 七 3.3.4 样式
    .editor {
        width: 100%;  
        :deep(.ql-editor) {
            min-height: 200px;
      }
    }
    </style>

//导入request.js请求工具
import request from '@/utils/request.js'
// 六 2.4 导入@/stores/token.js
import { useTokenStore } from '@/stores/token.js'

// 六  文章分类列表查询函数  
export const articleCategoryListService = (registerData) =>{    
    // 六 2.4获取token状态
    // 六 3.2
    // const tokenStore = useTokenStore()
    //通过请求头Authorization携带token
    // return request.get('/category', { headers: { 'Authorization': tokenStore.token } });
    
    return request.get('/category');
    
}

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


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


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

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


//七 3.5.1添加文章
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>
const addArticle=async (state)=>{
    articleModel.value.state = state
    let result = await articleAddService(articleModel.value);
    ElMessage.success(result.message? result.message:'添加成功')
    //再次调用getArticles,获取文章
    articleList()
    //隐藏抽屉
    visibleDrawer.value=false
}
<script setup>
import {
    Edit,
    Delete
} from '@element-plus/icons-vue'

import { ref } from 'vue'

// 七 3.5.3 
import { ElMessage } from 'element-plus'

// 七  1.3 articleCategoryListService搜索查询条件 文章列表查询 导入   七 1.4.2 articleListService  七3.5.1  articleAddService
import { articleCategoryListService ,articleListService,articleAddService} from '@/api/article.js'

// 七 3.1 导入添加抽屉组件
import {Plus} from '@element-plus/icons-vue'

// 七 3.3.2 导入组件和样式
import { QuillEditor } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css'


// 七 3.4 文章封面上传 导入token
import { useTokenStore } from '@/stores/token.js'
const tokenStore = useTokenStore();
// 七 3.4 上传图片成功回调
const uploadSuccess = (img) => {
    //img就是后台响应的数据,格式为:{code:状态码,message:提示信息,data: 图片的存储地址}
    articleModel.value.coverImg=img.data
}

// 七 3.1 控制抽屉是否显示
const visibleDrawer = ref(false)
// 七 3.1 添加表单数据模型
const articleModel = ref({
    title: '',
    categoryId: '',
    coverImg: '',
    content:'',
    state:''
})

//文章分类数据模型
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
    articleList()  //七  1.4.3 **当分页条的当前页和每页条数发生变化,重新发送请求获取数据**
}
//当前页码发生变化,调用此函数
const onCurrentChange = (num) => {
    pageNum.value = num
    articleList()  //七  1.4.3 **当分页条的当前页和每页条数发生变化,重新发送请求获取数据**
}



// 七  1.3 搜索查询条件 文章列表查询
const getArticleCategoryList = async () => {
    //获取所有分类
    let resultC = await articleCategoryListService();
        categorys.value = resultC.data
}


// 七  1.4.2  获取文章列表数据
const articleList = 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);

    //渲染视图
    total.value = result.data.total;
    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;
            }
        }
    }
}
    

//七  1.3 搜索查询条件 文章列表查询 调用函数getArticleCategoryList
getArticleCategoryList();
// 七  1.4.2  获取文章列表数据
articleList();


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

</script>
<template>
    <el-card class="page-container">
        <!-- 1 标题 -->
        <template #header>
            <div class="header">
                <span>文章管理</span>
                <div class="extra">
                    <el-button type="primary" @click="visibleDrawer = true">添加文章</el-button>  <!-- 七 3.2 @click="visibleDrawer = true" 绑定查询事件-->
                </div>
            </div>
        </template>
        <!-- 2 搜索表单 -->
        <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" @click="articleList">搜索</el-button>  <!-- 七 2  绑定查询事件-->
                <el-button @click="categoryId='';state='';articleList()">重置</el-button>  <!-- 七 2  绑定重置事件-->
            </el-form-item>
        </el-form>
        <!-- 3 文章列表 -->
        <el-table :data="articles" style="width: 100%">
            <el-table-column label="文章标题" width="400" prop="title"></el-table-column>
            <el-table-column label="分类" prop="categoryName"></el-table-column>
            <el-table-column label="发表时间" prop="createTime"> </el-table-column>
            <el-table-column label="更新时间" prop="updateTime"> </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>
        <!-- 4 分页条 -->
        <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" />


        <!-- 七 3.1 抽屉 -->
        <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="文章封面">
                    <!-- 
                        auto-upload:是否自动上传
                        action: 服务器接口路径
                        name: 上传的文件字段名
                        headers: 设置上传的请求头
                        on-success: 上传成功的回调函数 -->

                    <el-upload class="avatar-uploader" :auto-upload="true" :show-file-list="false"
                    action="/api/upload"
                    name="file"
                    :headers="{Authorization:tokenStore.token}"
                    :on-success="uploadSuccess"
                    >
                        <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">
                        <quill-editor theme="snow" v-model:content="articleModel.content" contentType="html"> <!-- 七 3.3.3页面使用quill组件 -->
                        </quill-editor>
                    </div>
                </el-form-item>
                <el-form-item>
                    <el-form-item>
                        <el-button type="primary" @click="addArticle('已发布')">发布</el-button> <!-- 七 3.5.2 @click="addArticle('已发布')"-->
                        <el-button type="info" @click="addArticle('草稿')">草稿</el-button> <!-- 七 3.5.2@click="addArticle('草稿')"-->
                    </el-form-item>
                </el-form-item>
            </el-form>
        </el-drawer>
    </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;
    }
}

/* 七 3.1 抽屉样式 */
.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;
  }
}
// 七 3.3.4 样式
.editor {
    width: 100%;  
    :deep(.ql-editor) {
        min-height: 200px;
  }
}
</style>

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

//抽屉标题
const title=ref('')
 <el-drawer v-model="visibleDrawer" :title="title" direction="rtl" size="50%">
 <el-button type="primary" @click="title='添加文章';visibleDrawer = true">添加文章</el-button> 
 <el-button :icon="Edit" circle plain type="primary" @click="title='修改文章';visibleDrawer='true'"></el-button> 

当点击修改文章按钮时,需要把当前这一条数据的详细信息显示到修改文章的抽屉上,这个叫回显

 <template #default="{ row }">
                    <el-button :icon="Edit" circle plain type="primary" @click="showDrawer(row)"></el-button> <!--七4.1.4 @click="title='修改文章';visibleDrawer='true'"  七 4.2 showDrawer(row) -->
                    <el-button :icon="Delete" circle plain type="danger"></el-button>
                </template>
// 七 4.2.2展示抽屉弹窗
const showDrawer = (row)=>{
    title.value = '修改文章';visibleDrawer.value = true
    //数据拷贝
    articleModel.value.title = row.title;
    articleModel.value.categoryId = row.categoryId;
    articleModel.value.coverImg = row.coverImg;
    articleModel.value.content = row.content;
    //扩展id属性,将来需要传参给后台完成文章的修改
    articleModel.value.id = row.id
}
<script setup>
import {
    Edit,
    Delete
} from '@element-plus/icons-vue'

import { ref } from 'vue'

// 七 3.5.3 
import { ElMessage } from 'element-plus'

// 七  1.3 articleCategoryListService搜索查询条件 文章列表查询 导入   七 1.4.2 articleListService  七3.5.1  articleAddService
import { articleCategoryListService ,articleListService,articleAddService} from '@/api/article.js'

// 七 3.1 导入添加抽屉组件
import {Plus} from '@element-plus/icons-vue'

// 七 3.3.2 导入组件和样式
import { QuillEditor } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css'


// 七 3.4 文章封面上传 导入token
import { useTokenStore } from '@/stores/token.js'
const tokenStore = useTokenStore();
// 七 3.4 上传图片成功回调
const uploadSuccess = (img) => {
    //img就是后台响应的数据,格式为:{code:状态码,message:提示信息,data: 图片的存储地址}
    articleModel.value.coverImg=img.data
}

// 七 3.1 控制抽屉是否显示
const visibleDrawer = ref(false)
// 七 3.1 添加表单数据模型
const articleModel = ref({
    title: '',
    categoryId: '',
    coverImg: '',
    content:'',
    state:''
})

//文章分类数据模型
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
    articleList()  //七  1.4.3 **当分页条的当前页和每页条数发生变化,重新发送请求获取数据**
}
//当前页码发生变化,调用此函数
const onCurrentChange = (num) => {
    pageNum.value = num
    articleList()  //七  1.4.3 **当分页条的当前页和每页条数发生变化,重新发送请求获取数据**
}



// 七  1.3 搜索查询条件 文章列表查询
const getArticleCategoryList = async () => {
    //获取所有分类
    let resultC = await articleCategoryListService();
        categorys.value = resultC.data
}


// 七  1.4.2  获取文章列表数据
const articleList = 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);

    //渲染视图
    total.value = result.data.total;
    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;
            }
        }
    }
}
    

//七  1.3 搜索查询条件 文章列表查询 调用函数getArticleCategoryList
getArticleCategoryList();
// 七  1.4.2  获取文章列表数据
articleList();


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

//<!-- 七 4.1 抽屉:title="title" -绑定标题 -->
const title=ref('')

// 七 4.2.2展示抽屉弹窗
const showDrawer = (row)=>{
    title.value = '修改文章';visibleDrawer.value = true
    //数据拷贝
    articleModel.value.title = row.title;
    articleModel.value.categoryId = row.categoryId;
    articleModel.value.coverImg = row.coverImg;
    articleModel.value.content = row.content;
    //扩展id属性,将来需要传参给后台完成文章的修改
    articleModel.value.id = row.id
}

</script>
<template>
    <el-card class="page-container">
        <!-- 1 标题 -->
        <template #header>
            <div class="header">
                <span>文章管理</span>
                <div class="extra">
                    <el-button type="primary" @click="title='添加文章';visibleDrawer = true">添加文章</el-button>  <!-- 七 3.2 @click="visibleDrawer = true" 绑定查询事件 七 4.1.3 title='添加文章'; -->
                </div>
            </div>
        </template>
        <!-- 2 搜索表单 -->
        <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" @click="articleList">搜索</el-button>  <!-- 七 2  绑定查询事件-->
                <el-button @click="categoryId='';state='';articleList()">重置</el-button>  <!-- 七 2  绑定重置事件-->
            </el-form-item>
        </el-form>
        <!-- 3 文章列表 -->
        <el-table :data="articles" style="width: 100%">
            <el-table-column label="文章标题" width="400" prop="title"></el-table-column>
            <el-table-column label="分类" prop="categoryName"></el-table-column>
            <el-table-column label="发表时间" prop="createTime"> </el-table-column>
            <el-table-column label="更新时间" prop="updateTime"> </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" @click="showDrawer(row)"></el-button> <!--七4.1.4 @click="title='修改文章';visibleDrawer='true'"  七 4.2.1 showDrawer(row) -->
                    <el-button :icon="Delete" circle plain type="danger"></el-button>
                </template>
            </el-table-column>
            <template #empty>
                <el-empty description="没有数据" />
            </template>
        </el-table>
        <!-- 4 分页条 -->
        <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" />


        <!-- 七 3.1 抽屉 -->
        <el-drawer v-model="visibleDrawer" :title="title" direction="rtl" size="50%"> <!-- 七 4.1 抽屉:title="title" -绑定标题 -->
            <!-- 添加文章表单 -->
            <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="文章封面">
                    <!-- 
                        auto-upload:是否自动上传
                        action: 服务器接口路径
                        name: 上传的文件字段名
                        headers: 设置上传的请求头
                        on-success: 上传成功的回调函数 -->

                    <el-upload class="avatar-uploader" :auto-upload="true" :show-file-list="false"
                    action="/api/upload"
                    name="file"
                    :headers="{Authorization:tokenStore.token}"
                    :on-success="uploadSuccess"
                    >
                        <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">
                        <quill-editor theme="snow" v-model:content="articleModel.content" contentType="html"> <!-- 七 3.3.3页面使用quill组件 -->
                        </quill-editor>
                    </div>
                </el-form-item>
                <el-form-item>
                    <el-form-item>
                        <el-button type="primary" @click="addArticle('已发布')">发布</el-button> <!-- 七 3.5.2 @click="addArticle('已发布')"-->
                        <el-button type="info" @click="addArticle('草稿')">草稿</el-button> <!-- 七 3.5.2@click="addArticle('草稿')"-->
                    </el-form-item>
                </el-form-item>
            </el-form>
        </el-drawer>
    </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;
    }
}

/* 七 3.1 抽屉样式 */
.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;
  }
}
// 七 3.3.4 样式
.editor {
    width: 100%;  
    :deep(.ql-editor) {
        min-height: 200px;
  }
}
</style>
//导入request.js请求工具
import request from '@/utils/request.js'
// 六 2.4 导入@/stores/token.js
import { useTokenStore } from '@/stores/token.js'

// 六  文章分类列表查询函数  
export const articleCategoryListService = (registerData) =>{    
    // 六 2.4获取token状态
    // 六 3.2
    // const tokenStore = useTokenStore()
    //通过请求头Authorization携带token
    // return request.get('/category', { headers: { 'Authorization': tokenStore.token } });
    
    return request.get('/category');
    
}

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


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


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

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


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

//七 4.3.1修改文章
export const articleUpdateService = (articleModel)=>{
    return request.put('/article',articleModel)
   }
<el-form-item>
                        <el-button type="primary" @click="title==='添加文章'? addArticle('已发布'): updateArticle('已发布')">发布</el-button> <!-- 七 3.5.2 @click="addArticle('已发布')  七 4.3.2 ;title==='添加文章'? addArticle('已发布'):updateArticle('已发布')"-->
                        <el-button type="info" @click="title==='添加文章'? addArticle('草稿'): updateArticle('草稿')">草稿</el-button> <!-- 七 3.5.2@click="addArticle('草稿')"  七 4.3.2 ;title==='添加文章'? addArticle('草稿'):updateArticle('草稿')-->
                    </el-form-item>
//七 4.4.4 修改文章
const updateArticle=async ()=>{
    let result = await articleUpdateService(articleModel.value)
    ElMessage.success(result.message? result.message:'修改成功')
    //隐藏抽屉
    visibleDrawer.value=false
    //再次访问后台接口,查询所有文章
    articleList()
}

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

//七 4.4.3 清空模型数据
const clearArticleModel = ()=>{
    articleModel.value.title='',
    articleModel.value.categoryId='',
    articleModel.value.coverImg='',
    articleModel.value.content='<br>',
    articleModel.value.state=''
}

修改 添加文章按钮的点击事件()

<el-button type="primary" @click="title='添加文章';visibleDrawer = true;clearArticleModel()">添加文章</el-button>  <!-- 七 3.2 @click="visibleDrawer = true" 绑定查询事件 七 4.1.3 title='添加文章';  七 4.3.3 ;clearArticleModel() -->
<script setup>
import {
    Edit,
    Delete
} from '@element-plus/icons-vue'

import { ref } from 'vue'

// 七 3.5.3 
import { ElMessage } from 'element-plus'

// 七  1.3 articleCategoryListService搜索查询条件 文章列表查询 导入   七 1.4.2 articleListService  七3.5.1  articleAddService 七 4.3.3articleUpdateService 
import { articleCategoryListService ,articleListService,articleAddService,articleUpdateService} from '@/api/article.js'

// 七 3.1 导入添加抽屉组件
import {Plus} from '@element-plus/icons-vue'

// 七 3.3.2 导入组件和样式
import { QuillEditor } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css'


// 七 3.4 文章封面上传 导入token
import { useTokenStore } from '@/stores/token.js'
const tokenStore = useTokenStore();
// 七 3.4 上传图片成功回调
const uploadSuccess = (img) => {
    //img就是后台响应的数据,格式为:{code:状态码,message:提示信息,data: 图片的存储地址}
    articleModel.value.coverImg=img.data
}

// 七 3.1 控制抽屉是否显示
const visibleDrawer = ref(false)
// 七 3.1 添加表单数据模型
const articleModel = ref({
    title: '',
    categoryId: '',
    coverImg: '',
    content:'',
    state:''
})

//文章分类数据模型
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
    articleList()  //七  1.4.3 **当分页条的当前页和每页条数发生变化,重新发送请求获取数据**
}
//当前页码发生变化,调用此函数
const onCurrentChange = (num) => {
    pageNum.value = num
    articleList()  //七  1.4.3 **当分页条的当前页和每页条数发生变化,重新发送请求获取数据**
}



// 七  1.3 搜索查询条件 文章列表查询
const getArticleCategoryList = async () => {
    //获取所有分类
    let resultC = await articleCategoryListService();
        categorys.value = resultC.data
}


// 七  1.4.2  获取文章列表数据
const articleList = 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);

    //渲染视图
    total.value = result.data.total;
    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;
            }
        }
    }
}
    

//七  1.3 搜索查询条件 文章列表查询 调用函数getArticleCategoryList
getArticleCategoryList();
// 七  1.4.2  获取文章列表数据
articleList();


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

//<!-- 七 4.1 抽屉:title="title" -绑定标题 -->
const title=ref('')

// 七 4.2.2展示抽屉弹窗
const showDrawer = (row)=>{
    title.value = '修改文章';visibleDrawer.value = true
    //数据拷贝
    articleModel.value.title = row.title;
    articleModel.value.categoryId = row.categoryId;
    articleModel.value.coverImg = row.coverImg;
    articleModel.value.content = row.content;
    //扩展id属性,将来需要传参给后台完成文章的修改
    articleModel.value.id = row.id
}

//七 4.4.3 修改文章
const updateArticle=async ()=>{
    let result = await articleUpdateService(articleModel.value)
    ElMessage.success(result.message? result.message:'修改成功')
    //隐藏抽屉
    visibleDrawer.value=false
    //再次访问后台接口,查询所有文章
    articleList()
}

//七 4.4.3 清空模型数据
const clearArticleModel = ()=>{
     
    articleModel.value.title='',
    articleModel.value.categoryId='',
    articleModel.value.coverImg='',
    articleModel.value.content=' ', // 这里清空不了富文本,没有找到解决方案 暂时多了一个空格处理
    articleModel.value.state=''
}

</script>
<template>
    <el-card class="page-container">
        <!-- 1 标题 -->
        <template #header>
            <div class="header">
                <span>文章管理</span>
                <div class="extra">
                    <el-button type="primary" @click="title='添加文章';visibleDrawer = true;clearArticleModel()">添加文章</el-button>  <!-- 七 3.2 @click="visibleDrawer = true" 绑定查询事件 七 4.1.3 title='添加文章';  七 4.3.3 ;clearArticleModel() -->
                </div>
            </div>
        </template>
        <!-- 2 搜索表单 -->
        <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" @click="articleList">搜索</el-button>  <!-- 七 2  绑定查询事件-->
                <el-button @click="categoryId='';state='';articleList()">重置</el-button>  <!-- 七 2  绑定重置事件-->
            </el-form-item>
        </el-form>
        <!-- 3 文章列表 -->
        <el-table :data="articles" style="width: 100%">
            <el-table-column label="文章标题" width="400" prop="title"></el-table-column>
            <el-table-column label="分类" prop="categoryName"></el-table-column>
            <el-table-column label="发表时间" prop="createTime"> </el-table-column>
            <el-table-column label="更新时间" prop="updateTime"> </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" @click="showDrawer(row)"></el-button> <!--七4.1.4 @click="title='修改文章';visibleDrawer='true'"  七 4.2.1 showDrawer(row) -->
                    <el-button :icon="Delete" circle plain type="danger"></el-button>
                </template>
            </el-table-column>
            <template #empty>
                <el-empty description="没有数据" />
            </template>
        </el-table>
        <!-- 4 分页条 -->
        <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" />


        <!-- 七 3.1 抽屉 -->
        <el-drawer v-model="visibleDrawer" :title="title" direction="rtl" size="50%"> <!-- 七 4.1 抽屉:title="title" -绑定标题 -->
            <!-- 添加文章表单 -->
            <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="文章封面">
                    <!-- 
                        auto-upload:是否自动上传
                        action: 服务器接口路径
                        name: 上传的文件字段名
                        headers: 设置上传的请求头
                        on-success: 上传成功的回调函数 -->

                    <el-upload class="avatar-uploader" :auto-upload="true" :show-file-list="false"
                    action="/api/upload"
                    name="file"
                    :headers="{Authorization:tokenStore.token}"
                    :on-success="uploadSuccess"
                    >
                        <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">
                        <quill-editor theme="snow" v-model:content="articleModel.content" contentType="html"> <!-- 七 3.3.3页面使用quill组件 -->
                        </quill-editor>
                    </div>
                </el-form-item>
                <el-form-item>
                    <el-form-item>
                        <el-button type="primary" @click="title==='添加文章'? addArticle('已发布'): updateArticle('已发布')">发布</el-button> <!-- 七 3.5.2 @click="addArticle('已发布')  七 4.3.2 ;title==='添加文章'? addArticle('已发布'):updateArticle('已发布')"-->
                        <el-button type="info" @click="title==='添加文章'? addArticle('草稿'): updateArticle('草稿')">草稿</el-button> <!-- 七 3.5.2@click="addArticle('草稿')"  七 4.3.2 ;title==='添加文章'? addArticle('草稿'):updateArticle('草稿')-->
                    </el-form-item>
                </el-form-item>
            </el-form>
        </el-drawer>
    </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;
    }
}

/* 七 3.1 抽屉样式 */
.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;
  }
}
// 七 3.3.4 样式
.editor {
    width: 100%;  
    :deep(.ql-editor) {
        min-height: 200px;
  }
}
</style>

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

//导入request.js请求工具
import request from '@/utils/request.js'
// 六 2.4 导入@/stores/token.js
import { useTokenStore } from '@/stores/token.js'

// 六  文章分类列表查询函数  
export const articleCategoryListService = (registerData) =>{    
    // 六 2.4获取token状态
    // 六 3.2
    // const tokenStore = useTokenStore()
    //通过请求头Authorization携带token
    // return request.get('/category', { headers: { 'Authorization': tokenStore.token } });
    
    return request.get('/category');
    
}

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


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


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

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


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

//七 4.3.1修改文章
export const articleUpdateService = (articleModel)=>{
    return request.put('/article',articleModel)
   }

//七 5.1删除文章
export const articleDeleteService = (id) => {
    return request.delete('/article?id='+id)
   }

<template #default="{ row }">
                    <el-button :icon="Edit" circle plain type="primary" @click="showDrawer(row)"></el-button> <!--七4.1.4 @click="title='修改文章';visibleDrawer='true'"  七 4.2.1 showDrawer(row) -->
                    <el-button :icon="Delete" circle plain type="danger" @click="deleteArticle(row)"></el-button> <!-- 七 5.2 为删除按钮绑定事件-->
                </template>
<script setup>
import {
    Edit,
    Delete
} from '@element-plus/icons-vue'

import { ref } from 'vue'

// 七 3.5.3 
import { ElMessage,ElMessageBox } from 'element-plus'

// 七  1.3 articleCategoryListService搜索查询条件 文章列表查询 导入   七 1.4.2 articleListService  七3.5.1  articleAddService 七 4.3.3articleUpdateService  七 5.3 articleDeleteService
import { articleCategoryListService ,articleListService,articleAddService,articleUpdateService,articleDeleteService} from '@/api/article.js'

// 七 3.1 导入添加抽屉组件
import {Plus} from '@element-plus/icons-vue'

// 七 3.3.2 导入组件和样式
import { QuillEditor } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css'


// 七 3.4 文章封面上传 导入token
import { useTokenStore } from '@/stores/token.js'
const tokenStore = useTokenStore();
// 七 3.4 上传图片成功回调
const uploadSuccess = (img) => {
    //img就是后台响应的数据,格式为:{code:状态码,message:提示信息,data: 图片的存储地址}
    articleModel.value.coverImg=img.data
}

// 七 3.1 控制抽屉是否显示
const visibleDrawer = ref(false)
// 七 3.1 添加表单数据模型
const articleModel = ref({
    title: '',
    categoryId: '',
    coverImg: '',
    content:'',
    state:''
})

//文章分类数据模型
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
    articleList()  //七  1.4.3 **当分页条的当前页和每页条数发生变化,重新发送请求获取数据**
}
//当前页码发生变化,调用此函数
const onCurrentChange = (num) => {
    pageNum.value = num
    articleList()  //七  1.4.3 **当分页条的当前页和每页条数发生变化,重新发送请求获取数据**
}



// 七  1.3 搜索查询条件 文章列表查询
const getArticleCategoryList = async () => {
    //获取所有分类
    let resultC = await articleCategoryListService();
        categorys.value = resultC.data
}


// 七  1.4.2  获取文章列表数据
const articleList = 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);

    //渲染视图
    total.value = result.data.total;
    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;
            }
        }
    }
}
    

//七  1.3 搜索查询条件 文章列表查询 调用函数getArticleCategoryList
getArticleCategoryList();
// 七  1.4.2  获取文章列表数据
articleList();


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

//<!-- 七 4.1 抽屉:title="title" -绑定标题 -->
const title=ref('')

// 七 4.2.2展示抽屉弹窗
const showDrawer = (row)=>{
    title.value = '修改文章';visibleDrawer.value = true
    //数据拷贝
    articleModel.value.title = row.title;
    articleModel.value.categoryId = row.categoryId;
    articleModel.value.coverImg = row.coverImg;
    articleModel.value.content = row.content;
    //扩展id属性,将来需要传参给后台完成文章的修改
    articleModel.value.id = row.id
}

//七 4.4.3 修改文章
const updateArticle=async (state)=>{
    articleModel.value.state = state
    let result = await articleUpdateService(articleModel.value)
    ElMessage.success(result.message? result.message:'修改成功')
    //隐藏抽屉
    visibleDrawer.value=false
    //再次访问后台接口,查询所有文章
    articleList()
}

//七 4.4.3 清空模型数据
const clearArticleModel = ()=>{
     
    articleModel.value.title='',
    articleModel.value.categoryId='',
    articleModel.value.coverImg='',
    articleModel.value.content=' ', // 这里清空不了富文本,没有找到解决方案 暂时多了一个空格处理
    articleModel.value.state=''
}


// 七 5.3 删除文章(记得引用ElMessageBox)  给删除按钮绑定事件
const deleteArticle = (row) => {
    // 提示用户 确认框
    ElMessageBox.confirm(
        '你确认删除该文章信息吗?',
        '温馨提示',
        {
            confirmButtonText: '确认',
            cancelButtonText: '取消',
            type: 'warning',
        }
    )
        .then(async() => {
            //用户点击了确认
            let result = await articleDeleteService(row.id)
            // 给出提示
            ElMessage({
                type: 'success',
                message: '删除成功',
            })
            //刷新列表//再次访问后台接口,查询所有文章
            articleList()
        })
        .catch(() => {
            //用户点击了取消
            ElMessage({
                type: 'info',
                message: '取消删除',
            })
        })
}

</script>
<template>
    <el-card class="page-container">
        <!-- 1 标题 -->
        <template #header>
            <div class="header">
                <span>文章管理</span>
                <div class="extra">
                    <el-button type="primary" @click="title='添加文章';visibleDrawer = true;clearArticleModel()">添加文章</el-button>  <!-- 七 3.2 @click="visibleDrawer = true" 绑定查询事件 七 4.1.3 title='添加文章';  七 4.3.3 ;clearArticleModel() -->
                </div>
            </div>
        </template>
        <!-- 2 搜索表单 -->
        <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" @click="articleList">搜索</el-button>  <!-- 七 2  绑定查询事件-->
                <el-button @click="categoryId='';state='';articleList()">重置</el-button>  <!-- 七 2  绑定重置事件-->
            </el-form-item>
        </el-form>
        <!-- 3 文章列表 -->
        <el-table :data="articles" style="width: 100%">
            <el-table-column label="文章标题" width="400" prop="title"></el-table-column>
            <el-table-column label="分类" prop="categoryName"></el-table-column>
            <el-table-column label="发表时间" prop="createTime"> </el-table-column>
            <el-table-column label="更新时间" prop="updateTime"> </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" @click="showDrawer(row)"></el-button> <!--七4.1.4 @click="title='修改文章';visibleDrawer='true'"  七 4.2.1 showDrawer(row) -->
                    <el-button :icon="Delete" circle plain type="danger" @click="deleteArticle(row)"></el-button> <!-- 七 5.2 为删除按钮绑定事件-->
                </template>
            </el-table-column>
            <template #empty>
                <el-empty description="没有数据" />
            </template>
        </el-table>
        <!-- 4 分页条 -->
        <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" />


        <!-- 七 3.1 抽屉 -->
        <el-drawer v-model="visibleDrawer" :title="title" direction="rtl" size="50%"> <!-- 七 4.1 抽屉:title="title" -绑定标题 -->
            <!-- 添加文章表单 -->
            <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="文章封面">
                    <!-- 
                        auto-upload:是否自动上传
                        action: 服务器接口路径
                        name: 上传的文件字段名
                        headers: 设置上传的请求头
                        on-success: 上传成功的回调函数 -->

                    <el-upload class="avatar-uploader" :auto-upload="true" :show-file-list="false"
                    action="/api/upload"
                    name="file"
                    :headers="{Authorization:tokenStore.token}"
                    :on-success="uploadSuccess"
                    >
                        <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">
                        <quill-editor theme="snow" v-model:content="articleModel.content" contentType="html"> <!-- 七 3.3.3页面使用quill组件 -->
                        </quill-editor>
                    </div>
                </el-form-item>
                <el-form-item>
                    <el-form-item>
                        <el-button type="primary" @click="title==='添加文章'? addArticle('已发布'): updateArticle('已发布')">发布</el-button> <!-- 七 3.5.2 @click="addArticle('已发布')  七 4.3.2 ;title==='添加文章'? addArticle('已发布'):updateArticle('已发布')"-->
                        <el-button type="info" @click="title==='添加文章'? addArticle('草稿'): updateArticle('草稿')">草稿</el-button> <!-- 七 3.5.2@click="addArticle('草稿')"  七 4.3.2 ;title==='添加文章'? addArticle('草稿'):updateArticle('草稿')-->
                    </el-form-item>
                </el-form-item>
            </el-form>
        </el-drawer>
    </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;
    }
}

/* 七 3.1 抽屉样式 */
.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;
  }
}
// 七 3.3.4 样式
.editor {
    width: 100%;  
    :deep(.ql-editor) {
        min-height: 200px;
  }
}
</style>

在Layout.vue中,页面加载完就发送请求,获取个人信息展示,并存储到pinia中,因为将来在个人中心中修改信息的时候还需要使用

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

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

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

}

// 八  1获取个人信息
export const userInfoGetService = ()=>{
    return request.get('/user/userInfo');
   }
import {defineStore} from 'pinia'
import {ref} from 'vue'
const useUserInfoStore = defineStore('userInfo',()=>{
    //定义状态相关的内容

    const info = ref({})

    const setInfo = (newInfo)=>{
        info.value = newInfo
    }


    const removeInfo = ()=>{
        info.value = {}
    }

    return {info,setInfo,removeInfo}

},{persist:true})

export default useUserInfoStore;

import {ref} from 'vue'
//导入接口函数
import { userInfoGetService } from '@/api/user.js'
//导入pinia
import useUserInfoStore  from '@/stores/userInfo.js'

const userInfoStore = useUserInfoStore();
//获取个人信息
const getUserInfo = async ()=>{
    let result = await userInfoGetService();
    //存储pinia
    userInfoStore.info =result.data;
}
getUserInfo()

 <div>编码集中营:<strong>{{ userInfoStore.info.nickname ? userInfoStore.info.nickname : userInfoStore.info.usrename }}</strong></div>
 <el-avatar :src="userInfoStore.info.userPic ? userInfoStore.info.userPic : avatar" />
​全部Layout.vue
<script setup>
import {
    Management,
    Promotion,
    UserFilled,
    User,
    Crop,
    EditPen,
    SwitchButton,
    CaretBottom
} from '@element-plus/icons-vue'
import avatar from '@/assets/default.png'
// ======= 八  3  个人用户信息
import {ref} from 'vue'
//导入接口函数
import { userInfoGetService } from '@/api/user.js'
//导入pinia
import useUserInfoStore  from '@/stores/userInfo.js'

const userInfoStore = useUserInfoStore();
//获取个人信息
const getUserInfo = async ()=>{
    let result = await userInfoGetService();
    //存储pinia
    userInfoStore.info =result.data;
}
getUserInfo()




</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">  <!-- 五 3.3  -->
                    <el-icon>
                        <Management />
                    </el-icon>
                    <span>文章分类</span>
                </el-menu-item>
                <el-menu-item index="/article/manage">  <!-- 五 3.3  -->
                    <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">  <!-- 五 3.3  -->
                        <el-icon>
                            <User />
                        </el-icon>
                        <span>基本资料</span>
                    </el-menu-item>
                    <el-menu-item index="/user/avatar">  <!-- 五 3.3  -->
                        <el-icon>
                            <Crop />
                        </el-icon>
                        <span>更换头像</span>
                    </el-menu-item>
                    <el-menu-item index="/user/resetPassword">  <!-- 五 3.3  -->
                        <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 ? userInfoStore.info.nickname : userInfoStore.info.usrename }}</strong></div> <!-- 八 4 {{ userInfoStore.info.nickname ? userInfoStore.info.nickname : userInfoStore.info.usrename }}-->
                <el-dropdown placement="bottom-end">
                    <span class="el-dropdown__box">
                        <el-avatar :src="userInfoStore.info.userPic ? userInfoStore.info.userPic : avatar" /> <!-- 八 4 userInfoStore.info.userPic ? userInfoStore.info.userPic : avatar-->
                        <el-icon>
                            <CaretBottom />
                        </el-icon>
                    </span>
                    <template #dropdown>
                        <el-dropdown-menu>
                            <el-dropdown-item command="profile" :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>
                    </template>
                </el-dropdown>
            </el-header>
            <!-- 中间区域 -->
            <el-main>
                <!-- 五 3.3  -->
                <!-- <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>

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

  • 基本资料

  • 更换头像

  • 重置密码

  • 退出登录

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

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

<!--  下拉菜单-->
                <!--  command: 条目被点击后会触发,在事件函数上可以声明一个参数,接收条目对应的指令 -->
                <el-dropdown placement="bottom-end" @command="handleCommand">
                    <span class="el-dropdown__box">
                        <el-avatar :src="userInfoStore.info.userPic ? userInfoStore.info.userPic : avatar" /> <!-- 八 4 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-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)
    }
}

<script setup>
import {
    Management,
    Promotion,
    UserFilled,
    User,
    Crop,
    EditPen,
    SwitchButton,
    CaretBottom
} from '@element-plus/icons-vue'
import avatar from '@/assets/default.png'
//  九 1
//dropDown条目被点击后,回调的函数
import {useRouter} from 'vue-router'

//九 2
import {ElMessage,ElMessageBox} from 'element-plus'
import { useTokenStore } from '@/stores/token.js'

// ======= 八  3  个人用户信息
import {ref} from 'vue'
//导入接口函数
import { userInfoGetService } from '@/api/user.js'
//导入pinia
import useUserInfoStore  from '@/stores/userInfo.js'

const userInfoStore = useUserInfoStore();
//获取个人信息
const getUserInfo = async ()=>{
    let result = await userInfoGetService();
    //存储pinia
    userInfoStore.info =result.data;
}
getUserInfo()
//九 2

const tokenStore = useTokenStore()
//九 1
const router = useRouter()
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">  <!-- 五 3.3  -->
                    <el-icon>
                        <Management />
                    </el-icon>
                    <span>文章分类</span>
                </el-menu-item>
                <el-menu-item index="/article/manage">  <!-- 五 3.3  -->
                    <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">  <!-- 五 3.3  -->
                        <el-icon>
                            <User />
                        </el-icon>
                        <span>基本资料</span>
                    </el-menu-item>
                    <el-menu-item index="/user/avatar">  <!-- 五 3.3  -->
                        <el-icon>
                            <Crop />
                        </el-icon>
                        <span>更换头像</span>
                    </el-menu-item>
                    <el-menu-item index="/user/resetPassword">  <!-- 五 3.3  -->
                        <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 ? userInfoStore.info.nickname : userInfoStore.info.usrename }}</strong></div> <!-- 八 4 {{ userInfoStore.info.nickname ? userInfoStore.info.nickname : userInfoStore.info.usrename }}-->
                <!--  下拉菜单-->
                <!--  command: 条目被点击后会触发,在事件函数上可以声明一个参数,接收条目对应的指令 -->
                <el-dropdown placement="bottom-end" @command="handleCommand">
                    <span class="el-dropdown__box">
                        <el-avatar :src="userInfoStore.info.userPic ? userInfoStore.info.userPic : avatar" /> <!-- 八 4 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>
                <!-- 五 3.3  -->
                <!-- <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>

viewsuserUserInfo.vue
<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中获取个人信息,替换模板数据即可

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

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

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

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

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

}

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


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

为修改按钮绑定单击事件

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

提供updateUserInfo函数

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 { ref } from 'vue'
// 十 3
import { ElMessage } from 'element-plus';
import { userInfoUpdateService } from '@/api/user.js';

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

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' }
    ]
}

// 十  3
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>
<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" @click="updateUserInfo">提交修改</el-button> <!-- 十  3  @click="updateUserInfo"-->
                    </el-form-item>
                </el-form>
            </el-col>
        </el-row>
    </el-card>
</template>

<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/userInfo.js'

const userInfoStore = useUserInfoStore()
//用户头像地址
const imgUrl=ref(userInfoStore.info.userPic)

img标签上绑定图片地址

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

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

  • action: 服务器接口路径

  • headers: 设置请求头,需要携带token

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

  • name: 上传图片的字段名称

<el-upload 
                    ref="uploadRef"
                    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="avatar" 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事件

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

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

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

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

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

}

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


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


   // 十  4 修改头像
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"> <!-- 十  4  @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
}
<script setup>
import { Plus, Upload } from '@element-plus/icons-vue'
import avatar from '@/assets/default.png'
// 十  3
import { useTokenStore } from '@/stores/token.js'
// 十 4调用接口,更新头像url
import {userAvatarUpdateService} from '@/api/user.js'
import {ElMessage} from 'element-plus'
// 十 2 
//读取用户信息
import {ref} from 'vue'
import useUserInfoStore from '@/stores/userInfo.js'

const userInfoStore = useUserInfoStore()
//用户头像地址
const imgUrl=ref(userInfoStore.info.userPic)

// 十  3
const tokenStore = useTokenStore();
//图片上传成功的回调
const uploadSuccess = (result)=>{
    //回显图片
    imgUrl.value = result.data
}

// 十 、3//获取el-upload元素
const uploadRef = ref()


// 十 4
const updateAvatar = async ()=>{
    let result = await userAvatarUpdateService(imgUrl.value)
    ElMessage.success(result.message? result.message:'修改成功')
    //更新pinia中的数据
    userInfoStore.info.userPic=imgUrl.value
}
</script>

<template>
    <el-card class="page-container">
        <template #header>
            <div class="header">
                <span>更换头像</span>
            </div>
        </template>
        <el-row>
            <el-col :span="12">
                 <!-- 
                        auto-upload:是否自动上传
                        action: 服务器接口路径
                        name: 上传的文件字段名
                        headers: 设置上传的请求头
                        on-success: 上传成功的回调函数                     
                    -->
                <el-upload 
                    ref="uploadRef"
                    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="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" @click="updateAvatar"> <!-- 十  4  @click="updateAvatar"-->
                    上传头像
                </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>
//导入request.js请求工具
import request from '@/utils/request.js'

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

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

}

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


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


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


   // 十二  重置密码
export const userUpdatePwdService=(userPwd)=>{    
    return request.patch('/user/updatePwd',userPwd)
}

 views/UserResetPassword.vue

<script setup>
import { ElMessage } from 'element-plus';
import { useTokenStore } from '@/stores/token.js'
import { ref } from 'vue'
import { userUpdatePwdService } from '@/api/user.js';
import {useRouter} from 'vue-router'
const router = useRouter()
const userPwd = ref({    
    old_pwd: '',
    new_pwd: '',
    re_pwd: '',
})

const tokenStore = useTokenStore()
const updateUserPwd = async ()=>{
    let result = await userUpdatePwdService(userPwd.value)
    // ElMessage.success(result.message? result.message:'修改成功')
     //退出登录
    //1.清空pinia中存储的token(pinia中个人信息没有存储密码信息,所以不处理)
    tokenStore.removeToken()
  

    //2.跳转到登录页面
    router.push('/login')
    ElMessage({
        type: 'success',
        message: '修改密码成功,请重新成功',
    })
}

const rules = {
    old_pwd: [
        { required: true, message: '请输入原始密码', trigger: 'blur' },
        {
            pattern: /^S{5,16}$/,
            message: '原始密码必须是5-16位的非空字符串',
            trigger: 'blur'
        }
    ],
    new_pwd: [
        { required: true, message: '请输入新密码', trigger: 'blur' },
        
        {
            pattern: /^S{5,16}$/,
            message: '新密码必须是5-16位的非空字符串',
            trigger: 'blur'
        }
    ],
    re_pwd: [
        { required: true, message: '请输入确认密码', trigger: 'blur' },
        {
            pattern: /^S{5,16}$/,
            message: '确认密码必须是5-16位的非空字符串',
            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="userPwd" :rules="rules" label-width="100px" size="large">
                    <el-form-item label="原密码" prop="old_pwd">
                        <el-input type="password" v-model="userPwd.old_pwd"></el-input>
                    </el-form-item>
                    <el-form-item label="新密码" prop="new_pwd">
                        <el-input type="password" v-model="userPwd.new_pwd"></el-input>
                    </el-form-item>
                    <el-form-item label="确认密码" prop="re_pwd">
                        <el-input type="password" v-model="userPwd.re_pwd"></el-input>
                    </el-form-item>
                    <el-form-item>
                        <el-button type="primary" @click="updateUserPwd">提交修改</el-button>
                    </el-form-item>
               
                </el-form>
            </el-col>
        </el-row>
    </el-card>
</template>

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注