当前位置: 首页 > article >正文

vue3项目【黑马大事件】笔记

目录

    • 1.项目架构
      • 1.1.pnpm包管理器
      • 1.2.ESLint & prettier
      • 1.3.husky 代码检查
      • 1.4.调整项目目录
      • 1.5.VueRouter4 路由
      • 1.6.Element Puls组件库
      • 1.7.Pinia - 构建用户仓库 和 持久化
      • 1.8.Pinia - 配置仓库统一管理
    • 2.数据交互 - 请求工具设计
      • 2.1.创建 axios 实例
      • 2.2.首页整体路由设计
    • 3.登录注册页面
      • 3.1.el-row和el-col
      • 3.2.表单校验
      • 3.3.注册前的预校验
      • 3.4.封装 api 实现注册功能
      • 3.5.登录页面
    • 4.首页 layout 架子
      • 4.1.登录访问拦截(导航守卫)
      • 4.2.用户基本信息获取&渲染
      • 4.3.退出登录
    • 5.文章分类页面
      • 5.1.基本架子
      • 5.2.文章分类渲染
      • 5.3.el-table 表格动态渲染
      • 5.4.el-table 表格 loading 效果
      • 5.5.添加分类弹窗
      • 5.6.封装弹层组件
      • 5.7.准备弹层表单
      • 5.8.确认提交
      • 5.9.删除分类
    • 6.文章管理页面
      • 6.1.表单区域
      • 6.2.表格区域
      • 6.3.中英国际化处理
      • 6.4.封装【分类选择】
      • 6.5.封装请求渲染文章API
      • 6.6.分页渲染 [element-plus 分页]
      • 6.7.添加 loading 处理
      • 6.8.搜索 和 重置功能
    • 7.文章管理【抽屉区域】
      • 7.1.点击显示抽屉
      • 7.2.封装抽屉组件
      • 7.3.完善抽屉表单结构
      • 7.4.上传文件
      • 7.5.富文本编辑器 【vue-quill】
      • 7.6.添加文章功能
      • 7.7.添加完成后的内容重置
      • 7.8.编辑文章回显
      • 7.9.编辑文章功能
      • 7.10文章删除
    • 8.AI编码
      • 8.1.AI编码的基本使用
      • 8.2.通意灵码
      • 8.3.【实践】静态结构 + 校验处理
      • 8.4.【实践】更新个人信息
      • 8.5.【实践】更换头像
      • 8.6.【实践】重置密码

1.项目架构

1.1.pnpm包管理器

  1. 安装方式:

    npm install -g pnpm
    
  2. 创建项目:

    pnpm create vue
    

在这里插入图片描述

  1. 切换到项目根目录

    cd .\big-event\
    
  2. 安装项目依赖

    pnpm install
    
  3. 启动项目

    pnpm dev
    

在这里插入图片描述
在这里插入图片描述

1.2.ESLint & prettier

进入项目后,VSCode会提醒安装 prettier的相关插件(我这里已经安装好了)

在这里插入图片描述

安装后,当代码保存,会自动根据prettier规范修改代码。

1.3.husky 代码检查

husky 是一个 git hooks 工具 ( git的钩子工具,可以在特定时机执行特定的命令 )

  1. git初始化 git init(在bash终端)

  2. 初始化 husky 工具配置

    pnpm dlx husky-init && pnpm install
    

    【注意】上面代码在git bash执行在这里插入图片描述

    初始化完毕后,会在根目录下生成一个husky文件夹

  3. 修改文件夹下的pre-commit文件,将npm test修改为pnpm lint

    在这里插入图片描述

  4. 在package.json中,配置需要校验的文件

    当我们提交代码,就会基于eslint将配置的文件进行校验并尝试修复

    手动尝试调用pnpm lint,报错,配置成功
    在这里插入图片描述
    在这里插入图片描述

1.4.调整项目目录

  • 清空部分目录,保留index.js、App.vue、main.js

在这里插入图片描述

  • index.js部分保留

    import { createRouter, createWebHistory } from 'vue-router'
    
    const router = createRouter({
      history: createWebHistory(import.meta.env.BASE_URL),
      routes: [],
    })
    
    export default router
    
  • App.vue保留骨架

    <script setup></script>
    
    <template>
      <div>app</div>
    </template>
    
    <style scoped></style>
    
  • mian.js部分保留

    import { createApp } from 'vue'
    import { createPinia } from 'pinia'
    
    import App from './App.vue'
    import router from './router'
    const app = createApp(App)
    
    app.use(createPinia())
    app.use(router)
    app.mount('#app')
    
  • 新增目录,在src下添加目录api(用于封装api)和utils(用于封装api和存放公共方法)

在这里插入图片描述

  • 将项目需要的图片复制到assets文件夹
    在这里插入图片描述

  • 安装 sass 依赖

    pnpm add sass -D
    
  • 将全局样式在main.js中引入

    import '@/assets/main.scss'
    

1.5.VueRouter4 路由

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

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: []
})
export default router
  1. history模式: createWebHistory() http://xxx/user
  2. hash模式: createWebHashHistory() http://xxx/#/use
  • createWebHistory() 和: createWebHashHistory()的参数是路由的基地址,默认是 ‘/’

  • import.meta.env.BASE_URLvite中的环境变量,我们可以在vite.config.js中配置,

    import { fileURLToPath, URL } from 'node:url'
    import { defineConfig } from 'vite'
    import vue from '@vitejs/plugin-vue'
    
    // https://vite.dev/config/
    export default defineConfig({
    plugins: [vue()],
    resolve: {
     alias: {
       '@': fileURLToPath(new URL('./src', import.meta.url)),
     },
    },
    
    // 配置基地址
    base: '/jd',
    })
    

    配置成功后,每个路由前都会添加基地址

    在这里插入图片描述

  • 路由跳转

    <script setup>
       const goList = () => {
         this.$router.push('/list')
       }
    </script>
    
    <template>
     <div>
       <button @click="$router.push('/home')">首页</button>
       <button @click="goList">列表页</button>
     </div>
    </template>
    
    <style scoped></style>
    

    执行上面代码会报错,因为vue3没有this,如果我们想在事件处理方法中跳转路由,应该利用路由对象router

    <script setup>
        import { useRoute, useRouter } from 'vue-router'
        const route = useRoute()
        const router = useRouter()
    
        const goList = () => {
          // this.$router.push('/list')
          console.log('当前路由参数', route)
          console.log('路由对象', router)
          // 跳转路由
          router.push('/list')
        }
    </script>
    
    <template>
      <div>
        <button @click="$router.push('/home')">首页</button>
        <button @click="goList">列表页</button>
      </div>
    </template>
    
    <style scoped></style>
    
  • 在控制台,打印出了获取路由参数useRoute和获取路由对象useRouter,在useRouter上有push方法,我们可以通过push方法跳转路由。

在这里插入图片描述

1.6.Element Puls组件库

  • 官方文档: https://element-plus.org/zh-CN/
  • 安装组件库

    pnpm add element-plus
    
  • 安装让组件按需导入的插件:

    pnpm add -D unplugin-vue-components unplugin-auto-import
    
  • 在vite.config.js配置文件

    import { fileURLToPath, URL } from 'node:url'
    import { defineConfig } from 'vite'
    import vue from '@vitejs/plugin-vue'
    import AutoImport from 'unplugin-auto-import/vite'
    import Components from 'unplugin-vue-components/vite'
    import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
    
    // https://vite.dev/config/
    export default defineConfig({
      plugins: [
        vue(),
        AutoImport({
          resolvers: [ElementPlusResolver()],
        }),
        Components({
          resolvers: [ElementPlusResolver()],
        }),
      ],
      resolve: {
        alias: {
          '@': fileURLToPath(new URL('./src', import.meta.url)),
        },
      },
    
      // 配置基地址
      base: '/jd',
    })
    

    重启项目,就可以使用组件了(将button改为el-button,默认样式改变)

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

    在这里插入图片描述

1.7.Pinia - 构建用户仓库 和 持久化

  1. 安装插件 pinia-plugin-persistedstate

    pnpm add pinia-plugin-persistedstate -D
    
  2. 导入并使用pinia(在main.js中)

    import persist from 'pinia-plugin-persistedstate'
    
    app.use(createPinia().use(persist))
    
  3. 配置 stores/user.js,添加持久化配置

    import { defineStore } from 'pinia'
    import { ref } from 'vue'
    
    // 用户模块
    export const useUserStore = defineStore(
      'big-user',
      () => {
        const token = ref('') // 定义 token
        const setToken = (t) => (token.value = t) // 设置 token
    
        return { token, setToken }
      },
      {
        persist: true // 持久化
      }
    )
    

1.8.Pinia - 配置仓库统一管理

  • 统一管理前mian.js代码

    import { createApp } from 'vue'
    import { createPinia } from 'pinia'
    import persist from 'pinia-plugin-persistedstate'
    
    import App from './App.vue'
    import router from './router'
    import '@/assets/main.scss'
    
    const app = createApp(App)
    
    app.use(createPinia())
    app.use(createPinia().use(persist))
    app.use(router)
    app.mount('#app')
    

    我们可以在 stores/index.js 中完成 pinia 初始化,再交付 main.js 使用,方便代码后期维护。

  • 统一管理后index.js代码

    import { createPinia } from 'pinia'
    import persist from 'pinia-plugin-persistedstate'
    
    const pinia = createPinia()
    pinia.use(persist)
    
    // 默认导出
    export default pinia
    
  • 统一管理后mian.js代码

    import { createApp } from 'vue'
    import App from './App.vue'
    import router from './router'
    import '@/assets/main.scss'
    import pinia from '@/stores/index'
    
    const app = createApp(App)
    app.use(pinia)
    app.use(router)
    app.mount('#app')
    

    我们将模块用于储存的js文件放入文件夹modules下,并导出到index.js,要使用导入index.js即可(以index.js作为唯一出口)。
    在这里插入图片描述

在这里插入图片描述

  • index.js

    import { createPinia } from 'pinia'
    import persist from 'pinia-plugin-persistedstate'
    
    const pinia = createPinia()
    pinia.use(persist)
    
    // 默认导出
    export default pinia
    // 统一导出
    export * from './modules/user'
    

    如果在App.vue使用存储(这里可以在user.js中给token一个值,这样才可以打印出来)

    // 统一导入
    import { useUserStore } from '@/stores'
    const userStore = useUserStore()
    console.log(userStore.token)
    

2.数据交互 - 请求工具设计

2.1.创建 axios 实例

  1. 安装 axios

    pnpm add axios
    
  2. 新建 utils/request.js 封装 axios 模块

    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. 摘取核心响应数据
        if (res.data.code === 0) {
          return res
        }
        ElMessage.error(res.data.message || '服务异常')
        // 处理业务失败,提示
        return Promise.reject(res.data)
      },
      err => {
        // 401拦截
        if (err.response?.status === 401) {
          router.push('/login')
        }
    
        // TODO 5. 处理401错误
        ElMessage.error(err.response.data.message || '服务异常')
        return Promise.reject(err)
      },
    )
    
    export default instance
    

2.2.首页整体路由设计

路由规则

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 } from 'vue-router'
    
    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'),
          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
    
  • 在App.vue配置一级路由出口

    <template>
      <div>路由出口</div>
      <hr />
      <router-view />
    </template>
    
  • 在LayoutContainer.vue配置二级路由出口

    <template>
      <div>二级路由出口</div>
      <hr />
      <router-view></router-view>
    </template>
    
  • 路由和路由出口的关系图
    在这里插入图片描述
    【小优化】网页打开时, url 默认是 / 路径,未匹配到组件时,会出现空白,我们可以通过重定向,匹配 / 后, 强制跳转到文章分类页

      {
            path: '/',
            redirect: '/article/channel',
            ...
      }
    

3.登录注册页面

  1. 安装 element-plus 图标库

    pnpm i @element-plus/icons-vue
    
  2. 静态结构准备(LoginPage.vue)

    <script setup>
    import { User, Lock } from '@element-plus/icons-vue'
    import { ref } from 'vue'
    const isRegister = ref(true)
    </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>
    

3.1.el-row和el-col

类似于栅格布局

在这里插入图片描述)

3.2.表单校验

  • 数据绑定和校验规则配置

    const formModel = ref({
      username: '',
      password: '',
      repassword: '',
    })
    
    const rules = {
      username: [
        {
          required: true,
          message: '请输入用户名',
          trigger: 'blur',
        },
        {
          min: 5,
          max: 10,
          message: '用户名必须是5-10位字符',
          trigger: 'blur',
        },
      ],
      password: [
        {
          // 1.非空校验
          required: true,
          message: '请输入密码',
          trigger: 'blur',
        },
        {
          // 2.正则校验
          pattern: /^\S{6,15}$/,
          message: '密码必须是6-15位非空字符',
          trigger: 'blur',
        },
      ],
      repassword: [
        {
          required: true,
          message: '请输入密码',
          trigger: 'blur',
        },
        {
          pattern: /^\S{6,15}$/,
          message: '密码必须是6-15位非空字符',
          trigger: 'blur',
        },
        {
          // 3.自定义校验
          validator: (rules, value, callback) => {
            if (value !== formModel.value.password) {
              callback(new Error('两次输入密码不一致'))
            } else {
              callback()
            }
          },
        },
      ],
    }
    
  • 表单绑定

     <el-form
          ref="form"
          size="large"
          autocomplete="off"
          v-if="isRegister"
          :rules="rules"
          :model="formModel"
    >
    </el-form>
    
  • 输入框绑定

    <el-form-item prop="username">
        <el-input
              :prefix-icon="User"
              placeholder="请输入用户名"
              v-model="formModel.username"
        ></el-input>
    </el-form-item>
    
  1. 在el-form上,绑定:rules和:model对象
  2. 在el-form-item的prop绑定校验规则
  3. el-input绑定数据

3.3.注册前的预校验

需求:点击注册按钮需要对表单进行预校验,失败则显示校验提示

  1. 通过 ref 获取到 表单组件

    const form = ref()
    
    <el-form ref="form">
    
  2. 注册之前进行校验

    <el-button
      @click="register"
      class="button"
      type="primary"
      auto-insert-space
    >
      注册
    </el-button>
    
    const register = async () => {
      await form.value.validate()
      console.log('开始注册请求')
    }
    

3.4.封装 api 实现注册功能

在api文件夹下创建user.js封装api

  1. api/user.js中

    import request from '@/utils/request'
    
    // 注册接口
    export const userRegisterService = ({ username, password, repassword }) =>
      request.post('/api/reg', { username, password, repassword })
    
  2. 在LoginPage.vue中调用api

    import { userRegisterService } from '@/api/user'
    const register = async () => {
      await form.value.validate()
      await userRegisterService(formModel.value)
      ElMessage.success('注册成功')
      isRegister.value = false
    }
    
  3. 绑定注册事件

     <el-button class="button" type="primary" auto-insert-space @click="register">
         注册
     </el-button>
    

3.5.登录页面

登录页面与首页实现基本相同

  1. 登录页校验和首页校验基本相同

    表单绑定

    <el-form
         ref="form"
         size="large"
         autocomplete="off"
         v-else
         :rules="rules"
         :model="formModel"
    >
    

    输入框绑定

    <el-form-item prop="username">
        <el-input
              :prefix-icon="User"
              placeholder="请输入用户名"
              v-model="formModel.username"
        ></el-input>
    </el-form-item>
    <el-form-item prop="password">
        <el-input
              name="password"
              :prefix-icon="Lock"
              type="password"
              placeholder="请输入密码"
              v-model="formModel.password"
        ></el-input>
    </el-form-item>
    
  2. 登录页与注册页共享一个formModule,不过注册成功后从登录页跳转到注册页时,清空formModule内容

    // 切换到登录,并重置表单内容
    watch(isRegister, () => {
      formModel.value = {
        username: '',
        password: '',
        repassword: '',
      }
    })
    

    在使用watch之前,我们应该要导入

    import { watch } from 'vue'
    
  3. 封装登录接口

    // 登录接口
    export const userLoginService = ({ username, password }) =>
      request.post('/api/login', { username, password })
    
  4. 点击登录按钮,调用登录api,向服务器发送请求,保存响应结果中的token并跳转首页

    import { useUserStore } from '@/stores'
    import { useRouter } from 'vue-router'
    
    // 实例化
    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('/')
    }
    
  5. 绑定登录事件

    <el-button class="button" type="primary" auto-insert-space @click="login">登录</el-button>
    

    注意:跳转到首页后,提示框可能没有样式,这是因为vscode在你使用ElMessage时帮你自动生成了导入语句,但因为我们在前面已经配置了按需导入,所以不用写导入语句。

    解决方法:检查导入语句中是否有以下代码,删掉即可

    import { ElMessage } from 'element-plus'
    

4.首页 layout 架子

  • 结构、样式预备代码

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

如果对以上element UI标签不熟悉可以查看官方文档

4.1.登录访问拦截(导航守卫)

没有登录的用户,只能浏览登录页,所以我们应该对没有登录的用户做拦截

  • 在router/index.js中

    import { useUserStore } from '@/stores'
    
    // 登录访问拦截(这段代码要写在router后面)
    router.beforeEach(to => {
      const useStore = useUserStore()
      if (!useStore.token && to.path !== '/login') {
        return '/login'
      }
    })
    

    router.beforeEach有(to,from)两个参数

    to:即将进入的路由

    from:当前导航正要离开的路由

    默认是直接放行,根据返回值决定,是放行还是拦截

    返回值操作
    undefined / true直接放行
    false拦回from的地址页面
    具体路径 或 路径对象拦截到对应的地址

在这里插入图片描述)

4.2.用户基本信息获取&渲染

  1. api/user.js封装接口

    // 获取用户信息
    export const userGetInfoService = () => request.get('/my/userinfo')
    
  2. stores/modules/user.js 定义数据

    const user = ref({})
    const getUser = async () => {
      const res = await userGetInfoService() // 请求获取数据
      user.value = res.data.data
    }
    const setUser = (obj) => (user.value = obj)
    
    // 导出(和token一起导出的,这里忽略了关于token的代码)
    return { user, getUser, setUser }
    

    这里大家不用忘了导入api

    import { userGetInfoService } from '@/api/user'
    
  3. layout/LayoutContainer页面中调用

    import { useUserStore } from '@/stores'
    const userStore = useUserStore()
    onMounted(() => {
      userStore.getUser()
    })
    

    如果这里要用onMounted,别忘了导入

    import { onMounted } from 'vue'
    
  4. 动态渲染

    <div>
      黑马程序员:<strong>{{ userStore.user.nickname || userStore.user.username }}</strong>
    </div>
    
    <!-- 如果没有头像,可以登录已经写好的网页上传 -->
    <el-avatar :src="userStore.user.user_pic || avatar" />
    

4.3.退出登录

  1. 注册点击事件

    <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>
    
  2. 导入router并实例化

    import { useRouter } from 'vue-router'
    const router = useRouter()
    
  3. api/modules/user.js中添加removeToken

    const removeToken = () => {
      token.value = ''
    }
    
  4. 添加相应功能

    // 下拉框菜单
    const handleCommand = (key) => {
      if (key === 'logout') {
        // 退出确认
        ElMessageBox.confirm('确定要退出登录吗?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning',
        }).then(() => {
          ElMessage.success('退出登录成功')
          // 退出
          userStore.removeToken()
          userStore.setUser({})
          router.push('/login')
        })
      } else {
        router.push(`/user/${key}`)
      }
    }
    

5.文章分类页面

5.1.基本架子

  1. 基本结构样式,用到了 el-card 组件

    <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-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>
    
  2. 考虑到多个页面复用,封装成组件(在@/components/PageContainer.vue)

    • props 定制标题
    • 默认插槽 default 定制内容主体
    • 具名插槽 extra 定制头部右侧额外的按钮
    <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;
        align-items: center;
        justify-content: space-between;
      }
    }
    </style>
    
  3. 页面中直接使用 ( unplugin-vue-components 会自动注册)

    文章分类测试:

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

    文章管理测试:

    <template>
      <page-container title="文章管理">
        <template #extra>
          <el-button type="primary">发布文章</el-button>
        </template>
    
        主体部分
      </page-container>
    </template>
    

在这里插入图片描述

5.2.文章分类渲染

  1. 新建 api/article.js 封装获取频道列表的接口

    // 获取频道列表
    export const artGetChannelsService = () => request.get('/my/cate/list')
    
  2. 页面中调用接口,获取数据存储

    const channelList = ref([])
    
    const getChannelList = async () => {
      const res = await artGetChannelsService()
      channelList.value = res.data.data
    }
    

5.3.el-table 表格动态渲染

  • 代码(替换主体部分):
    <script setup>
    	const onEditChannel = (row) => {
      		console.log(row)
    	}
    	const onDelChannel = (row) => {
    	  console.log(row)
    	}
    <script>
    
    <el-table :data="channelList" style="width: 100%">
      <el-table-column label="序号" width="100" type="index"> </el-table-column>
      <el-table-column label="分类名称" prop="cate_name"></el-table-column>
      <el-table-column label="分类别名" prop="cate_alias"></el-table-column>
      <el-table-column label="操作" width="100">
        <template #default="{ row }">
          <el-button
            :icon="Edit"
            circle
            plain
            type="primary"
            @click="onEditChannel(row)"
          ></el-button>
          <el-button
            :icon="Delete"
            circle
            plain
            type="danger"
            @click="onDelChannel(row)"
          ></el-button>
        </template>
      </el-table-column>
      <template #empty>
        <el-empty description="没有数据" />
      </template>
    </el-table>
    

5.4.el-table 表格 loading 效果

  1. 定义变量,v-loading绑定

    const loading = ref(false)
    
    <el-table v-loading="loading">
    
  2. 发送请求前开启,请求结束关闭

    const getChannelList = async () => {
      loading.value = true
      const res = await artGetChannelsService()
      channelList.value = res.data.data
      loading.value = false
    }
    

5.5.添加分类弹窗

  1. 创建变量控制弹窗显示,并创建方法控制变量

    const dialogVisible = ref(false)
    const onAddChnanel = () => {
      dialogVisible.value = true
    }
    
  2. 创建弹窗html结构(与el-table平级位置),并将显示和变量绑定

    <el-dialog v-model="dialogVisible" title="添加弹层" width="30%">
      <div>哒哒哒</div>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary"> 确认 </el-button>
       </span>
      </template>
    </el-dialog>
    
  3. 将按钮和方法绑定

    <el-button @click="onAddChnanel">添加分类</el-button>
    

5.6.封装弹层组件

  1. 封装组件 article/components/ChannelEdit.vue

    添加 和 编辑,可以共用一个弹层,所以可以将弹层封装成一个组件,组件对外暴露一个方法 open, 基于 open 的参数,初始化表单数据,并判断区分是添加 还是 编辑

    传参操作
    open({ })添加操作,添加表单初始化无数据
    open({ id: xx, … })编辑操作,编辑表单初始化需回显
    <script setup>
    import { ref } from 'vue'
    const dialogVisible = ref(false)
    
    const open = async (row) => {
      dialogVisible.value = true
      console.log(row)
    }
    
    // 对外暴露open方法
    defineExpose({
      open
    })
    </script>
    
    <template>
      <el-dialog v-model="dialogVisible" title="添加弹层" width="30%">
        <div>我是内容部分</div>
    
     	<!-- 取消确认 -->
        <template #footer>
          <span class="dialog-footer">
            <el-button @click="dialogVisible = false">取消</el-button>
            <el-button type="primary"> 确认 </el-button>
          </span>
        </template>
      </el-dialog>
    </template>
    
  2. 通过 ref 绑定(用channel-edit组件替换el-dialog)

    const dialog = ref()
    
    <!-- 弹窗 -->
    <channel-edit ref="dialog"></channel-edit>
    
  3. 导入组件(因为我们没有在@/components/创建ChannelEdit.vue组件,不会被unplugin-vue-components 自动注册)

    import ChannelEdit from './components/ChannelEdit.vue'
    
  4. 点击调用方法显示弹窗

    const onAddChannel = () => {
      dialog.value.open({})
    }
    const onEditChannel = (row) => {
      dialog.value.open(row)
    }
    

    注意,这里可能会报一些错误,重启项目就好了

5.7.准备弹层表单

  1. 准备数据 和 校验规则

    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'
        }
      ]
    }
    
  2. 准备表单

    <!-- 内容部分 -->
    <el-form
      :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"
          minlength="1"
          maxlength="10"
        ></el-input>
      </el-form-item>
      <el-form-item label="分类别名" prop="cate_alias">
        <el-input
          v-model="formModel.cate_alias"
          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="onSubmit"> 确认 </el-button>
       </span>
     </template>
    
  3. 编辑需要回显,表单数据需要初始化

    const open = async (row) => {
      dialogVisible.value = true
      // 这里用展开运算符拷贝数据
      formModel.value = { ...row }
    }
    
  4. 基于传过来的表单数据,进行标题控制,有 id 的是编辑

    <el-dialog
        v-model="dialogVisible"
        :title="formModel.id ? '编辑分类' : '添加分类'"
        width="30%"
    >
    

5.8.确认提交

  1. api/article.js 封装请求 API

    // 添加文章分类
    export const artAddChannelService = (data) => request.post('/my/cate/add', data)
    // 编辑文章分类
    export const artEditChannelService = (data) =>
      request.put('/my/cate/info', data)
    
  2. 页面中校验,判断,提交请求

    <el-form ref="formRef">
    
    const formRef = ref()
    const onSubmit = async () => {
      await formRef.value.validate()
      formModel.value.id
        ? await artEditChannelService(formModel.value)
        : await artAddChannelService(formModel.value)
      ElMessage({
        type: 'success',
        message: formModel.value.id ? '编辑成功' : '添加成功'
      })
      dialogVisible.value = false
    }
    
  3. 通知父组件进行回显(即刷新页面)

    const emit = defineEmits(['success'])
    
    const onSubmit = async () => {
      ...
      emit('success')
    }
    
  4. 父组件监听 success 事件,进行调用回显

    <channel-edit ref="dialog" @success="onSuccess"></channel-edit>
    
    const onSuccess = () => {
      getChannelList()
    }
    

5.9.删除分类

  1. api/article.js封装接口 api

    // 删除文章分类
    export const artDelChannelService = (id) =>
      request.delete('/my/cate/del', {
        params: { id }
      })
    
  2. 页面中添加确认框,调用接口进行提示

    const onDelChannel = async (row) => {
      await ElMessageBox.confirm('你确认删除该分类信息吗?', '温馨提示', {
        type: 'warning',
        confirmButtonText: '确认',
        cancelButtonText: '取消'
      })
      await artDelChannelService(row.id)
      ElMessage({ type: 'success', message: '删除成功' })
      getChannelList()
    }
    

6.文章管理页面

在这里插入图片描述

6.1.表单区域

  1. 基本架子

    <!-- 表单区域 -->
    <el-form inline>
      <el-form-item label="文章分类:">
        <el-select  style="width: 200px">
          <el-option label="新闻" value="111"></el-option>
          <el-option label="体育" value="222"></el-option>
        </el-select>
      </el-form-item  style="width: 200px">
      <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>
    

    表单外面还嵌套了上面封装的page-container组件,基本布局如下

    <page-container title="文章管理">
        <template #extra>
          <el-button @click="onAddArticle">添加文章</el-button>
        </template>
        <!-- 表单区域 -->
        <el-form inline>
        ...
        </el-form>
    
        <!-- 表格区域 -->
        ...
        <!-- 分页区域 -->
        ...
        <!-- 抽屉区域 -->
        ...
      </page-container>
    
  2. 表格准备,模拟假数据渲染

    import { Delete, Edit } from '@element-plus/icons-vue'
    import { ref } from 'vue'
    // 假数据
    const articleList = ref([
      {
        id: 5961,
        title: '文章1',
        pub_date: '2024-07-10 14:53:52.604',
        state: '已发布',
        cate_name: '体育'
      },
      {
        id: 5962,
        title: '文章2',
        pub_date: '2024-07-10 14:54:30.904',
        state: null,
        cate_name: '体育'
      }
    ])
    

6.2.表格区域

  1. 基本架子

    <el-table :data="articleList" style="width: 100%">
      <el-table-column label="文章标题" width="400">
        <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>
      <el-table-column label="操作" width="100">
        <template #default="{ row }">
          <el-button
            :icon="Edit"
            circle
            plain
            type="primary"
            @click="onEditArticle(row)"
          ></el-button>
          <el-button
            :icon="Delete"
            circle
            plain
            type="danger"
            @click="onDeleteArticle(row)"
          ></el-button>
        </template>
      </el-table-column>
      <template #empty>
        <el-empty description="没有数据" />
      </template>
    </el-table>
    
    const onEditArticle = (row) => {
      console.log(row)
    }
    const onDeleteArticle = (row) => {
      console.log(row)
    }
    

6.3.中英国际化处理

  • 默认是英文的,由于这里不涉及切换, 所以在 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>
    

6.4.封装【分类选择】

在这里插入图片描述

如图。由于文章分类选择下拉框在表单区域和抽屉区域都用到了,我们可以将其封装成组件

  1. 新建 article/components/ChannelSelect.vue

    <template>
      <el-select style="width: 200px">
        <el-option label="新闻" value="新闻"></el-option>
        <el-option label="体育" value="体育"></el-option>
      </el-select>
    </template>
    
  2. 在页面中导入使用

    // 导入
    import ChannelSelect from './components/ChannelSelect.vue'
    
    // 使用
    <el-form-item label="文章分类:">
      <channel-select></channel-select>
    </el-form-item>
    
  3. 在src/api/ariticle.js中封装获取文章分类的api

    // 获取文章分类
    export const artGetChannelsService = () => request.get('/my/cate/list')
    
  4. 调用接口,动态渲染下拉分类

    // ChannelSelect.vue,js部分
    import { artGetChannelsService } from '@/api/article'
    import { ref } from 'vue'
    
    defineProps({
      modelValue: {
        type: [Number, String]
      }
    })
    
    const emit = defineEmits(['update:modelValue'])
    const channelList = ref([])
    const getChannelList = async () => {
      const res = await artGetChannelsService()
      channelList.value = res.data.data
    }
    getChannelList()
    
    <!-- ChannelSelect.vue,html部分 -->
    <el-select
        style="width: 200px"
        :modelValue="modelValue"
        @update:modelValue="emit('update:modelValue', $event)"
      >
        <el-option
          v-for="channel in channelList"
          :key="channel.id"
          :label="channel.cate_name"
          :value="channel.id"
        ></el-option>
     </el-select>
    
  5. 父组件定义参数绑定

    const params = ref({
      pagenum: 1,
      pagesize: 5,
      cate_id: '',
      state: ''
    })
    
    <channel-select v-model="params.cate_id"></channel-select>
    
  6. 绑定发布状态,便于将来提交表单

    <el-select v-model="params.state">
      <el-option label="已发布" value="已发布"></el-option>
      <el-option label="草稿" value="草稿"></el-option>
    </el-select>
    

6.5.封装请求渲染文章API

如果没有数据,可以登录已完成的系统,添加几条数据

  1. api/article.js封装接口

    // 请求渲染文章
    export const artGetListService = (params) =>
      request.get('/my/article/list', { params })
    
  2. 页面中调用接口

    import { artGetListService } from '@/api/article'
    
    // 这里把前面的假数据articleList清空
    const articleList = ref([])
    const total = ref(0)
    
    const getArticleList = async () => {
      const res = await artGetListService(params.value)
      articleList.value = res.data.data
      total.value = res.data.total
    }
    getArticleList()
    

    如果这里报错不能识别params,请把params定义写到getArticleList方法前面

  3. 新建 utils/format.js 封装格式化日期函数

    import { dayjs } from 'element-plus'
    
    export const formatTime = (time) => dayjs(time).format('YYYY年MM月DD日')
    
  4. 导入使用

    import { formatTime } from '@/utils/format'
    
    <el-table-column label="发表时间">
      <template #default="{ row }">
        {{ formatTime(row.pub_date) }}
      </template>
    </el-table-column>
    

6.6.分页渲染 [element-plus 分页]

  1. 分页组件

    <el-pagination
      v-model:current-page="params.pagenum"
      v-model:page-size="params.pagesize"
      :page-sizes="[2, 3, 4, 5, 10]"
      layout="jumper, total, sizes, prev, pager, next"
      background
      :total="total"
      @size-change="onSizeChange"
      @current-change="onCurrentChange"
      style="margin-top: 20px; justify-content: flex-end"
    />
    
  2. 提供分页修改逻辑

    const onSizeChange = (size) => {
      params.value.pagenum = 1
      params.value.pagesize = size
      getArticleList()
    }
    const onCurrentChange = (page) => {
      params.value.pagenum = page
      getArticleList()
    }
    

6.7.添加 loading 处理

  1. 准备数据

    const loading = ref(false)
    
  2. el-table上面绑定

    <el-table v-loading="loading" > ... </el-table>
    
  3. 发送请求时添加 loading

    const getArticleList = async () => {
      loading.value = true
        
      ...
      
      loading.value = false
    }
    getArticleList()
    

6.8.搜索 和 重置功能

  1. 注册事件

    <el-form-item>
      <el-button @click="onSearch" type="primary">搜索</el-button>
      <el-button @click="onReset">重置</el-button>
    </el-form-item>
    
  2. 绑定处理

    const onSearch = () => {
      params.value.pagenum = 1
      getArticleList()
    }
    
    const onReset = () => {
      params.value.pagenum = 1
      params.value.cate_id = ''
      params.value.state = ''
      getArticleList()
    }
    

7.文章管理【抽屉区域】

7.1.点击显示抽屉

  1. 准备数据

    import { ref } from 'vue'
    const visibleDrawer = ref(false)
    
  2. 准备抽屉容器

    <el-drawer
      v-model="visibleDrawer"
      title="大标题"
      direction="rtl"
      size="50%"
    >
      <span>Hi there!</span>
    </el-drawer>
    
  3. 点击修改布尔值显示抽屉

    <el-button type="primary" @click="onAddArticle">发布文章</el-button>
    
    const visibleDrawer = ref(false)
    const onAddArticle = () => {
      visibleDrawer.value = true
    }
    

7.2.封装抽屉组件

添加 和 编辑,可以共用一个抽屉,所以可以将抽屉封装成一个组件

组件对外暴露一个方法 open, 基于 open 的参数,初始化表单数据,并判断区分是添加 还是 编辑

  1. 封装组件 article/components/ArticleEdit.vue

    <script setup>
    import { ref } from 'vue'
    const visibleDrawer = ref(false)
    
    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 there!</span>
      </el-drawer>
    </template>
    
  2. 导入组件

    import ArticleEdit from './components/ArticleEdit.vue'
    
  3. 通过 ref 绑定

    const articleEditRef = ref()
    
    <!-- 弹窗 -->
    <article-edit ref="articleEditRef"></article-edit>
    
  4. 点击显示抽屉

    // 编辑新增逻辑
    const onAddArticle = () => {
      articleEditRef.value.open({})
    }
    const onEditArticle = (row) => {
      articleEditRef.value.open(row)
    }
    

7.3.完善抽屉表单结构

  1. 准备数据

    const formModel = ref({
      title: '',
      cate_id: '',
      cover_img: '',
      content: '',
      state: ''
    })
    
    const open = async (row) => {
      visibleDrawer.value = true
      if (row.id) {
        console.log('编辑回显')
      } else {
        console.log('添加功能')
      }
    }
    
  2. 准备 form 表单结构

    import ChannelSelect from './ChannelSelect.vue'
    
    <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"
              width="100%"
            ></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>
    
  3. 一打开默认重置添加的 form 表单数据

    const defaultForm = {
      title: '',
      cate_id: '',
      cover_img: '',
      content: '',
      state: ''
    }
    const formModel = ref({ ...defaultForm })
    
    const open = async (row) => {
      visibleDrawer.value = true
      if (row.id) {
        console.log('编辑回显')
      } else {
        console.log('添加功能')
        formModel.value = { ...defaultForm }
      }
    }
    
  4. 【扩展】 下拉菜单 宽度width 设置

    defineProps({
      modelValue: {
        type: [Number, String]
      },
      width: {
        type: String
      }
    })
    
    <el-select
     ...
     :style="{ width }"
    

    这里是通过父传子,在不同地方使用同一个组件的宽度不一样,也可以为ChannelSelec组件t设置style="width: 100%" ,再为包裹

7.4.上传文件

  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>
    
  2. 准备数据 和 选择图片的处理逻辑

    const imgUrl = ref('')
    const onUploadFile = (uploadFile) => {
      imgUrl.value = URL.createObjectURL(uploadFile.raw)
      formModel.value.cover_img = uploadFile.raw
    }
    
  3. 样式美化

    .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;
        }
      }
    }
    

7.5.富文本编辑器 【vue-quill】

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

  1. 安装包

    pnpm add @vueup/vue-quill@latest
    
  2. 注册成局部组件

    import { QuillEditor } from '@vueup/vue-quill'
    import '@vueup/vue-quill/dist/vue-quill.snow.css'
    
  3. 页面中使用绑定

    <div class="editor">
      <quill-editor
        theme="snow"
        v-model:content="formModel.content"
        contentType="html"
      >
      </quill-editor>
    </div>
    
  4. 样式美化

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

7.6.添加文章功能

  1. 封装添加接口

    export const artPublishService = (data) =>
      request.post('/my/article/add', data)
    
  2. 注册点击事件调用

    <el-form-item>
      <el-button @click="onPublish('已发布')" type="primary">发布</el-button>
      <el-button @click="onPublish('草稿')" type="info">草稿</el-button>
    </el-form-item>
    
    // 发布文章
    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')
      }
    }
    
  3. 父组件监听事件,重新渲染

    <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()
    }
    

7.7.添加完成后的内容重置

  • 代码:

    const formRef = ref()
    const editorRef = ref()
    const open = async (row) => {
      visibleDrawer.value = true
      if (row.id) {
        console.log('编辑回显')
      } else {
        formModel.value = { ...defaultForm }
        imgUrl.value = ''
        editorRef.value.setHTML('')
      }
    }
    

7.8.编辑文章回显

如果是编辑操作,一打开抽屉,就需要发送请求,获取数据进行回显

  1. 封装接口,根据 id 获取详情数据

    export const artGetDetailService = (id) =>
      request.get('my/article/info', { params: { id } })
    
  2. 页面中调用渲染

    const open = async (row) => {
      visibleDrawer.value = true
      if (row.id) {
        console.log('编辑回显')
        const res = await artGetDetailService(row.id)
        formModel.value = res.data.data
        imgUrl.value = baseURL + formModel.value.cover_img
        // 提交给后台,需要的是 file 格式的,将网络图片,转成 file 格式
        // 网络图片转成 file 对象, 需要转换一下
        formModel.value.cover_img = await imageUrlToFile(imgUrl.value, formModel.value.cover_img)
      } else {
        console.log('添加功能')
        ...
      }
    }
    

    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;
      }
    }
    

7.9.编辑文章功能

  1. 封装编辑接口

    export const artEditService = (data) => request.put('my/article/info', data)
    
  2. 提交时调用

    const onPublish = async (state) => {
      ...
      if (formModel.value.id) {
        await artEditService(fd)
        ElMessage.success('编辑成功')
        visibleDrawer.value = false
        emit('success', 'edit')
      } else {
        // 添加请求
        ...
      }
    }
    

7.10文章删除

  1. 封装删除接口

    export const artDelService = (id) => request.delete('my/article/info', { params: { id } })
    
  2. 页面中添加确认框调用

    const onDeleteArticle = async (row) => {
      await ElMessageBox.confirm('你确认删除该文章信息吗?', '温馨提示', {
        type: 'warning',
        confirmButtonText: '确认',
        cancelButtonText: '取消'
      })
      await artDelService(row.id)
      ElMessage({ type: 'success', message: '删除成功' })
      getArticleList()
    }
    

8.AI编码

8.1.AI编码的基本使用

三方整合产品

  • 谷歌搜索:chatgpt 免费网站列表
  • https://github.com/LiLittleCat/awesome-free-chatgpt

我们可以尝试以下策略来优化我们的提问:

  • 明确提问

    确保问题表述清晰明确,关键字的准确度,决定了AI 对于需求的理解。

  • 细化需求:

    将问题拆分成多个小问题,可以帮助AI更具针对性地回答,也利于即时纠错。

  • 添加背景信息:

    提供有关问题背景的详细信息,也可以给 AI 预设一个角色,将有助于AI生成更具深度和价值的回答。

  • 适当引导:

    比如:“例如”、“请注意”、“请使用”等,来告诉模型你期望它做什么 或者 不做什么

  • 限制范围:

    通过限定回答的范围和长度,可以引导AI生成更精炼的回答

8.2.通意灵码

打开 vscode, 搜索并安装插件TONGYI Lingma

在这里插入图片描述

8.3.【实践】静态结构 + 校验处理

  • 提示词参考

    请基于 elementPlus 和 Vue3 的语法,生成组件代码
    要求:
    一、表单结构要求:

    1. 组件中包含一个el-form表单,有四行内容,前三行是输入框,第四行是按钮
    2. 第一行 label 登录名称,输入框禁用不可输入状态
    3. 第二行 label 用户昵称,输入框可输入
    4. 第三行 label 用户邮箱,输入框可输入
    5. 第四行按钮,提交修改

    二、校验需求:给昵称 和 邮箱添加校验

    1. 昵称 nickname 必须是2-10位的非空字符串
    2. 邮箱 email 符合邮箱格式即可,且不能为空
  • 参考目标代码:

    <script setup>
    import { ref } from 'vue'
    import { ElMessage } from 'element-plus'
    import { useUserStore } from '@/stores'
    import { userUpdateInfoService  } from '@/api/user'
    
    // 只用仓库中初始值(无需响应式)
    const {
      user: { email, id, nickname, username },
    } = useUserStore()
    
    const form = ref({
      username,
      nickname,
      email,
      id,
    })
    
    // 校验规则
    const rules = {
      nickname: [
        { required: true, message: '用户昵称不能为空', trigger: 'blur' },
        {
          min: 2,
          max: 10,
          message: '用户昵称必须是2-10位字符',
          trigger: 'blur',
        },
      ],
      email: [
        { required: true, message: '用户邮箱不能为空', trigger: 'blur' },
        {
          type: 'email',
          message: '请输入正确的邮箱地址',
          trigger: ['blur', 'change'],
        },
      ],
    }
    
    const formRef = ref(null)
    
    const resetForm = () => {
      formRef.value.resetFields()
    }
    </script>
    
    <template>
      <page-container title="用户信息">
        <div class="custom-form">
          <el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
            <el-form-item label="登录名称" prop="username">
              <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" >提交修改</el-button>
              <el-button @click="resetForm('formRef')">重置</el-button>
            </el-form-item>
          </el-form>
        </div>
      </page-container>
    </template>
    
    <style scoped>
    .custom-form {
      width: 100%;
      margin: 10px auto;
    }
    </style>
    
    

8.4.【实践】更新个人信息

  1. 封装接口

    // 更新个人信息
    export const userUpdateInfoService = ({ id, nickname, email }) =>
      request.put('/my/userinfo', { id, nickname, email })
    
  2. 页面中校验后,封装调用

     const onSubmit = async () => {
       // 调用表单引用上的validate方法进行表单验证
       await formRef.value.validate(valid => {
         if (valid) {
           ElMessage.success('提交成功!')
     	   userUpdateInfoService(form.value)
         } else {
           ElMessage.error('提交信息错误!')
         }
       })
     }
    
  3. 绑定按钮

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

8.5.【实践】更换头像

  1. 静态结构

    <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>
    
  2. 选择预览图片

    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
    >
    
  3. 上传头像

    • 封装接口

      // 上传头像
      export const userUploadAvatarService = (avatar) => request.patch('/my/update/avatar', { avatar })
      
    • 调用接口

      const onUpdateAvatar = async () => {
        await userUploadAvatarService(imgUrl.value)
        await userStore.getUser()
        ElMessage.success('上传成功')
      }
      
    • 绑定按钮

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

8.6.【实践】重置密码

  • 提示词参考:

    请基于 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 })
      
    2. 页面中调用

      import { userUpdatePassService } from '@/api/user'
      import { useRouter } from 'vue-router'
      import { useUserStore } from '@/stores'
      import { ref } from 'vue'
      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()
      }
      

http://www.kler.cn/a/396359.html

相关文章:

  • Vue监视属性变化watch
  • Python习题 251:修改文件名称
  • 相机光学(四十四)——ALL-PD和PDAF
  • arm64架构的linux 配置vm_page_prot方式
  • 前端(3)——快速入门JaveScript
  • 给阿里云OSS绑定域名并启用SSL
  • MySQL技巧之跨服务器数据查询:基础篇-动态参数
  • c++入门--引用与指针,const与引用,NULL与nullptr
  • T6识别好莱坞明星
  • maven手动上传jar到私服仓库:mvn deploy:deploy-file命令
  • linux rsync 同步拉取上传文件
  • 【SpringBoot】使用过滤器进行XSS防御
  • 在uniapp中使用canvas封装组件遇到的坑,数据被后面设备覆盖,导致数据和前面的设备一样
  • 编译原理(手绘)
  • 2024年【A特种设备相关管理(A4电梯)】新版试题及A特种设备相关管理(A4电梯)找解析
  • 【AlphaFold3】开源本地的安装及使用
  • [Mysql] Mysql的多表查询----多表关系(下)
  • 精华帖分享|浅谈金融时间序列分析与股价随机游走
  • Maven配置元素详解
  • MATLAB中的绘图技巧
  • 高并发下如何保障系统的正确性?性能与一致性博弈的技术探索
  • ⾃动化运维利器 Ansible-Jinja2
  • 【MySQL】索引原理及操作
  • 如何用Python爬虫精准获取商品历史价格信息及API数据
  • sql server into #t2 到临时表的几种用法
  • 8 软件项目管理