基于vue3框架的一个项目学习吧,部分不太重要的省略了,也算是对思路的一个梳理
Vue3项目的搭建:
1.npm create vite
2.配置浏览器自动打开:
"dev": "vite --open",
3.eslink:插件,用于代码检测
4.prettier: 同样是用于自动化和统一化代码的格式
5.styleLink工具配置
6.husky配置
7.commitLink配置
8.统一包管理器工具下载依赖
9.element-plus集成
在git中输入git commit命令时跳转彩色字页面
解决方法:
1.按insert(部分电脑是fn+insert)键进入编辑,然后再输入你提交的这部分东西的描述,中英文均可
2.按esc键退出编辑
3.输入:wq
tsconfig.json文件配置详解与常用配置-优快云博客
在使用typescript的时候,报以上错误,是JSX 元素隐式具有类型 “any”,因为不存在全局类型 “JSX.Element”。
解决的办法有二种:
一、不使用严格的类型检查,即在 tsconfig.json 中设置 “strict”: false
二、在 tsconfig.json中设置 “noImplicitThis”: false
...视频里安装了很多工程化插件,可以后续深入进行学习
包括项目环境变量的配置(本地环境/测试环境等)、svg图标的封装与使用、自定义插件注册svg全局组件
src别名的配置
快速获取到文件位置
注册全局组件
后续项目中如果有需要大量重复使用的组件,直接注册后就不用再多次在项目中引用了
scss全局样式:
1、清除默认样式 :
在main.ts中导入要作为的默认全局样式后,可以上npm官网复制清除样式代码
reset.scss(其实csdn上搜索也可以应该..)
2、全局变量的导入
mock数据(搭建的虚假接口,可以使用apifox等应用来代替
axios的二次封装:
使用axios插件来实现网络请求的发送
二次封装目的:使用请求拦截器,可以在其中处理一些业务(开始进度条、请求头携带公共参数等
使用响应拦截器:处理业务(进度条结束、简化服务器返回数据、处理http网络错误等
在组件中使用的时候就可以不用带‘api’,请求拦截器封装 -> 请求头(可以给服务器携带公共参数)
//request.use...
confug.header.token = '123'
一定要返回config
api的配置:
统一管理好接口,包括api地址,export方法进行使用
api的类型设置,可以为每一个接口单独配置数据类型(便于规范接口数据以及对数据类型进行检测
路由的配置:
npm i vue-router
路由切换时,需要滚动条
硅谷甄选项目实战:
自定义校验规则函数(也可以直接使用element-ui的内置组件)
const validatorUserName = (rule: any, valueL ant, callback: any) => {
//输入的文本内容是否符合规则
if(//正则表达式){
callback()
}else {
callback(new Error('返回错误信息')
}
}
//rule:校验规则对象;value: 表单元素-文本内容
//函数:符合条件callback则通过,不符合则报错
递归组件生成动态菜单
菜单栏依据(路由效果)权限
获取到需要动态渲染的菜单列表
<template v-for= "(item, index) in menuList" :key="item.path">
//分情况讨论 有无子路由
//没有子路由
<template v-if="!item.children"></template>
//有子路由
...
</template>
菜单图标的完全导入(将所有图标注册为全局组件)
imporet * as ElementPlusIconsVue from '@element-plus/icon-vue'
export default {
install(app:any){
Object.keys(allGloablComponent),forEach(key => {
...参考官网
})///
}
}
菜单使用(ele-plus)menu组件
菜单折叠效果实现
点击图标后菜单展开
//在pinia仓库中
let useLayOutSettingStrro = defineStore('SettingStore',{
state: ()=> {
return {
fold:false //控制菜单状态
}
}
})
export default LayOutSet
//左侧菜单 动态绑定class 可以直接使用bool等
<div class="layout" :class="{ fold: LayOutSet.fold ? true: false}">
</div>
刷新业务的实现
(监听仓库数据变化)
全屏模式的切换
//全屏按钮点击的回调 要保证浏览器的兼容
const fullScreen = () => {
//DOM对象的属性:可以用来判断当前是否为全屏模式 全屏为true
let full = document.fullscreenElement
if(!full){
//文档根节点的方法
document.documentElement.requestFullscreen()
} else{
document.exitFullscreen()
}
}
获取业务信息及token的理解
token:可以看作是用户的唯一标识(后台返回给前端
//在仓库中存储数据/ 异步逻辑方法 -》全局化
let useUserStore = defineStore('User', {
//存储数据
state: (): UserState =>{
token: GET_TOKEN(),//获取用户的唯一标识
//在请求拦截器中获取到用户的token
menuRoutes: constantRoute,//仓库存储生成菜单需要的路由(数组)
username: '',
avatar: ''...},
//存储逻辑
actions: {
//用户登录方法
async userLogin(data: loginForm){...}
//获取用户信息方法
userInfo(){
///获取用户信息存储到仓库中
let result = await reqUserInfo()
//获取成功
if(result.code == 200){
this.username = result.data.checkUser.name
this.avatar = result.data.checkUser.avatar
}else{}
}
}
})
//////////
//使用时则引入小仓库
退出登录业务
--清除token
//在仓库中设置一个退出登录方法
use erLogin(){
this.token = ''
this.username = ''
this.avatar = ''
localStorage.removeItem('TOKEN')
}
路由守卫
解决用户信息无法随路由持续化保存,刷新后在其他页面就获取不到token值了
//路由鉴权 permission.ts 在全局app.use中引入一下
//项目中路由能否被访问的权限设置
import router from '@/router'
//引入进度条(需要安装)
import "nprogress/nprogress.css
//从仓库中获取到token 引入大仓库 引入小仓库
import pinia from './store'
import useUSerStore from './store/modules/user'
let userStore = useUserStore(pinia)
//全局守卫:项目中任意路由切换都会触发的钩子
//全局前置守卫
router.beforeEach(async(to: any, from: any, next: any) => {
//to 要访问的路由
//from 从哪个路由来的
//next 路由的放行函数
nprogress.start()
//获取token,判断用户登录
let token = userStore.token
//获取用户名称
let username = userStore.username
//获取用户
if(token){
//登录成功
if(to.path == '/login'){
next({path: '/'})
}else {
if(username){
next()//放行
}else{
//没有用户信息,在守卫处获取到用户信息再放行
//记得使用try catch进行包裹
await userStore.userInfo()
}
}
}else{
//当访问的就是登录界面时
if(to.path== '/login'){
next()
} else {
next({path: '/login'})
}
}
})
//全局后置守卫
router.afterEach((to: any, from: any) => {
nprogress.done();
})
接口获取(接口的封装)
//定义类型封装
import request from "@/utils/request"//axios封装
enum API {
//获取接口地址
TRADEMARK_URL = "/admin/pro..."
}
//获取已有品牌的接口方法
export const reqHasTrademark = (page:number, limit: number) => request.get<any, any>(API.TRADEMARK_API + `${page}/${limit}`)
element-plus 上传文件组件
upload组件使用...属性action
插槽使用
//具名插槽 此处为 element中直接提供的 Footer
<template #footer>
...
</template>
收集新增品牌数据
let trademark = reactive<type>({
tmname: '',
logourl: '',
})//对应组件绑定model
对上传文件的约束,使用ele中upload的钩子函数
Object.assign(trademarkParams,row) //ES6语法合并对象
表单的校验,校验成功才能继续进行代码操作
分类组件业务
当拿到一级分类后,再对应渲染二级分类
//此方法为一级分类菜单下的change事件
//change--ele中组件的附加属性 具体查阅官方文档
const handler = () => {
//通知仓库获取二级分类数据 仓库中要有对应的方法
//使用仓库中的方法
getArr(id1);
}
点击添加新属性列表
<el-button @click = "addAttrValue"></el-button>
<el-table :data = "attrParams.attrValueList">
//...引入插槽 row为当前属性值对象
<template #="row, $index">
//关于权限之下设置是否能执行输入输出操作
<el-input placeholder = "请你输入属性名称"
v-model = "row.value"
v-if = "row.flag"
@blur = "toLook"></el-input>
<div v-else @click = "toEdit(row)">
{{row.value}}
</div>
</template>
//传入删除的插槽按钮,点击即删除此行
<el-table-column lable="属性值">
<tempate #="{row, index}">
<el-button @clicl="attrParams.attrValueList.aplice(index)"></el-button>
</tempate>
</el-table-column>
</el-table>
//若没有新增属性,可以提前禁用保存按钮
<el-button @click = "save" :disabled = "arrParams.arrValueList.length>0?false:true">保存</el-button>
<script>
//添加属性值按钮
const addAttrValue = () => {
//点击后为渲染列表添加一个空的列
attrParams.attrValueList.push(
valueName = ""
)
}
//点击保存后为第三个选项的属性
const save = async () => {
//通过调用api 将所填写的属性发送置后端 增加
let result:any = await reqAddOrUpdateAttr(attrParams)
EIMessage({
type:'success',
message:attrParams.id?'changesuc':'error'
})
//添加数据成功后,可以重新获取一遍页面上的数据
getArr()
}
</script>
属性值编辑与查看模式的切换
(并非单一属性)
//与上方相关 定义一个bool变量 进行查看或编辑操作
var flag = ref(false)
//属性值表单失去焦点
const toLook = () =>{
flag.value = false
}
const toLook = (row) =>{
//非法情况判断
if(row.valueName.trim()==''){
//直接删除对应属性值为空的元素
attrParams.attrValueList.splice($index, 1)
EIMessage({
meaasge: '属性值不能为囧'
})
}
//属性值不能重复
let repeat = attrParams.attrValueList.find((item)=>{
//要把当前失去焦点属性值的shu
if(item!=row){
return item.valueName === row.valueName
}
})
if(repeat){
//直接删除对应属性值重复的元素
attrParams.attrValueList.splice($index, 1)
EIMessage({
type:' error'
message: '属性值不可以重复'
})
return
}
row.flag = false
}
const onEdit = () =>{
flag.value = true
}
//该方法只能控制单一变量,更好的方法是在添加时就为各自一栏添加bool值flag
表单元素聚焦
(实现单一表单聚焦)
//获取当前所执行操作的实例
//在原有的el-input组件绑定
//:ref = "vc:any"=>input[$index]=vc
//在<div>盒子的点击事件中 个工具index去寻找
//<div v-else @click = "toEdit(row, $index)">
//在toEdit中引入nextTick:响应式数据发生变化,获取最新的DOM组件实例
nextTick(()=>{
inputArr.value[$index].focus()
})
//新添加的输入栏也应该为聚焦状态
//在addAttrValue = ()方法中
nextTick(()=>{
//数组的最后一项聚焦
inputArr.value[attrParams.attrValueList.length-1].focus()
})
属性修改业务完成
//对修改button @click = "updateAttr"
const updateAttr = () => {
//切换为添加与修改结构
scene.value = 1
//将已有的属性对象赋值
//使用ES6中的Object.assign进行对象合并 后者影响前者
//浅拷贝 实际指向的是同一个对象 后页面的修改状态可能会影响到前页面的修改状态
//Object.assign(attrParams,row)
//使用深拷贝
Object.assign(attrParams,Json.parse(JSON.stringify(row)))
}
属性删除业务完成
<el-button @click = "deleteAttr(row.id)"></el-button>
//提示框
<el-popconfirm :title = "`你确定删除吗${row.AttrName}?`">
<template>
<el-button @click = "deleteAttr(row.id)"></el-button>
</template>
</el-popconfirm>
//
const deleteAttr = async (attrId: number) =>{
let result: any = await reqRemoveAttr(attrId)
if(redult.code == 200){
ELMessage({
type: 'success',
message: '删除成功'
})else {
EIMessage({
type: 'error',
message: '删除失败'
})
}
}
}
路由跳转时,路由组件销毁时对于在pinia仓库中所保存的相关数据也要同时销毁
仓库中有自带的方法,可以清空仓库中的数组
onBeforeUnmount(() => {
//清空对应仓库
categoreyStore.$reset()
})
SPU管理模块
SPU:电商术语,表示标准化的产品单元
spu组成:
产品名称
产品品牌
产品描述
旗下产品图片介绍
销售属性(颜色、版本、尺码)
SKU:库存量最小单位
名称、价格、重量、描述、平台属性、销售属性
静态页面的搭建
设置场景值,进行后续场景切换的操作
let scene = ref<number>(0)
SPU管理模块的接口
import request from "@/utils/request"
enum API {
//获取已有SPU数据
HASSPO_URL = '/.../'
}
//获取某个三级分类下的接口
export const reqHasSpu = (page:number, limit:number, category3Id: string|number) => request.get<any>(API.HASSPO_URL + `${page}/${limit}?category3Id=${category3Id}`)
监听数据变化进行页面重渲染
界面切换
将不同界面封装成子组件
通过设置场景值,如scene = 1,scene=2,通过v-show来控制子组件的使用
(不使用v-if 消耗性能)
<SpuForm v-show = "scene == 1" @changeScene = "changeScene">
//子组件绑定自定义事件,通知父组件切换场景
const changeScene = (obj) => {
scene.value = obj.flag
//区分子组件中操作的事件为更新还是新增
if(obj.params == 'update'){
//更新留在当前页面
getHasSpu(pageNo.value)
} else {
//新增留在第一页
getHasSpu()
}
}
//子组件中
let $emit = defineEmits(['changeScene', {flag:0, SupParams.value.id?'update':'newadd'}])
//点击取消按钮 通知父组件切换场景为1,展示有的spu数据
const cancel = ()=>{
//子组件点击取消变为场景0
$emit('changeScene', 0)
}
获取已有的SPU数据
1:我们可以通过点击父组件按钮进行子组件数据的获取,但由于数据渲染是在子组件中展示,这样的话就需要父传子
2.直接通过父组件获取子组件中的实例:
//子组件在父组件中
<SupForm ref= "Sup"...>
//父组件中获取子组件实例
let Sup = ref<any>()
//点击按钮调用子组件中的方法
const clickBtn = () => {
spu.value.initHasSupData()
}
//子组件中对外暴露方法,使得父组件中可以使用(就可以根据父级 btw此处能够拿到方法->子组件通过v-show是已经挂载了的,如果使用v-if是拿不到方法的
//
//存储已有的spu数据
let AIITradeMark = ref<any>([])
//子组件的对外暴露 此处spu是父组件传来的
const initHasSpuData = async (spu) => {
//获取全部品牌数据
let result = await getData()
//获取不同数据通过调用api...
AIITradeMarke.value = result.data
//...
}
defineExpose({ initHasSpuData})
//
添加与修改SPU接口与ts类型
展示与收集已有SPU
--先定义应该要收集的数据字段
已有SPU照片墙数据的收集
图片的预览按钮,点击后弹出新窗口,窗口显示大图片
ele中的自带属性::on-remove(删除) :on-preview(预览)
<el-dialog>
<img w-full :src="dialogImageUrl" alt="Preview Image" />
</el-dialog>
展示已有属性值和属性对象
完成收集新增销售属性
通过过滤现在页面上已有的属性,使得选择栏中只包含没有的属性,点击添加属性后,把没有在列表中显示的属性渲染到列表中
//点击属性添加按钮后显示输入框 通过row(插槽中值)中的flag值进行控制显示
<el-input @blur = "toLook(row)"
v-model = "saleAttrValue"
v-if = "row.flag == true"
placeholder = "请你输入属性值"></el-input>
<el-button @click = "toEdit(row)" v-else></el-button>
const toEdit = (row) => {
row.flag = true
}
//表单元素失去焦点的事件回调
const toLook =(row) =>{
//整理收集到的属性ID与属性值的名字
const {baseSaleAttrId, saleAttrValue} = row
//整理成服务器需要的属性值形式
let newSaleAttrValue = {
baseSaleAttrId,
//saleAttrValue赋值给saleAttrValueName
saleAttrValueName:saleAttrValue
}
//追加进数组中
//非法情况判断
if((saleAttrValue as string).trim() == ''){
EIMessage({
type: 'error',
message: '属性值不能为空'
})
}
//判断属性值是否在数组中存在
let repeat = row.spuSaleAttrValueList.find(item => {
return item.saleAttrValueName == saleAtttrValue
})
if(repeat){
EIMessage({
type: 'error',
message: '属性值不能重复'
})
return
}
if(saleAttrValue)
row.spuSaleAttrValueList.push(newSaleAttrValue)
//切换为查看模式
row.flag = false
}
保存的数据要进行数据整理
SKU业务实现
类似于spu搭建
用户管理界面搭建
添加角色的按钮权限
表单的校验
//校验用户名字的回调函数
const validatorUsername = (rule:any, value:any,callBack:any)=>{
if(value.trim().length>=5){
callBack()
}else {
callBack(new Error('用户名字至少5位'))
}
}
...
//自定义校验规则
const rule = {
//用户名称
username: [{ required: true, trigger: 'blur', validator: validatorUsername}]
//用户名称
name: [{ required: true, trigger: 'blur', validator: validatorname}]}]
password...
}
用户名称切换后,无法即使加载,当前用户要刷新才会跳转会登录页
应该让浏览器重新刷新一次,让路由守卫重新判断用户信息
添加或更新后要重新获取用户的全部账号信息
getHasUser(userParams.id?pegeNo.value : 1)(可留可不留)
window.location.reload()
添加用户和修改用户注意事项
角色分配业务
要先获取到当前角色已有的职位
关于复选框性质——见ele中相关数据
权限分配
菜单权限,按钮权限
//抽屉组件 分配职位
<el-draw></el-draw>
//树形控件 tree
<script>
</script>
菜单权限的分析与路由的拆分
路由可以依据角色对象被分为静态路由(只对外暴露的是静态路由)、动态/异步路由(根据用户所携带的路由字段进行动态添加)、任意路由
//在user仓库中获取 使用递归
...
//过滤异步路由,把异步路由过滤出来
const filterAsyncRoute = (asyncRouter, myRouteList) => {
return asyncRouter.filter(item:any => {
if(myRouteList.includes(item.name)) {
if(item.children && item.children.length > 0)
{
item.children = filterAsyncRoute(item.children, myRouteList)
}
return true
}
})
}
if(result.code == 200){
this.username = result.data.name
this.avatar = result.data.avatar
//计算当前路由所需要的异步路由
let userAsyncRoute = filterAsyncRoute(asyncRoute, result.data.routes)
//菜单所需要的数据整理完毕
this.menuRoutes = [...constantRoute, ...userAsyncRoute, anyRoute]
//异步路由和任意路由没有注册,需要动态追加
[...userSyncRoute, anyRoute].forEach((router:any)=>{
router.add(router)
})
return 'ok'
} else {
return Promise.reject(new Error(result.message))
}
关于一个用户登录后另一个用户继续登录无法获取到更多的相关属性 使用深拷贝 lodash依赖引入
对异步路由列表进行深拷贝
cloneDeep(asyncRouter)
异步路由组件没有渲染完毕:异步路由我们是在获取用户信息是进行动态判断添加的——路由守卫控制放行
next(...to)//加载完毕再放行
按钮权限
用户信息中包含buttons数组字段,放置着按钮权限标识
os: btn = "btn.Attr.add" "btn.Attr.update"
组件判断
按钮标识进仓库 获取用户信息是进行存储
1、对按钮自身进行判断(过于麻烦冗杂)
2、全局自定义指令
//main.ts中引入自定义指令文件
import (isHasButton) from '@/directive/has.ts'
isHasButton(app)
//has.ts
export const isHasButton = (app:any) => {
app.directive('has', {
//使用此自定义指令挂载完毕时执行一次
//el 为使用此指令所挂载的dom元素 options配置项获取标识值
mounted(el:any, options:any){
if(!userStore.buttons.includes(option.value)){
el.parentNode.removeChild(el)//从父节点中删除此元素
}
}
})
}
//v-has = "btn.Attr.add"进行指令使用
数据大屏解决方法vw()vh()
1、直接换算
2、数据控制数据的放大与缩小
根据放大缩小的比例 使用scale