大事件项目
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检查工作流
pnpm dlx husky-init && pnpm install
修改 .husky/pre-commit 文件
pnpm lint
问题:默认进行的是全量检查,耗时问题,历史问题。
<br/>
暂存区的eslint校验
lint-staged 配置
安装
pnpm i lint-staged -D
配置
package.json
{
// ... 省略 ...
"lint-staged": {
"*.{js,ts,vue}": [
"eslint --fix"
]
}
}{
"scripts": {
// ... 省略 ...
"lint-staged": "lint-staged"
}
}
修改 .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
自动按需:
安装插件
pnpm add -D unplugin-vue-components unplugin-auto-import
然后把下列代码插入到你的
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()]
})
]
})
直接使用
<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'
}
}
登录功能
实现登录校验
【需求说明】给输入框添加表单校验
用户名不能为空,用户名必须是5-10位的字符,失去焦点 和 修改内容时触发校验
密码不能为空,密码必须是6-15位的字符,失去焦点 和 修改内容时触发校验
操作步骤:
model 属性绑定 form 数据对象,直接绑定之前提供好的数据对象即可
<el-form :model="formModel" >
rules 配置校验规则,共用注册的规则即可
<el-form :rules="rules" >
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>
prop 绑定校验规则
<el-form-item prop="username">
<el-input
v-model="formModel.username"
:prefix-icon="User"
placeholder="请输入用户名"
></el-input>
</el-form-item>
...
切换的时候重置
watch(isRegister, () => {
formModel.value = {
username: '',
password: '',
repassword: ''
}
})
<br/>
登录前的预校验 & 登录成功
【需求说明1】登录之前的预校验
登录请求之前,需要对用户的输入内容,进行校验
校验通过才发送请求
【需求说明2】登录功能
封装登录API,点击按钮发送登录请求
登录成功存储token,存入pinia 和 持久化本地storage
跳转到首页,给提示
【测试账号】
登录的测试账号: shuaipeng
登录测试密码: 123456
PS: 每天账号会重置,如果被重置了,可以去注册页,注册一个新号
<br/>
实现步骤:
注册事件,进行登录前的预校验 (获取到组件调用方法)
<el-form ref="form">
const login = async () => {
await form.value.validate()
console.log('开始登录')
}
封装接口 API
export const userLoginService = ({ username, password }) =>
request.post('api/login', { username, password })
调用方法将 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效果
请求前后一开一关
准备数据
const loading = ref(false)
el-table上面绑定
<el-table v-loading="loading" > ... </el-table>
发送请求时添加 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/
安装包
pnpm add @vueup/vue-quill@latest
注册成局部组件
import { QuillEditor } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css'
页面中使用绑定
<div class="editor">
<quill-editor
theme="snow"
v-model:content="formModel.content"
contentType="html"
>
</quill-editor>
</div>
样式美化
.editor {
width: 100%;
:deep(.ql-editor) {
min-height: 200px;
}
}
添加文章
文章编辑如果是编辑操作,一打开抽屉,就需要发送请求,获取数据进行回显
封装接口,根据 id 获取详情数据
export const artGetDetailService = (id) =>
request.get('my/article/info', { params: { id } })
页面中调用渲染
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/>
新车上路,只带前10个人
新项目准备上线,寻找志同道合的合作伙伴