黑马程序员Vue3大事件项目跟写笔记

黑马大事件项目日志

github地址

创建项目

pnpm

安装

npm install -g pnpm

创建项目

pnpm create vue

运行项目

pnpm dev

项目目录设置

在这里插入图片描述

路由配置

在router文件夹下创建一个index.js文件

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

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [],
})

export default router

两种模式,分别是hash和history模式

  • history不传递#
  • hash会传递一个#

关于参数

import.meta.env.BASE_URL

这个配置项在vite.config.js中,

能够配置一个基地址

plugins:[
    base:'/'
]

main.js中导入并使用

import router from './router'
app.use(router)

组件中使用测试

App.vue

<template>
	<el-button @click="$router.push('/home')">首页</el-button>
	<el-button @click="goList">列表页</el-button>
</template>

<script setup>
    import {useRouter} from 'vue-router'
    const router = useRouter()
    const goList = ()=>{
        router.push('/list')
    }
</script>

pinia配置

在store文件夹下创建modules文件夹,将每种需要存储的数据放在对应的js文件中,同时将pinia的初始配置写在index.js文件夹中,最后交给main.js使用

index.js

//这里是有关pinia的的配置
import { createPinia } from 'pinia'
//使用插件实现持久化存储
import persist from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(persist)
export default pinia

// import { useUserStore } from './modules/user'
// export { useUserStore }
// import { useCountStore } from './modules/count'
// export { useCountStore }
//对各个模块中的数据进行单独的导出和导入
export * from './modules/user'
export * from './modules/count'

main.js

import pinia from './stores'
app.use(pinia)

user.js

import { defineStore } from 'pinia'
import { ref } from 'vue'

// 用户模块
export const useUserStore = defineStore(
    //唯一标识
  'big-user',
  () => {
    const token = ref('') // 定义 token
    const setToken = (newToken) => (token.value = newToken) // 设置 token
    const removeToken = () => (token.value = '')
    return { token, setToken, removeToken }
  },
  {
    persist: true, // 持久化
  },
)

为了对数据进行一个持久化的处理,我们需要安装一个pinia插件

pnpm add pinia-plugin-persistedstate -D

Element-Plus的基本配置

官方文档 https://element-plus.org/zh-CN/

  • 安装
pnpm add element-plus

按需引入

  1. 安装插件
pnpm add -D unplugin-vue-components unplugin-auto-import
  1. 以下代码配置到vite或者webpack中
...
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    ...
    AutoImport({
      resolvers: [ElementPlusResolver()]
    }),
    Components({
      resolvers: [ElementPlusResolver()]
    })
  ]
})

axios请求配置

项目接口根路径 http://big-event-vue-api-t.itheima.net

接口文档https://apifox.com/apidoc/shared-26c67aee-0233-4d23-aab7-08448fdf95ff/api-93850835

新建立utils/request.js文件

首先需要安装axios

pnpm add axios

request.js

import axios from 'axios'
import { useUserStore } from '@/stores'
import { ElMessage } from 'element-plus'
import { router } from '@/router'
const baseURL = 'http://big-event-vue-api-t.itheima.net'

const instance = axios.create({
  // TODO 1. 基础地址,超时时间
  baseURL,
  timeout: 10000,
})
//请求拦截器
instance.interceptors.request.use(
  (config) => {
    // TODO 2. 携带token
    const useStore = useUserStore()
    if (useStore.token) {
      config.headers.Authorization = useStore.token
    }
    return config
  },

  (err) => Promise.reject(err),
)
//响应拦截器
instance.interceptors.response.use(
  (res) => {
    // TODO 3. 处理业务失败
    // TODO 4. 摘取核心响应数据
    // res.data数据
    if (res.data.code === 0) {
      return res
    }
    //处理业务失败,给出错误提示,抛出错误
    ElMessage.error(res.data.message || '服务异常')
    return Promise.reject(res.data)
  },
  (err) => {
    // TODO 5. 处理401错误
    //错误的特殊情况401权限不足或者token过期,则拦截登录
    if (err.response?.status === 401) {
      router.push('./login')
    }
    //错误的默认情况,给提示就好
    ElMessage.error(err.response.data.message || '服务异常')
    return Promise.reject(err)
  },
)
//导出实例
export default instance
//导出基地址
export { baseURL }

通过观看接口文档

header中需要传递Authorization

路由基本配置

  • 登录——一级路由
  • 架子——一级路由
    • 文章分类——二级路由
    • 文章管理——二级路由
    • 基本资料——二级路由
    • 更换头像——二级路由
    • 重置密码——二级路由
path文件功能组件名路由级别
/loginviews/login/LoginPage.vue登录&注册LoginPage一级路由
/views/layout/LayoutContainer.vue布局架子LayoutContainer一级路由
/article/manageviews/article/ArticleManage.vue文章管理ArticleManage二级路由
/article/channelviews/article/ArticleChannel.vue频道管理ArticleChannel二级路由
/user/profileviews/user/UserProfile.vue个人详情UserProfile二级路由
/user/avatarviews/user/UserAvatar.vue更换头像UserAvatar二级路由
/user/passwordviews/user/UserPassword.vue重置密码UserPassword二级路由

router/index.js

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

//创建路由实例
//1 .createWebHashHistoryhash模式
//2 .createWebHistoryhistory模式
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    { path: '/login', component: () => import('@/views/login/loginPage.vue') }, //登录页
    {
      path: '/',
      component: () => import('@/views/layout/layoutContainer.vue'),
      redirect: '/article/manage', //重定向到
      children: [
        {
          path: '/article/manage',
          component: () => import('@/views/article/ArticleManage.vue'),
        },
        {
          path: '/article/channel',
          component: () => import('@/views/article/ArticleChannel.vue'),
        },
        {
          path: '/user/profile',
          component: () => import('@/views/user/UserProfile.vue'),
        },
        {
          path: '/user/avatar',
          component: () => import('@/views/user/UserAvatar.vue'),
        },
        {
          path: '/user/password',
          component: () => import('@/views/user/UserPassword.vue'),
        },
      ],
    },
  ],
})

export default router

登录注册页面

element-plus表单

1. 注册登录 静态页面结构和i基本切换
	2. 注册功能(校验和注册)
	3. 登录功能(校验+登录+存储token)

安装element-plus图标库

pnpm i @element-plus/icons-vue

图标地址:https://element-plus.org/zh-CN/component/icon.html

登录注册静态页面结构

login/LoginPage.vue静态结构

<script setup>
    //引入User和Lock图标
import { User, Lock } from '@element-plus/icons-vue'
import { ref } from 'vue'
   //用来切换注册和登录页的
const isRegister = ref(true)
</script>

<template>
<!-- el-row表示的是一行,分成24分 -->
  <!-- 左右el-col表示的是列各占12分 
      :span="12"表示代表在一行中占12分
      :span="6" 表示在一行中占6分
      :offset="3"表示在一行中 ,左侧的margin的分数
      el-form :整个表单组件
      el-form-item:表单的一行(一个表单域)
      el-input 表单元素(输入框)
   -->
  <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>

注册表单验证规则

Element-plus表单文档

https://element-plus.org/zh-CN/component/form.html

校验相关

  1. <el-form>标签

    <el-form :model='ruleForm'>
    

    绑定整个form表单数据对象{xxx,xxx,xxx}

  2. <el-form>标签

    <el-form :rules='rules'>
    

    绑定整个表单rules检验规则对象{xxx,xxx,xxx}

  3. 表单元素标签

    <el-form v-model="ruleForm.xxx">
    

    绑定form的子属性

  4. <el-form-item>标签

    <el-form-item prop="xxx"></el-form-item>
    

校验规则和校验对象

1. `检验表单数据对象`,(应该和接口文档中保持一致)

注册接口文档地址:https://apifox.com/apidoc/shared-26c67aee-0233-4d23-aab7-08448fdf95ff/api-93850058

从接口文档中查看参数,需保持一致

const formModel =ref({
	username:'',
	password:'',
	repassword:''
})
  1. 校验规则

    1. 非空校验

      require:ture
      message:消息提示
      trigger:触发时机(blur,change……)
      
    2. 长度校验

      min:xxx
      max:xxx
      
    3. 正则校验

      pattern:正则表达式	
      
    4. 自定义校验

      自己写逻辑校验(校验函数)
      validaor:(rule,value,callback)
      (1) rule 当前检验规则相关信息
      (2) value 所校验的表单元素,目前的表单值
      (3) callback 回调函数
      callback() 校验成功
      callback(new Error(错误信息)) 校验失败
      

配置规则和表单代码

const formModel = ref({
  username: '',
  password: '',
  repassword: '',
})
const rules = {
  username: [
    { required: true, message: '请输入用户名', trigger: 'blur' },
    { min: 5, max: 10, message: '用户名必须是5-10位的字符', trigger: 'blur' },
  ],
  password: [
    { required: true, message: '请输入密码', trigger: 'blur' },
    { pattern: /^\S{6,15}$/, message: '密码必须是6-15位的非空字符', trigger: 'blur' },
  ],
  repassword: [
    { required: true, message: '请输入密码', trigger: 'blur' },
    { pattern: /^\S{6,15}$/, message: '密码必须是6-15位的非空字符', trigger: 'blur' },
    {
      validator: (rule, value, callback) => {
        //判断value 和当前form中收集的password是否一致
        if (value !== formModel.value.password) {
          callback(new Error('两次输入的密码不一致'))
        } else {
          callback() //校验成功需要正常回调,callback
        }
      },
      trigger: 'blur',
    },
  

注册表单元素的配置代码

 <!-- 注册相关表单 -->
<el-form
    :model="formModel"
    :rules="rules"
    ref="form"
    size="large"
    autocomplete="off"
    v-if="isRegister"
>
    <!--1注册标题-->
    <el-form-item>
      <h1>注册</h1>
    </el-form-item>
    <!--2用户名输入-->
    <el-form-item prop="username">
      <el-input
        v-model="formModel.username"
        :prefix-icon="User"
        placeholder="请输入用户名"
      ></el-input> 
    </el-form-item>
    <!--3密码输入-->
    <el-form-item prop="password">
      <el-input
        v-model="formModel.password"
        :prefix-icon="Lock"
        type="password"
        placeholder="请输入密码"
      >
      </el-input>
    </el-form-item>
    <!--4确认密码输入-->
    <el-form-item prop="repassword">
      <el-input
        v-model="formModel.repassword"
        :prefix-icon="Lock"
        type="password"
        placeholder="请输入再次密码"
      ></el-input>
    </el-form-item>
    <!--5注册-->
    <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>

界面提示展示

在这里插入图片描述

封装api实现注册登录功能

  1. 首先注册前的预校验

需要绑定表单数据

const form = ref()
<el-form ref="form">
  1. validate

    对整个表单的内容进行验证。 接收一个回调函数,或返回 Promise

​ 文档链接

​ https://element-plus.org/zh-CN/component/form.html


查看接口文档

注册

在这里插入图片描述

登录

在这里插入图片描述

api/user.js

import request from '@utils/request'

//注册接口
export const userRegisterService =({username,password,repassword})=>{
    return request.post('api/reg',{
        username,
        password,
        repassword
    })
}
//登录接口
export const userLoginService = ({
    username,password})=>{
    return request.post('api/login',{
        username,
        password
    })
}

细节:命名需要见名知意,以后代码很多很多的时候也方便理解和查看

LoginPage.vue

为注册和登录按钮添加事件

  • register
  • login
<el-button 
@click="login" 
class="button"
type="primary" 
auto-insert-space>
登录
</el-button>
<!------------------->
<el-button 
@click="register" 
class="button"
type="primary" 
auto-insert-space>
注册
</el-button>

Loginpage.vue

import { userRegisterService, userLoginService } from '@/api/user'
//导入用户模块仓库(为了存储token)
import {userUserStore} from '@/stores'
//导入路由 (首页跳转)
import {useRouter} from 'vue-router'
//注册校验和提交
const register = async () =>{
    await form.value.validate()
    await userRegisterService(formModel.value)
    ElMessage.success('注册成功')
    //切换到登录
    isRegister.value = false
}
const userStore = useUserStore()
const router = useRouter()
//登录校验和提交
const login = async () =>{
    await form.value.validate()
    const res= 
  	await userLoginService(formModel.value)
    //登录后存token
    userStore.setToken(res.data.token)
    //登录后消息提示
    ElMessage.success('登录成功')
    //登录成功后跳转到首页
    router.push('/')
}

此时还有bug,当我们在登录界面输入信息后,此时切换到注册页面,此时表单中的数据还存在,需要将此时的数据清空来获得更好的体验

可以使用watch监视isRegister的变化

import {watch} from 'vue'//需要导入watch
watch(isRegister,()=>{
    formModel.value={
        username:'',
        password:'',
        repassword:''
    }
})

注意:需要将注册和登录的表单数据都绑定v-model和prop

此时的LoginPage.vue代码

<script setup>
import { User, Lock } from '@element-plus/icons-vue'
import { ref, watch } from 'vue'
import { userRegisterService, userLoginService } from '@/api/user'
import { useUserStore } from '@/stores'
import { useRouter } from 'vue-router'
const isRegister = ref(false)
const form = ref()
const formModel = ref({
  username: '',
  password: '',
  repassword: '',
})
const rules = {
  username: [
    { required: true, message: '请输入用户名', trigger: 'blur' },
    { min: 5, max: 10, message: '用户名必须是5-10位的字符', trigger: 'blur' },
  ],
  password: [
    { required: true, message: '请输入密码', trigger: 'blur' },
    { pattern: /^\S{6,15}$/, message: '密码必须是6-15位的非空字符', trigger: 'blur' },
  ],
  repassword: [
    { required: true, message: '请输入密码', trigger: 'blur' },
    { pattern: /^\S{6,15}$/, message: '密码必须是6-15位的非空字符', trigger: 'blur' },
    {
      validator: (rule, value, callback) => {
        //判断value 和当前form中收集的password是否一致
        if (value !== formModel.value.password) {
          callback(new Error('两次输入的密码不一致'))
        } else {
          callback() //校验成功需要正常回调,callback
        }
      },
      trigger: 'blur',
    },
  ],
}
//validate对整个表单的内容进行验证。 接收一个回调函数,或返回 Promise。
const register = async () => {
  //注册成功之前先进行校验 校验成功->请求 校验失败->自动提示
  await form.value.validate()
  // console.log('开始注册请求')
  await userRegisterService(formModel.value)
  ElMessage.success('注册成功')
  //切换到登录
  isRegister.value = false
}
//导入store中的user仓库
//设置我们的token
const userStore = useUserStore()
const router = useRouter()
//登录之前进行一个预校验
const login = async () => {
  await form.value.validate()
  const res = await userLoginService(formModel.value)
  userStore.setToken(res.data.token)
  // console.log('开始登录', res)
  ElMessage.success('登录成功')
  //跳转到首页
  router.push('/')
}

//切换的时候需要重置表单的内容
watch(isRegister, () => {
  formModel.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
        :model="formModel"
        :rules="rules"
        ref="form"
        size="large"
        autocomplete="off"
        v-if="isRegister"
      >
        <el-form-item>
          <h1>注册</h1>
        </el-form-item>
        <el-form-item prop="username">
          <el-input
            v-model="formModel.username"
            :prefix-icon="User"
            placeholder="请输入用户名"
          ></el-input>
        </el-form-item>
        <el-form-item prop="password">
          <el-input
            v-model="formModel.password"
            :prefix-icon="Lock"
            type="password"
            placeholder="请输入密码"
          >
          </el-input>
        </el-form-item>
        <el-form-item prop="repassword">
          <el-input
            v-model="formModel.repassword"
            :prefix-icon="Lock"
            type="password"
            placeholder="请输入再次密码"
          ></el-input>
        </el-form-item>
        <el-form-item>
          <el-button @click="register" 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 :model="formModel" :rules="rules" ref="form" size="large" autocomplete="off" v-else>
        <el-form-item>
          <h1>登录</h1>
        </el-form-item>
        <el-form-item prop="username">
          <el-input
            v-model="formModel.username"
            :prefix-icon="User"
            placeholder="请输入用户名"
          ></el-input>
        </el-form-item>
        <el-form-item prop="password">
          <el-input
            v-model="formModel.password"
            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 @click="login" 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>
………………样式省略

注册界面点击注册后的再次提示(登录界面页相同)

在这里插入图片描述

登录成功后跳转到主页

在这里插入图片描述

首页layout架子

首先写好架子基本结构,这里还是使用的Element-plus组件来进行搭建

官方链接(布局容器)

https://element-plus.org/zh-CN/component/container.html

LayoutContainer.vue静态结构

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

<template>
  <!-- 
    el-menu 整个菜单组件 
    :default-active="$route.path" 配置默认高亮的菜单项
    router            router选项开启,el-menu-item 的index就是要跳转的路径
    el-sub-menu 多级菜单
  -->
  <!-- 顶级容器 -->
  <el-container class="layout-container">
    <!-- 侧边栏 -->
    <el-aside width="200px">
      <div class="el-aside__logo"></div>
      <el-menu
        active-text-color="#ffd04b"
        background-color="#232323"
        :default-active="$route.path"
        text-color="#fff"
        router
      >
        <el-menu-item index="/article/channel">
          <el-icon><Management /></el-icon>
          <span>文章分类</span>
        </el-menu-item>
        <el-menu-item index="/article/manage">
          <el-icon><Promotion /></el-icon>
          <span>文章管理</span>
        </el-menu-item>
        <!-- 多级菜单 -->
        <el-sub-menu index="/user">
          <!-- 表单的标题具名插槽 -->
          <template #title>
            <el-icon><UserFilled /></el-icon>
            <span>个人中心</span>
          </template>
          <!-- 展开的内容 默认插槽 -->
          <el-menu-item index="/user/profile">
            <el-icon><User /></el-icon>
            <span>基本资料</span>
          </el-menu-item>
          <el-menu-item index="/user/avatar">
            <el-icon><Crop /></el-icon>
            <span>更换头像</span>
          </el-menu-item>
          <el-menu-item index="/user/password">
            <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>
        <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-container 容器组件

  • el-aside 侧边栏

  • el-menu 菜单配置

    • <el-menu>
      active-text-color="#ffd04b"
      background-color="#232323"
      :default-active="$route.path"
      text-color="fff"
      router
      </el-menu>
      
  • el-menu-item 菜单中的选项

  • el-sub-menu 多级菜单

  • el-icon 图标

:default-active="$route.path" // 配置默认的高亮菜单项

router //router选项开启el-menu-item的index就是要跳转的路径

active-text-color="#ffd04b"//选中的选项颜色

登录拦截请求

在非登录界面中,我们没有token不能直接跳转

需要判断token是否存在,此时需要用到路由守卫,vue-3的路由守卫和vue-2有所不同

官方链接https://router.vuejs.org/zh/guide/advanced/navigation-guards.html

官方文档示例

 router.beforeEach(async (to, from) => {
   if (
     // 检查用户是否已登录
     !isAuthenticated &&
     // ❗️ 避免无限重定向
     to.name !== 'Login'
   ) {
     // 将用户重定向到登录页面
     return { name: 'Login' }
   }
 })

登录拦截请求 =>vue-3是直接放行的,根据返回值决定是放行还是拦截

  • 返回值
    • undefined/true 直接放行
    • false 拦截from的地址页面
    • 具体路径或者路径对象,拦截到对应的地址 ‘/login’ ‘{name:‘login’}’

router/index.js拦截配置

//导入pinia 的user仓库判断token
import {useUserStore} from '@/stores/index'

router.beforeEach((to)=>{
    const userStore=useUserStore()
    //如果没有token且访问的是非登录页,拦截到登录
    //其他情况正常放行
    if(!userStore.token&&to.path!=='/login')return '/login'
    
    return true;//不返回默认返回值为undefined也可以
})

页面展示

在这里插入图片描述

首页用户基本信息获取和退出登录功能

接口文档用户信息相关

在这里插入图片描述

token已经在请求拦截器中进行配置了,在配置用户信息接口的时候就不用传递了

api/user.js

//获取用户信息接口
export const UserGetInfoService=()=>{
    return request.get('/my/userinfo')
}

需要将用户信息进行 持久化存储

store/modules/user.js

(此处只是展示这一步的主要代码)

//头部导入此api
import { UserGetInfoService } from '@/api/user'

export const useUserStore = defineStore(()=>{
    ……………………其他存储的信息
    //提供用户对象和getUser方法
    const user =ref({})//设置为一空对象
    const getUser =async()=>{
        const res = await UserGetInfoService()
        user.value=res.data.data
    }
    //设置用户信息,后续退出即可传入一个空对象清空
    const setUser=(obj)=>{
        user.value=obj//设置为此对象
    }
    return {
        …………其他信息
        //导出
        user,getUser,setUser
    },
    {persist:true//持久化}
})

layoutContainer.vue的头像和切换结构分析

element-plus官方文档

https://element-plus.org/zh-CN/component/dropdown.html

<!-- 添加command事件 -->
        <el-dropdown placement="bottom-end" @command="handleCommand">
          <!-- 展示给用户默认看到的 -->
          <span class="el-dropdown__box">
            <el-avatar :src="userStore.user.user_pic || 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>

command属性中的参数是我们指定的跳转的路由路径,通过点击下拉菜单来跳转到指定页面进行相关操作

在这里插入图片描述

前面为整个下拉菜单添加了@command="handleCommand"事件

LayoutContainer.vue

(此处只展示这一部分的主要代码)

//引入生命周期钩子
import {onMounted} from 'vue'
//引入用户信息仓库
import {useUserStore} from '@/stores'
//引入路由
import {useRouter} from 'vue-router'

const router = useRouter()
const userStore = useUserStore()
//挂载完毕导入用户信息
onMounted(()=>{
    userStore.getUser()
})
//触发事件
const handleCommand =async(key)=>{
    if(key === 'logout'){
        //退出操作
        //退出前的询问
        await ElMessageBox.confirm(
            '确认要退出吗?',
            '温馨提示',
            {
                confirmButtonText: 'OK',
                cancelButtonText: 'Cancel',
                type: 'warning',
            })
        //清空本地的数据
        userStore.removeToken()
        userStore.setUser({})//设置为一个空对象
        //跳转到登录页面
        router.push('/login')
    }else{
        //跳转操作
        router.push(`/user/${key}`)//模板字符串
    }
}

此处的key指的是下拉菜单中绑定的command

<el-dropdown-item 
command="profile" 
:icon="User">基本资料
</el-dropdown-item>

即我们路由中配置的路径名,不是随便乱写的

首页用户昵称,有昵称名则传入nickname,没有则展示用户名

<strong>
{{ userStore.user.nickname || userStore.user.username }}
</strong>

首页用户头像,有头像则传入用户头像,没有头像则传入默认头像

<el-avatar 
:src="userStore.user.user_pic || avatar" />

提示展示

在这里插入图片描述

首页 Pinia 用户和token信息

在这里插入图片描述

退出后则token和user信息为空

在这里插入图片描述

文章分类模块

基本架子

views/aticle/ArticleChannel.vue

<script setup></script>

<template>
  <page-container title="文章分类">
    <template #extra>
      <el-button type="primary">添加分类</el-button>
    </template>
  </page-container>
</template>

<style lang="scss" scoped></style>

views/aticle/ArticleManage.vue

<script setup></script>

<template>
  <page-container title="文章管理">
    <template #extra>
      <el-button type="primary">添加文章</el-button>
    </template>
  </page-container>
</template>

<style lang="scss" scoped></style>

新建components/PageContainer.vue,因为文章管理和文章分类管理的目录的结构类似,可以进行复用

<script setup>
defineProps({
  title: {
    required: true,
    type: String,
  },
})
</script>

<template>
  <el-card class="page-container">
    <!-- 具名插槽定义按钮 -->
    <template #header>
      <!-- 头部 -->
      <div class="header">
        <span>{{ title }}</span>
        <div class="extra">
          <!-- 额外的按钮 -->
          <slot name="extra"></slot>
        </div>
      </div>
    </template>
    <!-- 页面内容,默认插槽 -->
    <slot></slot>
  </el-card>
</template>

<style lang="scss" scoped>
.page-container {
  min-height: 100%;
  box-sizing: border-box;
  .header {
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
}
</style>

通过父传子,使用prop将title传过去

Element-plus的Card介绍

https://element-plus.org/zh-CN/component/card.html

这里使用了具名插槽和默认插槽

笔者学到这里有点忘记插槽怎么使用了,准备回去再复习一下插槽的用法,然后补上笔记

页面展示

在这里插入图片描述

文章分类的渲染

需要获取文章分类的数据,我们需要观看接口,然后进行文章相关数据的api的创建

接口文档链接

https://apifox.com/apidoc/shared-26c67aee-0233-4d23-aab7-08448fdf95ff/api-93850053

api/article.js

import request from '@utils/request'

//获取文章分类
export const artGetChannelService=()=>{
    return request.get('/mycate/list')
}

ArticleChannel.vue中获取分类列表

import {artGetChannelService} from '@/api/article'
//初始化文章列表为一个空数组
const channelList =ref([])
//获取分类列表
const getChannelList = async ()=>{
    const res = await artGetChannelService()
    channelList.value=res.data.data
}
//调用
getChannelList()

我们需要在文章分类页生成一个表格,此时用到了Element-plus的表格table相关的组件

官方文档https://element-plus.org/zh-CN/component/table.html

此处列举重要的操作解释

<!-- type属性是用来显示序号 -->
<el-table-column type="index" width="50" />
<!-- prop属性是用来绑定数据,label属性是用来显示表头 -->
<el-table-column 
prop="cate_name" 
label="分类名称"></el-table-column>
<el-table-column
prop="cate_alias" 
label="分类别名"></el-table-column>

注意此处的prop接受的参数是上面getChannelList请求回来的数据信息参数名

lable则是这一列的标题

删除编辑操作

<el-table-column label="操作" width="150">
<!-- 作用域插槽 row就是channel的每一项即item,$index是下标-->
    <template #default="{ row, $index }">
    <el-button
    :icon="Edit"
    circle
    type="primary"
    plain
    @click="onEditChannel(row, $index)"
    ></el-button>
    <el-button
    :icon="Delete"
    circle
    type="danger"
    plain
    @click="onDelChannel(row, $index)"
    ></el-button>
    </template>
</el-table-column>

其中row和$index表示的每一行数据的信息参数和其序号

为了优化用户体验我们还需要添加上一个加载效果,

在请求到数据之前我们需要进行一个加载效果,在请求到数据后加载效果消失,这个操作也需要用到ELement-plus提供的指令v-lodading

//获取分类列表
const getChannelList = async () => {
  //发请求之前loading为true
  loading.value = true
  const res = await artGetChannelService()
  channelList.value = res.data.data
  console.log(channelList.value)
  // channelList.value = [] //空状态显示
  //请求结束loading为false
  loading.value = false
}

同时我们希望在没有任何文章信息的时候页面显示空的提示,也需要用到Element-plus组件empty

官方文档https://element-plus.org/zh-CN/component/empty.html

这一步ArrticleChannel.vue最终的代码

<script setup>
import { ref } from 'vue'
import { artGetChannelService } from '@/api/article'
//导入图标
import { Edit, Delete } from '@element-plus/icons-vue'
//发送请求

const channelList = ref([])
//加载
const loading = ref(false)
//获取分类列表
const getChannelList = async () => {
  //发请求之前loading为true
  loading.value = true
  const res = await artGetChannelService()
  channelList.value = res.data.data
  console.log(channelList.value)
  // channelList.value = [] //空状态显示
  //请求结束loading为false
  loading.value = false
}
//调用
getChannelList()
/**
 *
 * @param row 每一行的数据
 * @param $index 每一行的下标
 */
const onEditChannel = (row, $index) => {
  console.log(row, $index)
}
const onDelChannel = (row, $index) => {
  console.log(row, $index)
}
</script>

<template>
  <page-container title="文章分类">
    <template #extra>
      <el-button type="primary">添加分类</el-button>
    </template>
    <el-table v-loading="loading" :data="channelList" style="width: 100%">
      <!-- type属性是用来显示序号 -->
      <el-table-column type="index" width="50" />
      <!-- prop属性是用来绑定数据,label属性是用来显示表头 -->
      <el-table-column label="序号" width="100"></el-table-column>
      <el-table-column prop="cate_name" label="分类名称"></el-table-column>
      <el-table-column prop="cate_alias" label="分类别名"></el-table-column>
      <el-table-column label="操作" width="150">
        <!-- 作用域插槽 row就是channel的每一项即item,$index是下标-->
        <template #default="{ row, $index }">
          <el-button
            :icon="Edit"
            circle
            type="primary"
            plain
            @click="onEditChannel(row, $index)"
          ></el-button>
          <el-button
            :icon="Delete"
            circle
            type="danger"
            plain
            @click="onDelChannel(row, $index)"
          ></el-button>
        </template>
      </el-table-column>
      <!-- 作用域插槽 表格没有数据的时候显示该信息 -->
      <template #empty>
        <el-empty description="没有数据" />
      </template>
    </el-table>
  </page-container>
</template>

<style lang="scss" scoped></style>

页面显示效果

在这里插入图片描述

为空的数据显示

在这里插入图片描述

加载动画

在这里插入图片描述

编辑和添加弹层显示搭建

Element-plus对话框介绍

https://element-plus.org/zh-CN/component/dialog.html#dialog-%E5%AF%B9%E8%AF%9D%E6%A1%86

介于添加文章编辑都需要弹出对话框,所以我们可以在gai目录下建立一个componets文件夹,在该文件中新建立文件ChannelEdit.vue

这样也便于管理和页面层次,将对话框组件写到这里

views/article/components/ChannelEdit.vue

<template>
  <!-- 弹层部分 -->
  <el-dialog v-model="dialogVisible" title="添加弹层" width="500">
    <span>我是内容部分(表单渲染)</span>
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="dialogVisible = false">取消</el-button>
        <el-button type="primary" @click="dialogVisible = false"> 确认 </el-button>
      </div>
    </template>
  </el-dialog>
</template>

span便签内是我们的内容区域,后续需要将表单元素放在里面,以进行一个编辑和添加文章分类

在这个组件内,我们可以向外暴露一个方法open,基于open传来的参数,区分是进行编辑还是添加,

  • open({}) 添加功能
    • open({id:xxx,name:xxxx}) 编辑功能(表单渲染,)
    • open调用后会打开弹层

views/article/components/ChannelEdit.vue

<script setup>
	import {ref} from 'vue'
    //弹窗
    const dialogVisible = ref(false)
    //方法
    const open=(row)=>{
        //打开弹层
        dialogVisible.value=true
    }
	//向外暴露这个方法
    defineExpose({
        open,
    })
</script>

open函数中传入的row指的是文章分类的对象

- cate_name 名字
- cate_alias 类别

ArticleChannel.vue中导入该组件

<script setup>
    //引入组件ChannelEdit
	import ChannelEdit from '@/views/article/components/ChannelEdit.vue'
    /*其他代码*/
	    
    //编辑显示弹层
    const onEditChannel =(row)>{
        dialog.value.open(row)
    }
    //添加显示弹层
    const onAddChannel =()=>{
        dialog.value.open({})
    }
</script>
<template>
    <page-container title="文章分类">
        <!--编辑按钮-->
		<template #extra>
            ………………
		</template>    
		<!--表格部分-->
		<el-table>
            ………………
		</el-table>
		<!--弹层组件部分-->
    	<ChannelEdit ref="dialog"></ChannelEdit>
    </page-container>
</template>

对组件ChannelEdit添加了一个ref,通过ref找到该组件,能够方便的调用暴露出来的open函数

(写到这里又开始懵了,前面学习的ref和defineExpose都已经忘记了,还需要继续复习)

关于上一部分的插槽问题,这里画图解释一下

在这里插入图片描述

弹层数据的添加和编辑api

在这里插入图片描述

article.js中添加这两个接口


//添加文章分类
export const artAddChannelService = (data) => {
  return request.post('/my/cate/add', data)
}

//更新文章分类
export const artEditChannelService = (data) => {
  return request.put('/my/cate/info', data)
}

在弹层中添加内容部分添加一个表单,我们需要对这个表单填写进行验证、更新列表内容和数据提交

操作方式和8.2节一样

ChannelEdit.vue表单

<!-- 表单主要内容部分 -->
<el-form
      ref="fromRef"
      :model="formModel"
      :rules="rules"
      label-width="100px"
      style="padding-right: 30px"
>
	<el-form-item label="分类名称" prop="cate_name">
    	<el-input v-model="formModel.cate_name" placeholder="请输入分类名称"></el-input>
  </el-form-item>
  <el-form-item label="分类别名" prop="cate_alias">
        <el-input v-model="formModel.cate_alias" placeholder="请输入分类别名"></el-input>
      </el-form-item>
</el-form>

open方法的更新

const open =(row)=>{
    dialogVisible.value=true
    formModel.value={...row}
     //将传过来的row数据绑定到表单上,这里使用了展开运算符,将row的所有属性都赋值给formModel
}

表单数据的声明

//导入编辑和添加的服务
import { artEditChannelService, artAddChannelService } from '@/api/article'

//绑定到表单上
const formRef = ref()
//这里声明form中的数据,是从接口文档中看到的命名
const formModel = ref({
    cate_name:'',
    cate_alias:''
})
//表单校验规则
const rules = {
  cate_name: [
    { required: true, message: '请输入分类名称', trigger: 'blur' },
    { pattern: /^\S{1,10}$/, message: '分类名必须是1-10位的非空字符', trigger: 'blur' },
  ],
  cate_alias: [
    { required: true, message: '请输入分类别名', trigger: 'blur' },
    { pattern: /^[a-zA-Z0-9]{1,15}$/, message: '分类名必须是1-15位的字母或数字', trigger: 'blur' },
  ],
}

//设置一个触发事件
const emit = defineEmits(['success'])

//为确认按钮添加提交事件
const onSubmit= async()=>{
    //表单提交预校验
    await formRef.value.validate()
    //确认是添加还是编辑事件
    //确认是否存在id
    const idEdit=formModel.value.id?true:false;
    if(isEdit){
        await artEditChannelService(formModel.value)
        ElMessage.success('编辑成功')
    }
    else{
        await artAddChannelService(formModel.value)
        ElMessage.success('编辑成功')
    }
    //关闭弹层
    dialogVisible.value.false
    emit('success')//触发父亲组件的emit事件
}

ArticleChannel.vue中为弹层注册success事件

    <!-- 弹层部分 -->
<ChannelEdit ref="dialog" @success="onSuccess"></ChannelEdit>

触发事件

//通知刷新
const onSuccess = () => {
  getChannelList()
}

做这一步是为了让添加或者更新数据后,页面进行一个从新的渲染,不然数据改变了,但是页面中仍然显示的是之前的数据。


预校验展示

在这里插入图片描述

新增分类

在这里插入图片描述

此时加载的数据

在这里插入图片描述

编辑操作数学

在这里插入图片描述

由于之前是通过v-model双向绑定的,此时打开后表单元素中即使此元素的信息

删除文章分类

查看接口文档

在这里插入图片描述

这里是查询参数

api/article.js配置

//删除文章分类
export const artDeleteChannelService = (id) => {
  return request.delete('/my/cate/del', {
    params: {
      id,
    },
  })
}

ArticleChannel.vue里面删除功能添加

const onDelChannel = async(row)=>{
    //删除之前的确认框
    await ElMessageBox.confirm(
        '确认删除该分类吗?',
        '温馨提示',
        type:'warning',
        confirmButtonText:'确定',
        cancelButtonText:'取消'
  	)
    //发送删除请求
    await artDeleteChannelService(row.id)
    //成功提示
    ElMessage.success('删除成功')
    //删除成功从新渲染
    getChannelList()
}

删除提示

在这里插入图片描述

文章管理模块

静态页面搭建

ArticleManage.vue静态页面展示

<script setup>
import { ref } from 'vue'
import { Delete, Edit } from '@element-plus/icons-vue'
// 假数据
const articleList = ref([
  {
    id: 5961,
    title: '新的文章啊',
    pub_date: '2022-07-10 14:53:52.604',
    state: '已发布',
    cate_name: '体育',
  },
  {
    id: 5962,
    title: '新的文章啊',
    pub_date: '2022-07-10 14:54:30.904',
    state: '草稿',
    cate_name: '体育',
  },
])

//编辑
const onEditArticle = (row) => {
  console.log(row)
}

//删除
const onDeleteArticle = (row) => {
  console.log(row)
}
</script>

<template>
  <page-container title="文章管理">
    <template #extra>
      <el-button type="primary">添加文章</el-button>
    </template>
    <!-- 表单区域 -->
    <el-form inline>
      <el-form-item label="文章分类:">
        <el-select>
          <el-option label="新闻" value="111"></el-option>
          <el-option label="体育" value="222"></el-option>
        </el-select>
      </el-form-item>
      <el-form-item label="发布状态:">
        <el-select>
          <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="articleList">
      <el-table-column label="文章标题" prop="title">
        <template #default="{ row }">
          <el-link type="primary" :underline="false">{{ row.title }}</el-link>
        </template>
      </el-table-column>
      <el-table-column label="分类" prop="cate_name"></el-table-column>
      <el-table-column label="发表时间" prop="pub_date"></el-table-column>
      <el-table-column label="状态" prop="state"></el-table-column>
      <!-- 利用作用域插槽row可以获取当前行的数据 可以理解为v-for中item -->
      <el-table-column label="操作">
        <template #default="{ row }">
          <el-button
            circle
            plan
            :icon="Edit"
            type="primary"
            @click="onEditArticle(row)"
          ></el-button>
          <el-button
            circle
            plan
            :icon="Delete"
            type="danger"
            @click="onDeleteArticle(row)"
          ></el-button>
        </template>
      </el-table-column>
    </el-table>
  </page-container>
</template>
<style lang="scss" scoped></style>

在这里插入图片描述

关键解释

inline能够让以下表单元素一排排列

<el-form inline></el-form>

label是展示出来给用户看的

<el-form-item label="文章分类:"></el-form-item>

value是返回给后台的数据

<el-option value='111'></el-option>

el-link是让文章标题能够链接显示,通过作用域插槽传入数据

<template #default="{ row }">
  <el-link 
   type="primary" 
   :underline="false">
  {{ row.title }}
    </el-link>
</template>

中英文切换

Element-plus默认是使用的英文版本,我们可以导入中文包,让文字变为中文显示

官方文档

https://element-plus.org/zh-CN/component/config-provider.html

App.vue

<script setup>
import zh from 'element-plus/es/locale/lang/zh-cn.mjs'
</script>

<template>
  <!-- 国际化处理 -->
  <!-- 配置成中文 -->
  <el-config-provider :locale="zh">
    <router-view />
  </el-config-provider>
</template>

分类管理下拉菜单数据绑定

在文章管理、和添加文章都使用到了这个下拉菜单,所以可以封装成一个单独的组件来完成

article/components/ChannelSelect.vue

<script setup>
//导入获取文章列表api
import { artGetChannelService } from '@/api/article'
import { ref } from 'vue'

defineProps({
  modeValue: {
    type: [String, Number],
  },
})
    //定义触发事件
const emit = defineEmits(['update:modeValue'])

const channelList = ref([])
const getChannelList = async () => {
  const res = await artGetChannelService()
  channelList.value = res.data.data
  // console.log(channelList.value)
}
//进入页面就触发

getChannelList()
</script>
<template>
  <!-- 将下拉菜单封装成组件 -->
  <el-select 
 :modeValu="modeValue" @updata:modelValue="emit('update:modeValue', $event)">
    <el-option
      v-for="channel in channelList"
      :label="channel.cate_name"
      :value="channel.id"
      :key="channel.id"
    ></el-option>
  </el-select>
</template>

<style></style>

接口文档

在这里插入图片描述

ArticleManage.vue中定义一个params对象封装

const params =ref({
    pagenum:1,
    pagesize:5,
    cate_id:'',
    state:''
})
//导入下拉组件
import ChannelSelect from './components/ChannelSelect.vue'

…………

<ChannelSelect v-model="params.cate_id"></ChannelSelect>

vue3的v-model和vue2的有所区别

  • v-model 是:value 和@input 的简写vue2
  • v-model 是:modeValue和@update:modeValue的简写 vue3

为了实现这里下来框中的数据是我们在文章分类中添加的分类数据,

ChannelSelect.vue

//接受父组件传过来的数据
defineProp({
    modeValue:{
        type:[String,Number],        
    }
})
//定义触发事件
const emit = defineEmits(['update:modeValue'])

//定义一个空数组,用来渲染下拉列表中的数据 
const channelList = ref([])
const getChannelList =async()=>{
    const res = await artGetChannelService()
    channelList.value=res.data.data
}
//进入页面则触发渲染列表
getChannelList()

利用v-for渲染下拉列表

<template>
    <el-select :modeValue='modeValue'
		@updata:modeValue=
       "emit('update:modeValue',$event)">
    	<!--选项渲染-->
    	<el-option
           v-for='channel in channelList'
           :label='channel.cate_name'
           :value='channel.id'
           :key='channel.id'
           >
        </el-option>
    </el-select>
</template>

在这里插入图片描述


tips:学到这里又懵逼了,好像Vue-3的数据传递我又忘记了,这里需要回去再复习一下了。

文章管理数据动态渲染

查看接口文档https://apifox.com/apidoc/shared-26c67aee-0233-4d23-aab7-08448fdf95ff/api-93850788

在这里插入图片描述

前面两个参数是用来分页操作的,后两个参数用来渲染文章数据

在这里插入图片描述

我们需要使用到的数据有data和total

api/article.js

//文章:获取文章列表
export const artGetArticleService=(params)=>{
    return request.get('/my/article/list',{
        params
    })
}

ArticleManage.vue

<script setup>
//导入文章列表api
import {artGetArticleService} from "@/api/article"
const articleList =ref([])//文章列表
const total=ref(0)//总条数

//获取文章列表
const getArticleList-async()=>{
    const res =await artGetArticleService(params,value)
    articleList.value =res.data.data
    total.value=res.data.total
}
//进入页面立即调用
getArticleList()
</script>

在这里插入图片描述

我们发现此时时间需要进行一个格式化,Element-plus内置类dayjs,

通过dayjs来格式化时间

utils/format.js

import { dayjs } from 'element-plus'

export const formatTime = (time) => dayjs(time).format('YYYY年MM月DD日')

ArticleManage.vue

<script setup>
//导入格式化方法
import { formatTime } from '@/utils/format'
</script>


<template>
<!--在发布时间这一行-->
<el-table-column label="发表时间" prop="pub_date">
    <template #default="{ row }">
      {{ formatTime(row.pub_date) }}
    </template>
</el-table-column>
</template>

通过默认插槽格式化文章发布时间,这里格式化后的数据会渲染到界面上

在这里插入图片描述

日期变成我们想要的样子了

添加页码切换

Element-plus官方文档

https://element-plus.org/zh-CN/component/pagination.html

ArticleManage.vue分页区域

<template>
	<el-pagination
      v-model:current-page="params.pagenum"
      v-model:page-size="params.pagesize"
      :page-sizes="[2, 3, 5, 10]"
      :background="true"
      layout="jumper,total, sizes, prev, pager, next "
      :total="total"
      @size-change="onSizeChange"
      @current-change="onCurrentChange"
      style="margin-top: 20px; display: flex; justify-content: flex-end"
    />
</template>
  • current-page 绑定的当前页
  • page-size 表示的当前页的容量
  • page-sizes 表示用户能够选择的每页存放的数据数量
  • layout 表示用户能够进行的操作
  • total 表示数据总数
  • @size-change和current-change,能够检测到当前页码和当前每一页的条数

ArticleManage.vue

<script setup>
    //处理分页逻辑
    const onSizeChange = size=>{
        //console.log('当前每一页的条数',size)
        //只要是每一页条数变化了,原本正在访问的当前页的意义就不大了,数据已经不再原来那一页了
        //从新的第一页渲染即可
        params.value.pagenum=1
        params.value.pagesize=size
        //从新渲染
        getArticleList()
    }
    const onCurrentChange=page=>{
        //console.log('页码变化了',page)
        //更新当前页,基于最新的当前页渲染
        params.value.pagenum=page
        //从新渲染
        getArticleList()
    }
</script>    
    

添加分页切换的loading效果

<script setup>
	//loading状态
	const loading ref(false)//默认为false
    const getArticleList = async () => {
  	//loading状态
  	loading.value = true //开始loading
  	const res = await 	artGetArticleService(params.value)
  	articleList.value = res.data.data
  	total.value = res.data.total
  	//请求介乎
  	loading.value = false //结束loading
}
</script>

<template>
	<!--为表格添加loading-->
<el-table :data="articleList" v-loading="loading">
</el-table>
</template>

搜索和重置添加事件

<template>
	<!--首先为表单搜索和重置按钮添加事件-->
<el-form-item>
    <el-button type="primary" @click="onSearch">搜索</el-button>
    <el-button @click="onReset">重置</el-button>
</el-form-item>
</template>
<script setup>
    //搜索
    const onSearch =()=>{
        //搜索就是按照最新的条件进行检索,从第一页开始展示
        //重置页码
        params.value.pagenum=1
        getArticleList()
    }
    const onReset = () => {
  //重置
	//就是将筛选条件清空从新检索,从第一页开始展示
  params.value.pagenum = 1
  params.value.cate_id = ''
  params.value.state = ''
  getArticleList()
}
</script>

抽屉盒子

将抽屉封装成一个组件,设置一个变量来让抽屉显示和消失

views/article/components/ArticleEdit.vue

<script setup>
import { ref } from 'vue'
//控制抽题显示隐藏
const visibleDrawer = ref(false)

//组件对外暴露一个方法 open 基于open传来的参数,区分添加还是编辑
//open({})=>表单无需渲染,说明添加
//open({id:1,name:'xxx',…………})=>表单渲染,说明编辑
//open调用后会打开抽屉
const open = (row) => {
  //显示抽屉
  visibleDrawer.value = true
  console.log(row)
}
//暴露方法
defineExpose({
  open,
})
</script>

<template>
  <!--  抽屉-->
  <el-drawer 
v-model="visibleDrawer" 
title="大标题" 
direction="rtl" 
size="50%">
    <span>hi</span>
  </el-drawer>
</template>

解释

  • v-model=“visibleDrawer” 控制抽屉的显示隐藏
  • title 抽屉的标题
  • direction 控制抽屉的出现方向
  • size 抽屉显示的范围

显示效果

在这里插入图片描述

ArticleManage.vue

<script setup>
    //导入抽屉组件
    import ArticleEdit from './components/ArticleEdit.vue'
    
    const articleEditRef =ref()
    //添加文章
    const onAddArticle =()=>{
        articleEditRef.value.open({})
    }
    //编辑文章
    const onEditArticle =row=>{
        articleEditRef.valu.open(row)
    }
</script>
<template>
	<!--抽屉区域-->
	<ArticleEdit ref="articleEditRef">			</ArticleEdit>
</template>

在这里插入图片描述

点击编辑和添加分别能获得一个对象,通过判断传入的是否是空对象,来判断是添加还是编辑操作

抽屉组件封装内容

查看接口文档中发布文章

https://apifox.com/apidoc/shared-26c67aee-0233-4d23-aab7-08448fdf95ff/api-93850059

在这里插入图片描述

ArticleEdit.vue

<script setup>
    //导入下俩列表组件
import ChannelSelect from '@/views/article/components/ChannelSelect.vue'

    //提供默认数据
    //默认数据
const defaultForm = {
  title:'',//标题
  cate_id:'',//分类id
  cover_img:'',//封面图片 file对象
  content:'',//string内容
  state:''//状态
}
//提供数据
const formModel =ref({
  ...defaultForm
})
const open = (row) => {
  //显示抽屉
  visibleDrawer.value = true
  //
  if(row.id){
    //需要基于row.id发送请求,获取编辑对应的详情数据,进行回显
    console.log('编辑回显')
  }else{
    //添加之前重置数据
    formModel.value= {
      ...defaultForm
    }//基于默默人数据重置form数据
    console.log('添加')
  }
  // console.log(row)
}
//暴露方法
defineExpose({
  open,
})
</script>

抽屉结构


<template>
  <el-drawer
    v-model="visibleDrawer"
    :title="formModel.id ? '编辑文章' : '添加文章'"
    direction="rtl"
    size="50%"
  >
    <!-- 发表文章表单 -->
    <el-form :model="formModel" ref="formRef" label-width="100px">
      <el-form-item label="文章标题" prop="title">
        <el-input v-model="formModel.title" placeholder="请输入标题"></el-input>
      </el-form-item>
      <el-form-item label="文章分类" prop="cate_id">
        <channel-select
          v-model="formModel.cate_id"
        ></channel-select>
      </el-form-item>
      <el-form-item label="文章封面" prop="cover_img"> 文件上传 </el-form-item>
      <el-form-item label="文章内容" prop="content">
        <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>
</template>

下拉菜单组件中设置下拉菜单宽度占满

ChannelSelect.vue

<script setup>
	defineProps({
        width:{
            type:String
        }
    })
</script>
<template>
<el-select
    :modeValue="modeValue"
    @updata:modelValue="emit('update:modeValue', $event)"
    :style="{width}">
    <el-option
      v-for="channel in channelList"
      :label="channel.cate_name"
      :value="channel.id"
      :key="channel.id"
    ></el-option>
  </el-select>
</template>

文件新增、上传文件

官方文档

https://element-plus.org/zh-CN/component/upload.html#upload-%E4%B8%8A%E4%BC%A0

在这里插入图片描述

ArticleEdit.vue

  1. 关闭自动上传,准备结构
import { Plus } from '@element-plus/icons-vue'

<el-upload
  class="avatar-uploader"
  :auto-upload="false"
  :show-file-list="false"
  :on-change="onUploadFile"
>
  <img v-if="imgUrl" :src="imgUrl" class="avatar" />
  <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
  1. 准备数据 和 选择图片的处理逻辑
const imgUrl = ref('')
const onUploadFile = (uploadFile) => {
  imgUrl.value = URL.createObjectURL(uploadFile.raw)
  // 立刻将图片对象,存入 formModel.value.cover_img 将来用于提交

  formModel.value.cover_img = uploadFile.raw
}
  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;
    }
  }
}

在这里插入图片描述

富文本编辑器的导入

使用的是这个vue-quill
官网地址
https://vueup.github.io/vue-quill/
安装

pnpm add @vueup/vue-quill@latest

局部导入

//富文本编辑器相关
import { QuillEditor } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css'
<QuillEditor
  theme="snow"
  v-model:content="formModel.content"
  contentType="html"
></QuillEditor>

v-model:content 绑定内容,
contentType=“html” 绑定内容格式为html
展示结果

在这里插入图片描述

添加文章

  1. 封装文章接口

ArticleEdit.vue

export const artPublishService = (data) => request.post('/my/article/add', data)
  1. 注册点击事件
<el-form-item>
  <el-button @click="onPublish('已发布')" type="primary">发布</el-button>
  <el-button @click="onPublish('草稿')" type="info">草稿</el-button>
</el-form-item>

<script>
// 发布文章
const emit = defineEmits(['success'])
const onPublish = async (state) => {
  // 将已发布还是草稿状态,存入 state
  formModel.value.state = state

  // 转换 formData 数据
  const fd = new FormData()
  for (let key in formModel.value) {
    fd.append(key, formModel.value[key])
  }

  if (formModel.value.id) {
    console.log('编辑操作')
  } else {
    // 添加请求
    await artPublishService(fd)
    ElMessage.success('添加成功')
    //关闭抽屉
    visibleDrawer.value = false
      //添加成功通知父组件
    emit('success', 'add')
  }
}
</script>
  1. 父组件监听事件,重新渲染
<article-edit ref="articleEditRef" @success="onSuccess"></article-edit>

// 添加修改成功
const onSuccess = (type) => {
  if (type === 'add') {
    // 如果是添加,需要跳转渲染最后一页,编辑直接渲染当前页
    const lastPage = Math.ceil((total.value + 1) / params.value.pagesize)
    //更新成最大页码数
    params.value.pagenum = lastPage
  }
  ///如果是编辑,则直接渲染当前页
  getArticleList()
}
  1. 添加完成后的内容重置
const formRef = ref()
const editorRef = ref()
const open = async (row) => {
  visibleDrawer.value = true
  if (row.id) {
    console.log('编辑回显')
  } else {
    formModel.value = { ...defaultForm }
      //这里重置我们还需要重置图片上传img地址
    //富文本编辑器内容需要手动重置
    imgUrl.value = ''
    editorRef.value.setHTML('')
  }
}

文章编辑

  1. 封装文章接口
    article.js
//文章:获取文章详情
export const artGetDetailService = (id) => {
  return request.get("/my/article/info", {
    params: { id },
  });
};
  1. 编辑按钮点击回显
//导入及地址

import { baseURL } from "@/utils/request";
const open = async (row) => {
  //显示抽屉
  visibleDrawer.value = true;
  //
  if (row.id) {
    //需要基于row.id发送请求,获取编辑对应的详情数据,进行回显
    // console.log('编辑回显')
    const res = await artGetDetailService(row.id);
    // console.log(res)
    formModel.value = res.data.data;
    //图片需要单独处理回显
    imgUrl.value = baseURL + formModel.value.cover_img;
    //注意:提交给后台,需要的格式是file对象格式
    //需要将我们的网络图片地址=》转换成file对象,存储起来,将来便于提交
    const file = await imageUrlToFile(imgUrl.value, formModel.value.cover_img);
    formModel.value.cover_img = file;
  } else {
    //添加之前重置数据
    formModel.value = {
      ...defaultForm,
    }; //基于默默人数据重置form数据
    //这里重置我们还需要重置图片上传img地址
    //富文本编辑器内容需要手动重置
    imgUrl.value = "";
    editRef.value.setHTML("");
    // console.log('添加')
  }
  // console.log(row)
}; // 将网络图片地址转换为File对象

chatGPT prompt:封装一个函数,基于 axios, 网络图片地址,转 file 对象, 请注意:写中文注释

// 将网络图片地址转换为File对象
async function imageUrlToFile(url, fileName) {
  try {
    // 第一步:使用axios获取网络图片数据
    const response = await axios.get(url, { responseType: "arraybuffer" });
    const imageData = response.data;

    // 第二步:将图片数据转换为Blob对象
    const blob = new Blob([imageData], {
      type: response.headers["content-type"],
    });

    // 第三步:创建一个新的File对象
    const file = new File([blob], fileName, { type: blob.type });

    return file;
  } catch (error) {
    console.error("将图片转换为File对象时发生错误:", error);
    throw error;
  }
}

编辑提交

  1. 封装文章接口
    article.js
//文章:编辑文章
export const artEditArticleService = (data) => {
  return request.put("/my/article/info", data);
};

ArticleEdit.vue

const onPublish = async (state) => {
  //将已发布还是草稿状态都存入formModel
  formModel.value.state = state;
  //注意当前接口需要的是formData对象
  //将普通对象转化为formData对象
  const formData = new FormData();
  for (let key in formModel.value) {
    //将formModel的key-value对添加到formData中
    formData.append(key, formModel.value[key]);
  }
  //发送请求
  if (formModel.value.id) {
    //编辑
    // console.log('编辑')
    await artEditArticleService(formData);
    ElMessage.success("修改成功");
    visibleDrawer.value = false;
    //通知父组件渲染当前页
    emit("success", "edit");
  } else {
    //添加
    await artPublishArticleService(formData);
    ElMessage.success("添加成功");
    //关闭抽屉
    visibleDrawer.value = false;
    //添加成功通知父组件
    emit("success", "add");
  }
};

删除文章

  1. 封装文章接口
    article.js
//文章:删除文章
export const artDeleteArticleService = (id) => {
  return request.delete("/my/article/info", {
    params: {
      id,
    },
  });
};

ArticleManage.vue

//删除
const onDeleteArticle = async (row) => {
  //如何删除
  // articleEditRef.value.open(row)
  await ElMessageBox.confirm("确认删除该文章吗?", "温馨提示", {
    type: "warning",
    confirmButtonText: "确定",
    cancelButtonText: "取消",
  });
  await artDeleteArticleService(row.id);
  ElMessage.success("删除成功");
  getArticleList();
};

个人中心

基本资料

UserProfile.vue
基于 ai 提问生成

请基于 elementPlus 和 Vue3 的语法,生成组件代码
要求:
一、表单结构要求
1.  组件中包含一个el-form表单,有四行内容,前三行是输入框,第四行是按钮
2. 第一行 label 登录名称,输入框禁用不可输入状态
3. 第二行 label 用户昵称,输入框可输入
4. 第三行 label 用户邮箱,输入框可输入
5. 第四行按钮,提交修改

二、校验需求
给昵称 和 邮箱添加校验
1. 昵称 nickname 必须是2-10位的非空字符串
2. 邮箱 email 符合邮箱格式即可,且不能为空
<script setup>
  import PageContainer from "@/components/PageContainer.vue";
  import { ref } from "vue";
  import { useUserStore } from "@/stores";
  import { userUpdateInfoService } from "@/api/user";

  const formRef = ref();

  // 是在使用仓库中数据的初始值 (无需响应式) 解构无问题
  const {
    user: { email, id, nickname, username },
    getUser,
  } = useUserStore();

  const form = ref({
    id,
    username,
    nickname,
    email,
  });

  const rules = ref({
    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", "change"],
      },
    ],
  });

  const submitForm = async () => {
    // 等待校验结果
    await formRef.value.validate();
    // 提交修改
    await userUpdateInfoService(form.value);
    // 通知 user 模块,进行数据的更新
    getUser();
    // 提示用户
    ElMessage.success("修改成功");
  };
</script>
<template>
  <page-container title="基本资料">
    <!-- 表单部分 -->
    <el-form
      ref="formRef"
      :model="form"
      :rules="rules"
      label-width="100px"
    >
      <el-form-item label="登录名称">
        <el-input
          v-model="form.username"
          disabled
        ></el-input>
      </el-form-item>
      <el-form-item
        label="用户昵称"
        prop="nickname"
      >
        <el-input v-model="form.nickname"></el-input>
      </el-form-item>
      <el-form-item
        label="用户邮箱"
        prop="email"
      >
        <el-input v-model="form.email"></el-input>
      </el-form-item>
      <el-form-item>
        <el-button
          type="primary"
          @click="submitForm"
          >提交修改</el-button
        >
      </el-form-item>
    </el-form>
  </page-container>
</template>

头像

头像静态结构

<script setup>
import { ref } from 'vue'
import { Plus, Upload } from '@element-plus/icons-vue'
import { useUserStore } from '@/stores'

const userStore = useUserStore()

const imgUrl = ref(userStore.user.user_pic)
const onUploadFile = (file) => {
  console.log(file)
}
</script>

<template>
  <page-container title="更换头像">
    <el-row>
      <el-col :span="12">
        <el-upload
          ref="uploadRef"
          class="avatar-uploader"
          :auto-upload="false"
          :show-file-list="false"
          :on-change="onUploadFile"
        >
          <img v-if="imgUrl" :src="imgUrl" class="avatar" />
          <img v-else src="@/assets/avatar.jpg" width="278" />
        </el-upload>
        <br />
        <el-button type="primary" :icon="Plus" size="large">
          选择图片
        </el-button>
        <el-button type="success" :icon="Upload" size="large">
          上传头像
        </el-button>
      </el-col>
    </el-row>
  </page-container>
</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>

选择预览图片

const uploadRef = ref()
const imgUrl = ref(userStore.user.user_pic)
const onUploadFile = (file) => {
  const reader = new FileReader()
  reader.readAsDataURL(file.raw)
  reader.onload = () => {
    imgUrl.value = reader.result
  }
}
<el-upload ref="uploadRef"></el-upload>
<el-button
  @click="uploadRef.$el.querySelector('input').click()"
  type="primary"
  :icon="Plus"
  size="large"
  >选择图片</el-button
>

上传头像

  1. 封装接口
export const userUploadAvatarService = (avatar) =>
  request.patch("/my/update/avatar", { avatar });
  1. 调用接口
const onUpdateAvatar = async () => {
  await userUploadAvatarService(imgUrl.value);
  await userStore.getUser();
  ElMessage({ type: "success", message: "更换头像成功" });
};

重置密码

请基于 elementPlus 和 Vue3 的语法,生成组件代码
要求:
一、表单结构要求
1. 组件中包含一个el-form表单,有四行内容,前三行是表单输入框,第四行是两个按钮
2. 第一行 label 原密码
3. 第二行 label 新密码
4. 第三行 label 确认密码
5. 第四行两个按钮,修改密码 和 重置

二、form绑定字段如下:
const pwdForm = ref({
  old_pwd: '',
  new_pwd: '',
  re_pwd: ''
})

三、校验需求
所有字段,都是 6-15位 非空
自定义校验1:原密码 和 新密码不能一样
自定义校验2:新密码 和 确认密码必须一样

静态结构 + 校验处理

<script setup>
import { ref } from 'vue'
const pwdForm = ref({
  old_pwd: '',
  new_pwd: '',
  re_pwd: ''
})

const checkOldSame = (rule, value, cb) => {
  if (value === pwdForm.value.old_pwd) {
    cb(new Error('原密码和新密码不能一样!'))
  } else {
    cb()
  }
}

const checkNewSame = (rule, value, cb) => {
  if (value !== pwdForm.value.new_pwd) {
    cb(new Error('新密码和确认再次输入的新密码不一样!'))
  } else {
    cb()
  }
}
const rules = {
  // 原密码
  old_pwd: [
    { required: true, message: '请输入密码', trigger: 'blur' },
    {
      pattern: /^\S{6,15}$/,
      message: '密码长度必须是6-15位的非空字符串',
      trigger: 'blur'
    }
  ],
  // 新密码
  new_pwd: [
    { required: true, message: '请输入新密码', trigger: 'blur' },
    {
      pattern: /^\S{6,15}$/,
      message: '密码长度必须是6-15位的非空字符串',
      trigger: 'blur'
    },
    { validator: checkOldSame, trigger: 'blur' }
  ],
  // 确认新密码
  re_pwd: [
    { required: true, message: '请再次确认新密码', trigger: 'blur' },
    {
      pattern: /^\S{6,15}$/,
      message: '密码长度必须是6-15位的非空字符串',
      trigger: 'blur'
    },
    { validator: checkNewSame, trigger: 'blur' }
  ]
}
</script>
<template>
  <page-container title="重置密码">
    <el-row>
      <el-col :span="12">
        <el-form
          :model="pwdForm"
          :rules="rules"
          ref="formRef"
          label-width="100px"
          size="large"
        >
          <el-form-item label="原密码" prop="old_pwd">
            <el-input v-model="pwdForm.old_pwd" type="password"></el-input>
          </el-form-item>
          <el-form-item label="新密码" prop="new_pwd">
            <el-input v-model="pwdForm.new_pwd" type="password"></el-input>
          </el-form-item>
          <el-form-item label="确认新密码" prop="re_pwd">
            <el-input v-model="pwdForm.re_pwd" type="password"></el-input>
          </el-form-item>
          <el-form-item>
            <el-button @click="onSubmit" type="primary">修改密码</el-button>
            <el-button @click="onReset">重置</el-button>
          </el-form-item>
        </el-form>
      </el-col>
    </el-row>
  </page-container>
</template>

封装接口,更新密码信息

  1. 封装接口
export const userUpdatePassService = ({ old_pwd, new_pwd, re_pwd }) =>
  request.patch("/my/updatepwd", { old_pwd, new_pwd, re_pwd });
  1. 页面中调用
const formRef = ref();
const router = useRouter();
const userStore = useUserStore();
const onSubmit = async () => {
  const valid = await formRef.value.validate();
  if (valid) {
    await userUpdatePassService(pwdForm.value);
    ElMessage({ type: "success", message: "更换密码成功" });
    userStore.setToken("");
    userStore.setUser({});
    router.push("/login");
  }
};
const onReset = () => {
  formRef.value.resetFields();
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值