Vue3-项目

大事件项目

eslint+prettierrc

/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')

module.exports = {
  root: true,
  extends: [
    'plugin:vue/vue3-essential',
    'eslint:recommended',
    '@vue/eslint-config-prettier/skip-formatting'
  ],
  parserOptions: {
    ecmaVersion: 'latest'
  },
  rules: {
    //前置条件
    //关闭prettier  format on save 关闭
    //安装eslints
    'prettier/prettier': [
      'warn',
      {
        singleQuote: true, // 单引号
        semi: false, // 无分号
        printWidth: 80, // 每行宽度至多80字符
        trailingComma: 'none', // 不加对象|数组最后逗号
        endOfLine: 'auto' // 换行符号不限制(win mac 不一致)
      }
    ],
    //eslint关注与规范,如果不符合,报错
    'vue/multi-word-component-names': [
      'warn',
      {
        ignores: ['index'] // vue组件名称多单词组成(忽略index.vue)
      }
    ],
    'vue/no-setup-props-destructure': ['off'], // 关闭 props 解构的校验
    // 💡 添加未定义变量错误提示,create-vue@3.6.3 关闭,这里加上是为了支持下一个章节演示。
    'no-undef': 'error'
  },
  globals: {
    ElMessage: 'readonly',
    ElMessageBox: 'readonly',
    ElLoading: 'readonly'
  }
}

prettierrc.json

{

  "$schema": "https://json.schemastore.org/prettierrc";,
  "semi": false,
  "tabWidth": 2,
  "singleQuote": true,
  "printWidth": 100,
  "trailingComma": "none"
}

husky检查工作流

  1. git初始化 git init

  2. 初始化 husky 工具配置  https://typicode.github.io/husky/

pnpm dlx husky-init && pnpm install
  1. 修改 .husky/pre-commit 文件

pnpm lint

问题:默认进行的是全量检查,耗时问题,历史问题。

<br/>

暂存区的eslint校验

lint-staged 配置

  1. 安装

pnpm i lint-staged -D
  1. 配置 package.json

{
  // ... 省略 ...
  "lint-staged": {
    "*.{js,ts,vue}": [
      "eslint --fix"
    ]
  }
}

{
  "scripts": {
    // ... 省略 ...
    "lint-staged": "lint-staged"
  }
}

  1. 修改 .husky/pre-commit 文件

pnpm lint-staged

路由

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

// createRouter 创建路由实例,===> new VueRouter()
// 1. history模式: createWebHistory()   http://xxx/user
// 2. hash模式: createWebHashHistory()  http://xxx/#/user

// vite 的配置 import.meta.env.BASE_URL 是路由的基准地址,默认是 ’/‘
// https://vitejs.dev/guide/build.html#public-base-path

// 如果将来你部署的域名路径是:http://xxx/my-path/user
// vite.config.ts  添加配置  base: my-path,路由这就会加上 my-path 前缀了

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

export default router

// 组合式api
// 1.获取路由对象router useRouter
// const router = useRouter()
// 2.获取路由参数 route useRoute
// const route = userRoute()
<el-button @click="userStore.setToken('Bearer sidnflsngsskfanlfsa')">
  登录
</el-button>
  <el-button @click="userStore.removeToken()">退出</el-button>

引入 element-ui 组件库

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

  • 安装

$ pnpm add element-plus

自动按需:

  1. 安装插件

pnpm add -D unplugin-vue-components unplugin-auto-import
  1. 然后把下列代码插入到你的 ViteWebpack 的配置文件中

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

  1. 直接使用

<template>
  <div>
    <el-button type="primary">Primary</el-button>
    <el-button type="success">Success</el-button>
    <el-button type="info">Info</el-button>
    <el-button type="warning">Warning</el-button>
    <el-button type="danger">Danger</el-button>
    ...
  </div>
</template>

彩蛋:默认 components 下的文件也会被自动注册~

封装request

import axios from 'axios'
import { useUserStore } from '@/stores'
import { ElMessage } from 'element-plus'
import router from '@/router' //js文件中导入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 userStore = useUserStore()
    if (userStore.token) {
      config.headers.Authorization = userStore.token
    }
    return config
  },
  (err) => Promise.reject(err)
)

// 响应拦截器
instance.interceptors.response.use(
  (res) => {
    // TODO 4. 摘取核心响应数据
    if (res.data.code === 0) {
      return res
    }
    // TODO 3. 处理业务失败
    // 处理业务失败,给错误提示,抛出错误
    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 }

注册登录表单校验

就四个点:formnModel(表单对象)、ruler(规则)、v-model(绑定表单对象某一属性)、prop(对应规则)

结构相关:
el-row 一行 一行分为24份
el-col 表示列 
  (1):span="12" 表示一行内12份 50% 
  (2):span="6" 表示在一行内6份 25%
  (3):offset="3" 表示左侧的margin份数

el-form 整个表单组件
el-form-item 表单的一行(一个表单域)
el-input 表单元素(输入框)

校验相关:
(1) el-form => :model="ruleForm" 绑定整个form的数据对象 {xxx,xxx,xxx}
(2) el-form => :rules="rules" 绑定整个rules规则对象 {xxx,xxx,xxx}
(3) 表单元素 => v-model="ruleForm.xxx" 给表单元素,绑定form的子属性
  (4)el-form-item prop配置生效的是那个校验规则  (和rules中的字段对应)

// 整个表单的校验规则
// 1.非空校验 required  message消息提示 trigger 触发时机 blur和change
// 2.长度校验 min:xxx max:xxx
// 3.正则校验 pattern:正则校验  \S非空字符

自定义校验

注册预校验

elmessage等三个组件 不用局部导入直接使用

module.exports = {
  ...
  globals: {
    ElMessage: 'readonly',
    ElMessageBox: 'readonly',
    ElLoading: 'readonly'
  }
}

登录功能

实现登录校验

【需求说明】给输入框添加表单校验

  1. 用户名不能为空,用户名必须是5-10位的字符,失去焦点 和 修改内容时触发校验

  2. 密码不能为空,密码必须是6-15位的字符,失去焦点 和 修改内容时触发校验

操作步骤:

  1. model 属性绑定 form 数据对象,直接绑定之前提供好的数据对象即可

<el-form :model="formModel" >
  1. rules 配置校验规则,共用注册的规则即可

<el-form :rules="rules" >
  1. v-model 绑定 form 数据对象的子属性

<el-input
  v-model="formModel.username"
  :prefix-icon="User"
  placeholder="请输入用户名"
></el-input>
<el-input
  v-model="formModel.password"
  name="password"
  :prefix-icon="Lock"
  type="password"
  placeholder="请输入密码"
></el-input>
  1. prop 绑定校验规则

<el-form-item prop="username">
  <el-input
    v-model="formModel.username"
    :prefix-icon="User"
    placeholder="请输入用户名"
  ></el-input>
</el-form-item>
...
  1. 切换的时候重置

watch(isRegister, () => {
  formModel.value = {
    username: '',
    password: '',
    repassword: ''
  }
})

<br/>

登录前的预校验 & 登录成功

【需求说明1】登录之前的预校验

  • 登录请求之前,需要对用户的输入内容,进行校验

  • 校验通过才发送请求

【需求说明2】登录功能

  1. 封装登录API,点击按钮发送登录请求

  2. 登录成功存储token,存入pinia 和 持久化本地storage

  3. 跳转到首页,给提示

【测试账号】

  • 登录的测试账号:  shuaipeng

  • 登录测试密码:  123456

PS: 每天账号会重置,如果被重置了,可以去注册页,注册一个新号

<br/>

实现步骤:

  1. 注册事件,进行登录前的预校验 (获取到组件调用方法)

<el-form ref="form">
    
const login = async () => {
  await form.value.validate()
  console.log('开始登录')
}
  1. 封装接口 API

export const userLoginService = ({ username, password }) =>
  request.post('api/login', { username, password })
  1. 调用方法将 token 存入 pinia 并 自动持久化本地

const userStore = useUserStore()
const router = useRouter()
const login = async () => {
  await form.value.validate()
  const res = await userLoginService(formModel.value)
  userStore.setToken(res.data.token)
  ElMessage.success('登录成功')
  router.push('/')
}

渲染用户列表

退出功能

返回promise都需要async await

文章分类

pageContainer

文章分类渲染

添加/编辑分类-弹层封装

弹窗表单规则验证

效果:

编辑回显

<br/>

添加分类

删除分类

// 删除文章分类
export const artDelChannelService = (id) =>
  // get delete 第二个参数都是config params
  request.delete('/my/cate/del', {
    params: { id }
  })
//删除按钮绑onDelChannel方法
const onDelChannel = async (row) => {
  await ElMessageBox.confirm('你确认要删除吗?', '温馨提示', {
    type: 'waring',
    confirmButtonText: '确认',
    cancelButtonText: '取消'
  })
  await artDelChannelService(row.id)
  ElMessage.success('输出成功')
  getChannelList()
}

文章管理

<!-- 表单区域 -->
    <el-form inline>
      <el-form-item label="文章分类">
        <!-- label展示给用户,value是提交服务器 -->
        <el-select style="width: 150px">
          <el-option label="新闻" value="1110"></el-option>
          <el-option label="体育" value="129"></el-option>
        </el-select>
      </el-form-item>
      <el-form-item label="发布状态">
        <el-select style="width: 150px">
          <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>

<br/>

const getArtList = async () => {
  loading.value = true
  const res = await artGetListService(params.value)
  articleList.value = res.data.data
  total.value = res.data.total
  loading.value = false
}
  接收数据给articleList
  articleList
  表格el-table :data 绑定
  下面的el-table-column 的prop绑定对应渲染数据

配置中文

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

<template>
  <div>
    <el-config-provider :locale="zhCn">
      <router-view></router-view
    ></el-config-provider>
  </div>
</template>

<style scoped></style>

文章分类组件封装

文章列表渲染

处理分页逻辑

添加loading效果

请求前后一开一关

  1. 准备数据

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

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

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

搜索/重置功能

总的来说就是改变params的条件

实现不同的渲染

传给articleList 进而传给页面

// 搜索功能  ->安装最新的条件检索 从第一页开始展示
const onSearch = () => {
  params.value.pagenum = 1 //重置页面  ,v-model="params.state"通过id绑定请求 故不用传其他值
  getArtList()
}
//重置功能  条件清空重新检索渲染
const onReset = () => {
  params.value.pagenum = 1
  params.value.cate_id = ''
  params.value.state = ''
  getArtList()
}

文章新增

封装抽屉组件

直接设置无效

<channel-select
          v-model="formModel.cate_id"
          width="100%"
        ></channel-select>

子传父

上传文件(预览图片)
<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>

富文本编辑器 [ vue-quill ]

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

  1. 安装包

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

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

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

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

添加文章

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

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

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

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('添加功能')
    ...
  }
}

个人中心

修改个人信息

<script setup>
import { userUpdateInfoService } from '@/api/user'
import { useUserStore } from '@/stores'
import { ref } from 'vue'
const {
  user: { username, nickname, email, id },
  getUser
} = useUserStore()

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

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' }
  ]
})
const formRef = ref(null)
const submitForm = async () => {
  // 等待校验结果
  await formRef.value.validate()
  //提交修改
  await userUpdateInfoService(userInfo.value)
  // 通知user模块更新
  getUser()
  // 提示用户修改成功
  ElMessage.success('修改成功')
}
</script>

<template>
  <page-container title="基本资料">
    <el-row>
      <el-col :span="12">
        <el-form
          :model="userInfo"
          :rules="rules"
          ref="formRef"
          label-width="100px"
          size="large"
        >
          <el-form-item label="登录名称">
            <el-input v-model="userInfo.username" disabled></el-input>
          </el-form-item>
          <el-form-item label="用户昵称" prop="nickname">
            <el-input v-model="userInfo.nickname"></el-input>
          </el-form-item>
          <el-form-item label="用户邮箱" prop="email">
            <el-input v-model="userInfo.email"></el-input>
          </el-form-item>
          <el-form-item>
            <el-button type="primary" @click="submitForm">提交修改</el-button>
          </el-form-item>
        </el-form>
      </el-col>
    </el-row>
  </page-container>
</template>

更换头像

更新密码

<script setup>
import { ref } from 'vue'
import { userUpdatePasswordService } from '@/api/user'
import { useUserStore } from '@/stores'
import { useRouter } from 'vue-router'

const formRef = ref()
const pwdForm = ref({
  old_pwd: '',
  new_pwd: '',
  re_pwd: ''
})

const checkDifferent = (rule, value, callback) => {
  // 校验新密码和原密码不能一样
  if (value === pwdForm.value.old_pwd) {
    callback(new Error('新密码不能与原密码一样'))
  } else {
    callback()
  }
}
const checkSameAsNewPwd = (rule, value, callback) => {
  // 校验确认密码必须和新密码一样
  if (value !== pwdForm.value.new_pwd) {
    callback(new Error('确认密码必须和新密码一样'))
  } else {
    callback()
  }
}
const rules = ref({
  old_pwd: [
    { required: true, message: '请输入原密码', trigger: 'blur' },
    { min: 6, max: 15, message: '原密码长度在6-15位之间', trigger: 'blur' }
  ],
  new_pwd: [
    { required: true, message: '请输入新密码', trigger: 'blur' },
    { min: 6, max: 15, message: '新密码长度在6-15位之间', trigger: 'blur' },
    { validator: checkDifferent, trigger: 'blur' }
  ],
  re_pwd: [
    { required: true, message: '请再次输入新密码', trigger: 'blur' },
    { min: 6, max: 15, message: '确认密码长度在6-15位之间', trigger: 'blur' },
    { validator: checkSameAsNewPwd, trigger: 'blur' }
  ]
})

const userStore = useUserStore()
const router = useRouter()

const submitForm = async () => {
  await formRef.value.validate()
  await userUpdatePasswordService(pwdForm.value)
  ElMessage.success('密码修改成功')

  // 密码修改成功后,退出重新登录
  // 清空本地存储的 token 和 个人信息
  userStore.setToken('')
  userStore.setUser({})

  // 拦截登录
  router.push('/login')
}

const resetForm = () => {
  formRef.value.resetFields()
}
</script>

<template>
  <page-container title="修改密码">
    <el-row>
      <el-col :span="12">
        <el-form
          ref="formRef"
          :model="pwdForm"
          :rules="rules"
          label-width="100px"
        >
          <el-form-item label="原密码" prop="old_pwd">
            <el-input v-model="pwdForm.old_pwd" show-password></el-input>
          </el-form-item>
          <el-form-item label="新密码" prop="new_pwd">
            <el-input v-model="pwdForm.new_pwd" show-password></el-input>
          </el-form-item>
          <el-form-item label="确认密码" prop="re_pwd">
            <el-input v-model="pwdForm.re_pwd" show-password></el-input>
          </el-form-item>
          <el-form-item>
            <el-button type="primary" @click="submitForm">修改密码</el-button>
            <el-button @click="resetForm">重置</el-button>
          </el-form-item>
        </el-form></el-col
      >
    </el-row>
  </page-container>
</template>

<br/>

已有 2 条评论

  1. 新车上路,只带前10个人

  2. 新项目准备上线,寻找志同道合的合作伙伴

发表评论:

陕ICP备2023000669号-1