硅谷甄选运营平台

1.项目初始化

模板路由配置

一级路由搭建

2.登录模块

登录静态页面搭建

element plus组件库使用

登录验证

注意要配置生产环境,解决跨域问题,才能成功访问到接口

登录时间判断

注意Date静态方法与new Date()调用的实例方法区别

实例方法: 需要通过new Date()实例化一个日期对象后调用,操作的是具体的日期对象。

常见实例方法:

  • getFullYear(): 获取年份。
  • getMonth(): 获取月份(0-11,0表示一月)。
  • getDate(): 获取日期(1-31)。
  • getDay(): 获取星期几(0-6,0表示星期日)。
  • getHours(): 获取小时(0-23)。
  • getMinutes(): 获取分钟(0-59)。
  • getSeconds(): 获取秒(0-59)。
  • toString(): 返回日期对象的字符串表示。

静态方法: 不需要实例化,可以直接通过Date构造函数调用,通常用于全局时间相关的操作。

常见静态方法:

  • Date.now(): 返回自1970年1月1日午夜以来的毫秒数。
  • Date.parse(): 解析一个日期字符串,返回该日期的时间戳(从1970年1月1日午夜开始的毫秒数)。
  • Date.UTC(): 根据提供的年份、月份、日期等参数,返回该日期的UTC时间戳。

登录模块表单校验

位el-form添加:model绑定收集的数据,ref打标签,便于后面调用组件实例身上的validate方法校验表单,:rules传入校验规则

自定义表单校验

向rules的validator传入函数,函数传入的三个参数

rule: any,校验配置对象

value: any, 表单文本内容

callback: any,回调函数,满足条件放行通过,否则抛出错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const validatePass = (rule: any, value: any, callback: any) => {
if (value === '') {
callback(new Error('Please input the password'))
} else {
if (ruleForm.checkPass !== '') {
if (!ruleFormRef.value) return
ruleFormRef.value.validateField('checkPass')
}
callback()
}
}
const validatePass2 = (rule: any, value: any, callback: any) => {
if (value === '') {
callback(new Error('Please input the password again'))
} else if (value !== ruleForm.pass) {
callback(new Error("Two inputs don't match!"))
} else {
callback()
}
}

const rules = reactive<FormRules<typeof ruleForm>>({
pass: [{ validator: validatePass, trigger: 'blur' }],
checkPass: [{ validator: validatePass2, trigger: 'blur' }],
})

3.主页面(layout)

主页面静态页面搭建

注意calc()和scss全局变量灵活控制样式

利用组件递归动态生成导航菜单

当有动态路由时,把路由交由pinia管理

将动态路由存储在Pinia(或其他状态管理工具)中的做法在大型Vue 3项目中比较常见,尤其是在需要根据用户权限动态生成或控制路由的情况下。将动态路由存储在Pinia中是为了更好地管理权限、动态添加路由、实现状态持久化,以及解耦业务逻辑。这种做法对于需要根据用户权限动态生成路由的应用非常有效。这么做有几个原因:

用户权限管理

在一些应用中,不同用户有不同的权限,访问的路由也有所不同。将动态路由存储在Pinia中,可以根据用户登录后的权限数据来动态生成可访问的路由表。

  • 示例: 用户登录后,后端返回用户的权限信息。前端根据这些权限信息生成动态路由,并存储在Pinia中,方便在路由守卫或其他组件中访问和控制。
1
2
3
4
5
6
7
8
9
10
11
12
// 假设权限信息从后端获取后存储在 Pinia 中
const useAuthStore = defineStore('auth', {
state: () => ({
roles: [], // 用户角色或权限
routes: [], // 动态路由
}),
actions: {
setRoutes(routes) {
this.routes = routes;
}
}
});

路由动态添加

当应用加载时,可以根据用户的权限或其他条件,动态地将路由添加到Vue Router中。将这些路由放在Pinia中可以在应用的任何地方方便地管理和修改路由。

  • 示例: 应用初始化时,根据用户角色动态添加路由:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { useAuthStore } from './store/auth';
import router from './router';

const authStore = useAuthStore();

// 根据权限生成路由
const dynamicRoutes = generateRoutes(authStore.roles);

// 存储在 Pinia 中
authStore.setRoutes(dynamicRoutes);

// 动态添加路由
dynamicRoutes.forEach(route => {
router.addRoute(route);
});

状态管理与持久化

通过Pinia管理动态路由,可以与其他应用状态一起进行集中管理和持久化。比如,当用户刷新页面时,动态路由依然可以从Pinia中恢复,不需要重新向后端请求权限信息来生成路由。

  • 示例: 结合持久化插件将动态路由存储在本地,以避免页面刷新后路由信息丢失:
1
2
3
4
5
6
import createPersistedState from 'pinia-plugin-persistedstate';

const pinia = createPinia();
pinia.use(createPersistedState({
paths: ['auth.routes'] // 持久化动态路由
}));

写样式时一个注意点

发现加上了scoped但是父组件中的样式还是会影响子组件的样式

动态创建菜单时注意点

当菜单项有且仅有一个子路由时,使用了el-menu-item来呈现该子路由,而不是使用el-sub-menu。这么做的原因:

简化用户导航:如果父菜单项只有一个子路由,那么这个父菜单项和子路由基本上是绑定在一起的,导航到父菜单项时通常直接会进入这个子路由页面。因此,将唯一的子路由直接呈现为一级菜单项,可以简化用户的点击操作,使导航更加直观。

避免冗余点击:当父菜单项只有一个子路由时,如果使用el-sub-menu,用户点击父菜单项后还需要再点击子菜单项,这种情况就显得不必要和冗余。直接将唯一的子路由呈现为一级菜单项,可以减少用户的点击次数,提升用户体验。

UI/UX体验一致:在用户看来,一个只包含一个子菜单的父菜单项如果使用了el-sub-menu,可能会让他们感到多余或困惑。直接将子菜单项提升为主菜单项,可以提供一致且简洁的用户体验。

路由与视图的自然映射:在某些设计中,一个父路由与其唯一的子路由可能代表同一个页面或功能。此时,子路由实际上代表的是主路由的具体视图,因此直接呈现为一级菜单更加合理。

可能出现的内存溢出问题

如果把menuRoutes放在组件内部,则会引发内存溢出

使用el-icon结合component(is属性对应相应meta里对应的图标)实现菜单图标

el-menu下还要包裹一层template结构

因为 el-menu 是整个菜单的容器,它只渲染一次,而 v-for 是用于循环渲染子元素的指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<el-menu class="menu">
<template v-for="m in menuRoutes" :key="m.path" >
<!-- 没有子路由,呈现el-menu-item -->
<el-menu-item :index="m.path" v-if="!m.meta.isHidden && !m.children">
<template #title>
<el-icon>
<component :is="m.meta.icon"></component>
</el-icon>
<span> {{ m.meta?.title }} </span>
</template>
</el-menu-item>
<!-- 有且仅有一个子路由,也呈现el-menu-item,避免冗余导航 -->
<el-menu-item :index="m.children[0]?.path" v-if="!m.meta.isHidden && m.children?.length === 1">
<template #title>
<el-icon>
<component :is="m.meta.icon"></component>
</el-icon>
<span>{{ m.children[0].meta.title }}</span>
</template>
</el-menu-item>
<!-- 有一个以上子路由,采用折叠菜单 -->
<el-sub-menu :index="m.path" v-if="!m.meta.isHidden && m.children && m.children.length > 1">
<template #title>
<el-icon>
<component :is="m.meta.icon"></component>
</el-icon>
<span>{{ m.meta.title }}</span>
</template>
<Menu :menuRoutes="m.children"></Menu>
</el-sub-menu>

</template>
</el-menu>

要循环渲染的是子元素而非el-menu

el-menu 作为容器: el-menu 是一个整体的菜单容器,不应被循环渲染。它内部的菜单项才是需要循环渲染的部分。直接在 el-menu 上使用 v-for 会导致整个菜单容器被多次渲染,而不是循环渲染每个菜单项。

循环的目标是子元素: v-for 的目标是循环生成多个子元素(如 el-menu-itemel-sub-menu),而不是循环生成多个 el-menu 容器。因此,v-for 应该放在一个用于包裹子元素的 <template> 或直接在子元素标签上。

保持结构清晰:v-for 放在合适的位置,可以保持代码结构清晰,避免误解和潜在的渲染问题。直接把 v-for 放在 el-menu 上,可能会使代码混乱且不符合逻辑。

搭建路由时的注意点

使用编程式路由导航,配置路由时要写完整路径

注意给路由命名不能重复,否则后面的路由会顶掉前面的路由使其不能匹配

注意menu组件里不能包含el-menu,否则递归使用组件时el-sub-menu下也是从el-menu开始的结构而非el-menu-item,会导致菜单项与父级平级,看起来没有向里的缩进量。

还要注意搭建路由时要考虑清楚,/home之所以是/的子路由,是因为home组件要展示在LayOut组件中,/screen会直接跳走为另一个页面所以与/平级,其他/acl和/product都是挂载的LayOut组件,所以其子路由自然在LayOut组件中展示(理解好嵌套路由和组件之间的关系)

其他问题

关于整个页面出现滚动条,发现是Logo组件未限定高度

添加LayOut组件一级路由切换子路由时的动画

1
2
3
4
5
6
<router-view v-slot="{ Component }">
<transition name="fade">
<!-- 渲染LayOut一级路由的子路由 -->
<component :is="Component" />
</transition>
</router-view>

el-menu-item的icon不要放到插槽中,否则折叠菜单后文字和icon都消失了,但el-sub-menu的icon放到插槽里

1
2
3
4
5
6
<el-icon>
<component :is="m.meta.icon"></component>
</el-icon>
<template #title>
<span>{{ m.children[0].meta.title }}</span>
</template>

回忆动态绑定样式(3种方式)

以前觉得样式尽量写到css里,但在真实开发中感觉行内样式也挺香的,特别是只用给span这种普普通通的元素加点margin啥的

注意折叠后不仅宽度要变,可能元素的定位也要发生变化

注意注意,解构store对象要用storeToRefs包裹,否则解构出的数据失去响应式!!!(当然后面还是觉得xxxstore.xxx比较香)

一个很奇怪的地方,切换菜单图标用refs解构成功,用storeTorefs有问题,说不能读取undefined上的value,是因为会忽略store中的方法和非响应式数据

学到了新的route的api

1
2
// 获取匹配到的路由数组
const matchedRoutes = route.matched;

刷新业务实现

点击刷新按钮:

​ 1.更改存放layout配置仓库中isRefresh的值(值是多少不重要,只要变化就好)

​ 2.监测到isRefresh修改后,卸载content部分展示组件再重新挂载( v-if 实现卸载与挂载的切换) (注意v-if是在渲染出来的组件身上)

1
2
3
4
5
6
7
8
9
<template>
<!-- 使用过渡动画 -->
<router-view v-slot="{ Component }">
<transition name="fade">
<!-- 渲染LayOut一级路由的子路由,注意是卸载和挂载这里渲染出来的组件 -->
<component :is="Component" v-if="flag"/>
</transition>
</router-view>
</template>

全屏业务实现

原生实现:

(1)Element.requestFullscreen():用于发出异步请求使元素进入全屏模式;

(2)Document.exitFullscreen() :用于让当前文档退出全屏模式;

插件实现:

screenfull.request(); // 全屏
screenfull.exit(); // 退出全屏
screenfull.toggle(); // 全屏切换

获取用户信息与token理解

token就是每个用户的唯一标识,当用户登录成功后,服务器端返回token,之后再向服务器发请求都需携带token

1
2
3
4
5
6
7
8
9
10
//请求拦截器
request.interceptors.request.use(config => {
// 如果用户小仓库已经有TOKEN的话,以后发请求的请求头都要携带TOKEN
let { token } = useAcountStore()
if (token) {
//config配置对象,headers属性请求头,经常给服务器端携带公共参数
config.headers.token = token
}
return config;
});

注意仓库中数据大部分要是响应式的,否则可能修改无效

路由鉴权

注意新建的ts文件实现某个业务要记得在main.ts中引入

注意路由鉴权文件中的router是整个项目的路由器,而非用useRouter()创建的路由器对象,原来学习路由守卫时是直接写在src/router/index.ts里的,现在只是把这部分提到新的地方src/permission.ts

1
2
// 引入整个项目的路由器(而非创建路由器对象)
import router from "./router";

注意在组件以外的地方使用小仓库要记得引入大仓库并传入

4.品牌管理模块

注意写接口时老师用的箭头函数,只有一行时可以省略()和return,但我写的普通函数要记得加return

在前端定义品牌的 TypeScript 数据类型时,id 属性可有可无是因为品牌的状态不同:

  1. 已有品牌:对于已经存在的品牌,id 是数据库中唯一标识该品牌的字段,因此它是必有的,用来区分和操作具体的品牌记录。
  2. 新增品牌:在创建新品牌时,id 通常由后端生成。前端在发送请求前,新增的品牌是没有 id 的,因为此时品牌还未被存储到数据库中,因此 id 属性可以为空或不存在。新品牌数据在发送到后端后,后端会生成一个 id 并返回给前端。

TypeScript 类型定义示例

在 TypeScript 中,你可以使用可选属性 (?) 来定义 id 可以有也可以没有:

1
2
3
4
5
typescript复制代码interface Brand {
id?: number; // 已有品牌有id,新增品牌没有id
name: string;
logoUrl: string;
}

这里 id? 表示 id 属性是可选的,只有在已有品牌时才会存在,而在新增品牌时可以不定义这个属性。

什么时候使用接口,什么时候使用类型别名?

  • 接口:更适合用于定义对象的结构,尤其是需要扩展或被类实现时。

  • 类型别名:更适合用于定义复杂的类型组合,如联合类型、交叉类型或需要表达更复杂的类型逻辑时。

  • 如果你主要是定义对象的形状,尤其是需要扩展或实现时,推荐使用接口。

  • 如果你需要更灵活地组合类型或定义复杂的类型逻辑,推荐使用类型别名。

定义ts类型

响应式数据

1
2
// 存储已有品牌数据
let trademarkArr = reactive<Records>([])

普通数据

1
let result: TrademarkResponseData = await reqHasTrademark(currentPage.value, pageSize.value)

分页器组件

虽然数据改变了,但分页器组件渲染有问题

给分页器组件添加一个key,每次数据改变后更新key

1
2
3
4
5
6
7
8
9
10
if (result.code === 200) {
// 让数据改变后分页器重新渲染
pagination.value = Date.now()
total.value = result.data.total
trademarkArr = result.data.records
// 小米的logoUrl缺少http://
if (((trademarkArr[0].logoUrl).indexOf('http://')) === -1) {
trademarkArr[0].logoUrl = 'http://' + trademarkArr[0].logoUrl
}
}

记得新增和修改品牌向服务器发请求时要带data,不然服务器拿不到更新后的数据

表单校验

form上打标识、model指定数据收集在哪、rules指定校验规则

1
<el-form ref="formRef" :model="trademarkParams" :rules="rules">

rules中自定义校验规则

1
const validatePass = (rule: any, value: any, callback: any) => {  if (value === '') {    callback(new Error('Please input the password'))  } else {    if (ruleForm.checkPass !== '') {      if (!ruleFormRef.value) return      ruleFormRef.value.validateField('checkPass')    }    callback()  } }
1
2
3
4
5
const rules = reactive<FormRules<typeof ruleForm>>({
pass: [{ validator: validatePass, trigger: 'blur' }],
checkPass: [{ validator: validatePass2, trigger: 'blur' }],
age: [{ validator: checkAge, trigger: 'blur' }],
})

在合适时机调用 formRef.value.validate()校验所有表单项

1
2
3
4
5
async function confirmDialog() {
// 校验所有表单项
await formRef.value.validate()
// 校验不通过后续不执行
.....

注意初次的formRef.value为undefined,可以用ts的问号写法或者下一次DOM更新时(nextTick)清除上一次表单校验提示错误信息

注意get,delete请求时会在url上携带id等数据,不用传递额外数据;而post,put请求往往会额外传递data数据告诉服务器需添加或修改的数据

注意删除品牌时传的当前页码:为什么要等于1?因为此时trademarkArr是还没有更新的,服务器那边已经修改了,但这边展示的数据还没有更新,要重新请求,所以这里的trademarkArr长度也是没有更新的,=1的时候实际上这页已经没有数据了,要返回上一页

1
2
// 重新获取品牌数据
await getHasTradeMark(trademarkArr.length <= 1 ? currentPage.value - 1 : currentPage.value)

5.属性管理模块

注意el-select框v-model收集选中的分类的id,但id初始值为’‘(空字符串),如果设为0最开始会展示在select框上

细节:一级分类的选中值变化时,要清空二级,三级分类的id和三级分类数组(二级分类数组不用请,已经获取了新的数据了);二级分类选中值变化时,清空三级分类id即可

使用storeTorefs和不使用各有优劣,使用了看起来比较简洁,但是每次就要.value了;不使用的话看起来比较复杂但不容易出问题

注意收集用户的数据只收集新增一个数据项应具有的属性,id等由服务器返回的数据通通没有,收集的数据可以用在新增和修改中;修改时将收集到用户上传的数据与当前要修改的完整数据项使用Object.assign合并成新的数据项

注意vue开发工具常有延迟,可通过页面来观察展示的数据,记得要给el-table传入data属性指定表格的数据

注意el-table-column插槽中可以传入row,即为当前的数据项,可以通过v-model将用户输入保存到当前数据项(类比v-for的item)

1
2
3
4
5
6
<el-table-column label="属性值名称">
<!-- row:代表attrValueList中每一项,即当前属性值对象 -->
<template #="{ row }">
<el-input v-model="row.valueName" placeholder="请输入" />
</template>
</el-table-column>

注意每次新增前要先清上一次用户输入的数据再拿到需要的数据

注意input聚焦(.focus())这样的操作一般都要放在nextTick中

对一堆input来说,每次让新添加上来的input聚焦,即所有input的最后一个(可以为这些input的ref传入函数,把他们存到数组中,方便拿到每一个input组件实例)

1
2
3
4
nextTick(() => {
// 每次让最后一个el-input组件实例聚焦
inputArr[attrParams.attrValueList.length - 1].focus()
})

注意Object.assign()是浅拷贝,新、旧数据指向同一个对象,并未开辟新的存储空间

,会出bug,比如修改属性后点击取消也会修改原来row里的属性值,但服务器那边的数据并没有被修改。可以使用lodash解决,也可以Object.assign(a,JSON.parse(JSON.stringfy(b))),把原来的对象先转成JSON字符串,再转为JSON对象,在堆内存中开辟新的空间

1
2
// 使用lodash解决
Object.assign(attrParams, cloneDeep(row))

发现了一个影响用户体验的地方,原来我把属性数组、请求获取属性的方法放在了category小仓库里,导致每次重新获取属性比较慢,会出现几秒的暂无数据页面,但把这两个数据放到attribute组件里就好了

发现使用categoryStore.$reset后路由组件渲染出问题了,显示找不到父节点?

6.SPU管理模块

el-table-column上的prop属性可以直接写源数据中的字段名称(能找到数组中每个对象中的字段)

注意,分页器初始每页显示的条数必须要在page-sizes数组中,不然page-sizes上会显示一个初始值

可以通过给form设置label-width值使每个表单项对齐

如何在父组件中拿到子组件的实例对象:用ref,在切换场景我用的v-show,所以子组件是被挂载了,只是未显示

接口崩了的处理办法,可以再配置swagger上的接口地址,再封装一个request.ts

v-model既可以收集又可以更新

数组的map方法,map() 方法返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值。map() 方法按照原始数组元素顺序依次处理元素。注意: map() 不会对空数组进行检测。注意: map() 不会改变原始数组。

1
2
3
4
5
6
7
// el-upload里上传图片的格式必须是{name:...,url:...},用map转换一下
imgList.value = imgResult.data.map((item) => {
return {
name: item.imgName,
url: item.imgUrl
}
})

当数据比较多时,分散在各个接口中时,可以先声明几个变量来存储数据,最后整合交给服务器

注意row传进来的是整个一行的对象(完整的对象)

注意filter与every的结合使用,可以过滤出符合某些条件的元素

要想Select框拿到数据需要在select上写v-model收集要拿到的数据,并在option上设置value拿到每一项对应的数据,要收集多个数据时可使用模板字符串,后面再对拿到的数据做处理如split

注意字符串split后是个数组,解构要用[]

注意对空、重复属性值的检查可以放在将新的属性值添加到原数组之前(此时不用剔除自己,因为还没收集到),也可以放到之后(此时要剔除自己,收集到了,且还要将原数组中空、重复的这个属性值去掉)

因为要收集用户输入,所以一定是在失去焦点之后才做判断

JS中的短路逻辑

imgUrl: (item.response && item.response.data) || item.imgUrl 这个代码中:

  • item.response 为真,并且 item.response.data 也存在时,表达式返回 item.response.data 的值。
  • item.response 为假(如 nullundefined)时,整个 item.response && item.response.data 表达式返回 nullundefined,此时通过 || 运算符,会使用右边的值 item.imgUrl 作为 imgUrl 的值。

不能直接写成 item.response.data || item.imgUrl 的原因:

  1. 防止访问未定义属性的错误
    • 如果 item.response 不存在(即为 undefinednull),那么 item.response.data 的访问会导致错误。
    • item.response && item.response.data 通过短路求值,首先检查 item.response 是否存在。如果不存在,就不会继续访问 item.response.data,从而避免了潜在的错误。
  2. 处理 item.responsenullundefined 的情况
    • 使用 item.response && item.response.data 可以优雅地处理 item.responsenullundefined 的情况,返回 nullundefined,然后通过 || 运算符使用 item.imgUrl 作为默认值。

注意接口定义ts数据类型要和老师完全一致

注意区分插槽传的参数$index(当前第几行)和v-for里面传的index(v-for遍历生成的索引)不是一个东西

1
2
3
4
5
<template #="{ row,$index }">
<el-tag closable v-for="(item, index) in row.spuSaleAttrValueList" :key="item.id"
@close="closeTag(row, index)" :style="{ marginRight: '6px' }">{{ item.
saleAttrValueName }}</el-tag>
</template>

注意新增SPU在清空上一次数据时,不仅要清空spuParams,还要清空其他用来收集SPU数据的变量,如imgList,attrList(最开始页面展示的就是这里面的数据)

数组的reduce方法

1
array.reduce(callback(accumulator, currentValue, currentIndex, array), initialValue)

参数解释

  1. callback: 一个函数,用于执行每个数组元素的累加计算。它有四个参数:
    • accumulator (累计器):上一次调用回调函数时返回的累积值,或者是初始值 initialValue(如果提供了的话)。
    • currentValue (当前值):数组中正在处理的当前元素。
    • currentIndex (当前索引):当前元素在数组中的索引。如果提供了 initialValue,则索引为 0,否则为 1
    • array (数组):调用 reduce 方法的数组。
  2. initialValue (可选): 作为第一次调用 callbackaccumulator 的初始值。如果没有提供初始值,数组的第一个元素将被作为初始值,并从第二个元素开始执行回调函数。

返回值

  • reduce 方法返回累计计算的结果。

先开始把sku的attrId,valueId收集到spu的attr对象身上,最后使用Reduce方法把这个字段切割并封装成对象赋值给skuParams

遍历取反的思想

1
2
3
4
5
6
7
8
9
10
11
// 设置默认图片
function setDefaultImg(row:any) {
// 勾选当前图片前的勾选框
// 先让所有图片不选,再勾选当前的(row)勾选框
imgArr.value.forEach((item) => {
imgTable.value.toggleRowSelection(item, false)
})
imgTable.value.toggleRowSelection(row, true)

skuParams.value.skuDefaultImg = row.imgUrl
}

7.用户管理模块

使input两端不能输入空格,可使用v-model.trim(输入的内容中间输入的空格不会去除)

清楚上一次表单校验提示的错误信息,可以在抽屉打开时进行,利用element plus提供的事件

1
<el-drawer v-model="showUserDrawer" direction="rtl" @open="formRef.clearValidate()">

trigger:’change’会有问题,点击取消,上次输入的信息变成空串也会触发

表单校验中v-if和v-show的问题

发现表单校验一个神奇的地方,修改用户是只展示用户名和用户昵称的,添加用户的话还要输入用户密码,所以采用v-show/v-if控制用户密码el-input显示与隐藏。但是用v-show的话密码输入框还是会挂载的,只是不展示,所以表单校验还会校验密码,就会出问题;但用v-if的话,密码输入框会被卸载,也就不会校验密码了

在 Vue 中,v-ifv-show 的工作机制不同,这也是为什么在你的示例中,使用 v-if 时不会触发校验,而使用 v-show 时会触发校验的原因。

总结:

  • 当使用 v-if="!userParams.id" 时,如果 userParams.id 存在,表单项 <el-form-item> 及其子元素根本就不会被渲染到页面上,因此表单验证也不会触发。

  • 当使用 v-show="!userParams.id" 时,即使 userParams.id 存在,表单项 <el-form-item> 及其子元素仍然存在于 DOM 中,只是被隐藏了。因此,表单验证仍然会执行。

  • **使用 v-if**:适用于需要在特定条件下完全移除表单项及其验证的场景。

  • **使用 v-show**:适用于需要在表单项可见或不可见时,依然保持表单项及其验证的场景。

每次修改完用户让浏览器强制刷新一次,因为可能修改的是自己的用户,那么就要重新获取用户信息并让用户重新登录(这里的逻辑写在了路由守卫里)

修改了login下的index.vue和permission.ts

注意展示和数据,可以将数据加工成需要的样子但可能比较复杂,展示时在模板里的js表达式也能得到相应效果

数组api的灵活使用(map,reduce)

1
2
3
4
5
6
7
8
9
// 第1种:使用reduce
// checkedRoleIds.value = checkedRoles.value.reduce((prev: any, next) => {
// prev.push(next.id)
// return prev
// }, [])
// 第2种,使用map
checkedRoleIds.value = checkedRoles.value.map(item => {
return item.id
})

注意删除加了气泡确认框后,把原来绑在delete按钮上的回调要换到气泡框的@confirm上

搜索用户业务

在获取用户列表时将输入的用户名作为query参数传给接口,来获取相应用户

注意搜索后不仅要更新当前用户列表,还要更新新返回的数据条数,当然这个新增业务可以直接添加在获取用户列表中

事件回调直接绑定异步函数可能出现问题

为何重置按钮直接绑定getUser请求会显示无网络呢,但把它写一个回调来调getUser却能正常使用?

当在渲染过程中引发了一个异步操作时,可能导致浏览器认为网络请求出错,尤其是在开发环境或某些浏览器中更容易出现这种情况

确保 getUser 的执行是在点击事件的处理过程中被调用,而不是直接作为事件处理函数返回值。这样做有几个好处:

  1. 避免 Vue 的异步错误处理问题:包装在回调中的异步函数调用,可以避免 Vue 在处理异步操作时可能产生的错误。
  2. 更清晰的代码结构:回调函数使代码更易读,并且可以在回调中加入额外的逻辑(如条件判断、额外操作等)。

可能的原因:

  • 异步操作的返回值:直接绑定异步函数会将返回的 Promise 暴露给 Vue 的事件处理机制,可能引发不可预见的行为。
  • 生命周期冲突:Vue 可能在某些时候因组件状态或渲染的原因,无法正确处理直接绑定的异步函数

想要el-input组件在按下回车后调用搜索的回调,需要在el-form上阻止按下enter(提交表单)的默认行为(会刷新页面)

1
2
<el-form ref="formRef" label-width="auto" inline @submit.prevent="">
<el-input placeholder="请输入用户姓名" v-model="searchName" @keyup.enter="searchUser" />

分配权限

勾选菜单其实只要勾选叶子节点就可以了,利用递归(把这个业务封装成一个函数,然后在函数中再次调用,即为递归)

reduce在递归中的使用?prev对累加递归调用函数的结果未可知,感觉不是一个prev了啊,每次的初始值如果赋空值的话结果根本保存不下来的,所以初始的空数组要在函数中传,而递归中传的是prev

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 收集被选中的权限的ID
// // 法一:用foreach
function getCheckedMenu(menuList: Menu[], initArr: number[]) {
menuList.forEach(item => {
if (!item.children || item.children.length <= 0) {
if (item.select) {
initArr.push((item.id as number))
}
} else {
getCheckedMenu(item.children, initArr)
}
})
return initArr
}
// 法二:用reduce
function getCheckedMenu(menuList: Menu[], initArr: number[]) {
return menuList.reduce((prev: number[], next) => {
// 检查是否为叶子节点(没有子节点)
if ((!next.children || next.children.length <= 0)) {
if (next.select) {
prev.push(next.id as number);
}
} else {
// 递归处理子节点
getCheckedMenu(next.children, prev);
}
return prev;
}, initArr);
}

初始时选中的是从数据中过滤出来的,后续被选中的权限要用计算得到

注意要分配角色、权限也需要拿到id,也要Object.assign()合并params和row

注意搜索和重置清空搜索框的时机

1
2
3
4
5
6
7
8
9
10
11
12
// 搜索按钮回调
function searchRole() {
getRole()
// 清空搜索关键字
keyword.value = ''
}
// 重置按钮回调
function refresh() {
// 清空搜索关键字
keyword.value = ''
getRole()
}

注意模板里用ref不用.value

el-table的树形,默认展开使用expand-row-keys要是字符串类型

1
<el-table :data="permissionList" style="width: 100%; margin-bottom: 20px" row-key="id" border :expand-row-keys="['1']">

注意对话框等结构不要写到表格的结构中

注意追加新的菜单,还要收集pid和level,才能知道是给谁追加几级菜单

权限管理->需设置异步路由(需满足条件才能访问),原来的全是常量路由

popover里面嵌套颜色选择器color-picker会出现的问题,颜色选择器上的事件会冒泡到popover上,而且stop没有用,可能因为是原生事件?解决方法:1.手动控制popover的显示与隐藏,其上的visible属性。2.或者在取色器上增加一个:teleported=”false”

(解释:

  • 默认 teleport 行为Element Plus 中的 el-popoverel-color-picker 都使用了 teleport 技术,默认会将弹出的内容(如颜色选择器)移动到 body 的末尾。这样可以避免由于 z-index 或父元素样式导致的显示问题。
  • 事件处理问题:当弹出的内容被 teleport 移动到 body 外部时,事件冒泡和触发机制可能不如预期,尤其是点击时。因为点击颜色选择器时,弹出层和触发层在不同的 DOM 层次中,事件可能没有被正确捕获和处理。
  • **teleported="false"**:当你将 teleported 设置为 false 时,弹出层内容会被渲染在原本组件的 DOM 结构内部,不再移到 body 外。这样可以确保事件冒泡和 DOM 层次的操作保持一致,从而避免 popover 弹出框由于错误的事件处理而消失。

因此,通过禁用 teleport 功能,所有的元素都保持在相同的 DOM 层次中,这就避免了因为跨层事件冒泡带来的问题。)

8.数据大屏

自适应问题:

vw,vh解决,思路简单,但计算麻烦,可同rem一起使用

scale缩放解决

1
2
3
4
5
6
7
8
9
10
11
12
.content {
/* #region 核心:将content的变换原点变为父元素几何中心 */
position: fixed;
left: 50%;
top:50%;
transform-origin: left top;
/* #endregion */
width: 1920px;
height: 1080px;
background-color: #ddd;
transform: scale(1.1);
}

再用js确定缩放倍数,并将content再位移到原来的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 得到content
let content = document.querySelector('.content')
// content缩放
content.style.transform = `scale(${getScale()}) translate(-50%,-50%)`
// 得到缩放倍数
function getScale(w = 1920, h = 1080) {
let ww = window.innerWidth / w
let wh = window.innerHeight / h
return ww < wh ? ww : wh
}
window.onresize = () => {
// content缩放
content.style.transform = `scale(${getScale()}) translate(-50%,-50%)`
}

reduce有时会搞不清prev变成了什么,这时直接用foreach就好.注意这里的逻辑(这里的要保持父子关系,即不能把children过滤出来和父路由同级,不然就变成下图这样了,但是也要过滤掉不能访问的子路由)和上面勾选菜单(只要最下面的叶子节点判断勾选就好了,不用管以上的节点)的处理不一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 从异步路由中过滤出用户可以访问的路由
// 注意每次使用这个函数要对asyncRoutes进行深拷贝,否则会修改元数据
function filterAsyncRoutes(asyncRoutes: RouteRecordRaw[], routeNameArr: string[]) {
return asyncRoutes.filter(item => {
// 有孩子
if (routeNameArr.includes((item.name as string))) {
// 路由名字匹配
if (item.children && item.children.length > 0) {
// 如果有孩子
// 要继续过滤出匹配的孩子
item.children = filterAsyncRoutes(item.children, routeNameArr)
}
// 没有孩子则直接返回路由
return true
}
});
}

以上过滤出来的异步路由只是能够展示出来,但还没有注册

动态追加了异步路由和任意路由后,异步路由加载白屏问题:

1
2
3
4
// 获取用户信息
await getUserInfo()
// 如果加载异步路由直接放行会出现白屏,等待路由组件渲染完毕再放行
next({...to})

注意页面需要倒计时效果,如果是用延时调用,需要在模板里调用函数获得时间,这样就可以数据更新-》模板重新渲染-》重新调用该函数-》重新执行延时调用(从而实现与定时器一样的效果)

设置按钮权限可以用自定义全局指令,只用引用一次仓库里的按钮数据

定义全局指令,要定义在函数里,然后在main.ts中引入就会执行

这里为什么要这样写?在 Axios 中,DELETE 请求的配置对象需要显式地将请求体数据放在 data 属性中。通过 { data: idList },你明确告诉 Axios 在发送 DELETE 请求时,将 idList 作为请求体的一部分传递到服务器端。

data 属性:用来指定请求体内容,类似于 POSTPUT 请求中常见的行为。

传递数据:这样后端能够接收到你指定的 idList,处理相应的批量删除操作。

在 Axios 中发送 DELETE 请求时,通常无法像 POST 请求那样直接通过第二个参数传递请求体的数据。DELETE 方法的规范更倾向于使用 URL 路径或查询字符串传递参数,而不像 POST 请求有专门的请求体用于发送数据

1
2
3
4
// 批量删除用户方法
export function reqBatchRemoveUser(idList: number[]) {
return request.delete(API.BATCHREMOVEUSERS_URL, { data: idList })
}

改变页码要在请求之前,因为当前页码改变会触发current-change事件所以传入了当前页码导致不会返回首页

1
2
3
4
5
6
7
8
9
10
// 获取已有SKU
async function getSku(pager: number = 1) {
currentPage.value = pager
// 发请求
let result: SkuResponseData = await reqSku(currentPage.value, pageSize.value)
// 存储已有SKU
skuList.value = result.data.records
// 存储数据总条数
total.value = result.data.total
}

硅谷甄选运营平台
http://example.com/2024/09/24/硅谷甄选运营平台/
作者
monica
发布于
2024年9月24日
许可协议