前言

记录平时遇到的技术问题和学习到的新知识


vue3 的 useAttrs 使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Grandpa.vue
<father text="文本" @handle="handle"/>
const handle = () => { console.log('Grandpa') }
// father.vue
<son :="attrs">
const attrs = useAttrs() // 获取所有的未在 defineProps、defineEmits 中声明的属性与事件
// son.vue
<div @click="handle">我是son {{ attrs.a }}</div>
const emit = defineEmits(['handle'])
const handle = () => {
emit('handle') // 可以直接调用 Grandpa 的方法,代替了 vue2 中的 $listeners
}

// 原因是在 v-bind:onHandle="handle",在 vue3 中也会被认为是一个事件,
// 可以在 emits 里面接收 handle (注意:要去掉 on)

scale 大屏

1
2
3
4
5
6
7
8
9
// 想像一个定位在正中心的 div
// 在 transform-origin: 0 0 的作用
div.style.transform = `scale(${w}) translate(-50%, -50%)`
// 该代码就是,先缩放再位移,
// 每次改变窗口重新设置该属性时会覆盖原有的,transform: translate(-50%, -50%);
// 就相当于没有位于正中心放大,而是偏右下一点,设置为左上理解为先放大再拽回来

// 下面的就可以不用设置,因为每次都是先定位到中心再缩放
div.style.transform = `translate(-50%, -50%) scale(${w})`

grid 布局

参考

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// 容器属性
1.display: grid 指定一个容器采用网格布局。display: inline-grid;

2.grid-template-columns 属性定义每一列的列宽,grid-template-rows 属性定义每一行的行高
repeat() 简化重复的值 grid-template-columns: repeat(3, 33.33%) 或 repeat(2, 100px 20px 80px);
auto-fill 自动填充 grid-template-columns: repeat(auto-fill, 100px);
fr 比例关系 grid-template-columns: 1fr 1fr;
minmax() 长度范围 grid-template-columns: 1fr 1fr minmax(100px, 1fr);
auto 浏览器自己决定长度 grid-template-columns: 100px auto 100px;
网格线的名称 grid-template-columns: [c1] 100px [c2] 100px [c3] auto [c4];
网格线的多个名称 [fifth-line row-5]

3.grid-row-gap 属性行间距,grid-column-gap 属性列间距,
grid-row-gap: 20px; grid-column-gap: 20px;
grid-gap 简写属性 grid-gap: <grid-row-gap> <grid-column-gap>;
grid-gap: 20px 20px; 省略了第二个值,浏览器认为第二个值等于第一个值。
根据最新标准,上面三个属性名的 grid-前缀已经删除

4.grid-template-areas 属性,指定"区域"
grid-template-areas: 'a b c'
'd e f'
'g h i';
grid-template-areas: "header header header"
"main main sidebar"
"footer footer footer";
如果某些区域不需要利用,则使用"点"(.)表示。

5.grid-auto-flow 属性,默认的放置顺序是"先行后列"
grid-auto-flow: column;
row dense 和 column dense 项目指定位置以后,剩下的项目怎么自动放置。
grid-auto-flow: row dense; 表示"先行后列",并且尽可能紧密填满,尽量不出现空格。

6.justify-items 属性,align-items 属性,place-items 属性
单元格内容的水平位置(左中右),设置单元格内容的垂直位置(上中下)
justify-items: start | end | center | stretch (默认值);
align-items: start | end | center | stretch (默认值);
place-items: <align-items> <justify-items>; 省略第二个值,则浏览器认为与第一个值相等

7.justify-content 属性,align-content 属性,place-content 属性
整个内容区域在容器里面的水平位置(左中右),align-content属性是整个内容区域的垂直位置(上中下)
justify-content: start | end | center | stretch | space-around | space-between | space-evenly;
align-content: start | end | center | stretch | space-around | space-between | space-evenly;
place-content: <align-content> <justify-content> 省略第二个值,浏览器就会假定第二个值等于第一个值

8.grid-auto-columns 属性,grid-auto-rows 属性,设置浏览器自动创建的多余网格的列宽和行高
grid-auto-rows: 50px; 新增的行高统一为50px

9.grid-template 属性,grid 属性
grid-template属性是grid-template-columns、grid-template-rows和grid-template-areas这三个属性的合并简写形式。
grid属性是grid-template-rows、grid-template-columns、grid-template-areas、 grid-auto-rows、grid-auto-columns、grid-auto-flow这六个属性的合并简写形式。


// 项目属性
1.grid-column-start 属性,grid-column-end 属性,grid-row-start 属性,grid-row-end 属性
项目的位置是可以指定的,具体方法就是指定项目的四个边框,分别定位在哪根网格线
grid-column-start: 2; grid-column-start: header-start; 指定为网格线的名字
grid-column-start: span 2; span关键字,表示"跨越",跨越2个网格

2.简写属性,grid-column 属性,grid-row 属性
grid-column属性是grid-column-start和grid-column-end的合并简写形式,
grid-row属性是grid-row-start属性和grid-row-end的合并简写形式。
grid-column: 1 / 3; grid-row: 1 / 3; grid-column: 1 / span 2;grid-row: 1 / span 2;

3.grid-area属性;指定项目放在哪一个区域。
grid-area: e;
grid-area属性还可用作grid-row-start、grid-column-start、grid-row-end、grid-column-end的合并简写形式,直接指定项目的位置。
grid-area: <row-start> / <column-start> / <row-end> / <column-end>;
grid-area: 1 / 1 / 3 / 3;

4.justify-self 属性,align-self 属性,place-self 属性
justify-self属性设置单元格内容的水平位置(左中右),跟justify-items属性的用法完全一致,但只作用于单个项目。
align-self属性设置单元格内容的垂直位置(上中下),跟align-items属性的用法完全一致,也是只作用于单个项目。

一键置灰

1
2
3
4
html {
filter: grayscale(.95);
-webkit-filter: grayscale(.95);
}

组件双向绑定函数写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<component :="createModel()"></component>
// 每次返回一个函数
// 由于该函数里面有响应式的 form
// 每次 form 改变会导致重新调用该函数返回一个新的对象(modelValue 每次都是最新的值)
const createModel = () => {
return {
modelValue: form.value,
'onUpdate:modelValue': (v: any) => form.value = v // 加 on 前缀是绑定事件
}
}

// 当然也可以使用 computed,这样上面就不用写括号调用了
const createModel = computed(() => {
return {
modelValue: form.value,
'onUpdate:modelValue': (v) => form.value = v
}
})

分页记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1. 新增、编辑、删除等操作后重新获取列表,不需要将页数重置到第一页

// 2. 当最后一页只有一条数据时候,删除后,重新请求当页没有数据 bug 不需要额外处理,
// 在分页组件中传了 size、tool 当没有那页数据时,current 会自动变为前一页
// current-change 会触发事件,然后重新发起一个请求,

// 3. 在使用查询的时候,一般需要将 current 变为第一页 (也可以不处理,原因如下)
// 如果不改变为第一页,请求无数据会自动将 current 改为第一页,原因同上,
// 请求的数据没有到当前页数,则 current 为最后一页。

// 注意:组件上面双向绑定 current-page 了手动改变后, current-change 不会触发事件
// 手动改变 current-page 超出最大页数,也可以成功。
// 组件没有绑定 current-page 的话,分页请求只能在 current-change,
// 也就不能在查询中改为第一页,组件无法同步显示值。(单项或双向绑定可以)

验证码代码记录

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<button @click="getCode" :disabled="showCode !== 0" >
{{showCode===0?'获取验证码':showCode}}
</button>

const showCode = ref(0)
const phone = ref('')
const getCode = () => {
if (showCode.value === 0) {
let timerPhone
clearInterval(timerPhone)
// 校验手机号
if (!verifyPhone(phone.value)) return
showCode.value = 60
timerPhone = setInterval(() => {
showCode.value--
if (showCode.value === 0) {
clearInterval(timerPhone)
}
}, 1000)
// 获取验证码
axios({
phone: phone.value
}).then(res => {
console.log('验证码', res);
}).catch((res) => {
clearInterval(timerPhone)
showCode.value = 0
})
}
}

const verifyPhone = (phone) => {
if (phone === '') {
uni.showToast({
title: '请输入手机号',
icon: 'none'
});
return false
} else {
// 手机号不为空
let verify = /^1(3|4|7|5|8)([0-9]{9})/.test(phone)
if (!verify) {
uni.showToast({
title: '手机号格式不正确',
icon: 'none'
});
return false
}
return true
}
}

Vite 兼容

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/* 1 使用 @vitejs/plugin-legacy 处理
安装 yarn add @vitejs/plugin-legacy terser -D
注意:使用该方案必须 vite 要在 3 以上版本
*/
// 配置如下
import legacyPlugin from "@vitejs/plugin-legacy";
plugins: [
legacyPlugin({
targets: ["defaults", "ie >= 11", "chrome 52"],
additionalLegacyPolyfills: ["regenerator-runtime/runtime"],
})
]

/* 2 使用 babel 处理 (当上面的方案使用后还是报错页面白屏,采用该方案)
参考:https://juejin.cn/post/7242220704288964666
安装 yarn add @rollup/plugin-babel -D
yarn add @babel/preset-env core-js@3 regenerator-runtime
*/
// main.ts 引入
import 'core-js/stable';
import 'regenerator-runtime/runtime';
// vite.config.ts
import { babel } from '@rollup/plugin-babel';
build: {
cssTarget: "chrome52",
target: "es2015",
rollupOptions: {
plugins: [
babel({
babelHelpers: "bundled",
plugins: [],
compact: false,
presets: [
[
"@babel/preset-env",
{
useBuiltIns: false, // 这里不能配置成 'entry'
targets: {
browsers: "last 2 versions and not dead, > 0.2%, Firefox ESR",
},
},
],
],
}),
],
},
},

// 该配置踩坑:useBuiltIns 要配置成 false 故 corejs 配置也就无效要删除了。
// 使用 babel 不能开启分包策略 否则报错
// main.ts 引用最好在最上面,确保在所有包加载之前 (不过,实际测试:不放在最上面也可以)

css 高度记录

csdn

1
2
3
4
5
6
7
css 父元素设置了 height: 100% 和 min-height: 100% 当内容超出后,高度还是 100%?
解决:
1.父元素不设置 height: 100% 即可,但是子元素无法使用 height:100% (参考上面的 csdn 解决)
如:添加祖先元素 display: flex; overflow: auto
父元素由于 align-items: stretch 父元素和祖先元素一样高,子元素可以使用 height: 100%
2.父元素要设置背景图片,子元素超出后,父元素不会变高,设置 height: fit-content;
背景图片会随着滚动来改变,父元素设置 overflow: auto; height: auto (滚动条在里面)

input 正整数输入

1
2
3
4
5
6
7
<el-input v-model.number="a" oninput="value=value.replace(/^0+(\d)|[^\d]+/g,'')" type="number" />
// oninput 里面的 value 相当于 this.value (这里无法直接修改 a)
// 注意:如果使用 type="number",@input 监听不到 - . 输入

// 下面的写法,v-model.number 不会转化成数字 (可以不写)
<el-input v-model.number="a" @input="v => a = v.replace(/^0+(\d)|[^\d]+/g, '')"></el-input>
// 注意 @input (这里无法直接修改 v)

new promise((resolve)=> resolve(new promise()))

1
2
3
4
5
6
在这种情况下,返回的新 Promise 对象会被立即执行,并且它的状态将决定外部 Promise 的状态。
如果内部的 Promise 对象被解决(resolved),外部的 Promise 对象也将被解决;
如果内部的 Promise 对象被拒绝(rejected),外部的 Promise 对象也将被拒绝。

这种行为是由 Promise 的规范所定义的。当你在 Promise 的回调函数中返回一个 Promise 对象时,
外部的 Promise 对象将等待内部的 Promise 对象完成,并采用相同的状态。

pinia 外部使用

pinia 在组件外部使用 参考

1
2
3
4
5
6
7
8
9
10
11
// 不推荐 注意:初始化了多个pinia 违反了pinia的单例模式,导致持久化插件失效
// 在外部 js 引入 pinia,传递给 use 函数
// pinia.js
import { createPinia } from 'pinia';
const pinia = createPinia();
export default pinia;
// 使用
import pinia from '@/stores/pinia';
import { useAccessToken } from '@/stores';

const store = useAccessToken(pinia);

v-model.number 输入不了小数、最后一位不能输入 0

1
2
3
4
5
// 注意这里不要使用 parseFloat 
// parseFloat('')为 NAN ,Number('') 则为 0
<el-input type="number" v-model="a" @blur="a = Number(a)" />

// 或者封装一个函数处理 blur 统一处理

触底加载

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<div class="about-item-scroll">
<div class="about-item-container">
<div class="about-item" v-for="item in []" :key="item.id">
</div>
</div>
</div>

// 触底加载
let isLoadingMore = false
let page = 1
const scrollHande = () => {
// 节流阀
if (!isLoadingMore) return
// 获取内容高度
var scrollH = document.querySelector('.about-item-container').scrollHeight
// 获取窗口高度 (这里不能使用offsetHeight, 因为:如果子元素大于父元素的高度时,offsetHeight 获取的是子元素的高度)
var innerH = document.querySelector('.about-item-scroll').clientHeight
// 获取滚出去的内容高度
var top = document.querySelector('.about-item-scroll').scrollTop

// 当内容还剩余200的高度未滚出的时候发送请求
// console.log(scrollH, top, innerH)
if (scrollH - top - innerH < 200) {
console.log('触底了')
// 发送请求
page++
init();
}
}
let isScrollEventBound = false; // 控制第一次请求后绑定事件(因为有的DOM在请求后才会出现)
const init = () => {
isLoadingMore = false
http.get('', { page, size }).then(res => {
if (res.code == 200) {
list = [...list.value, ...res.data]
isLoadingMore = !(page * size >= res.count)

if (!isScrollEventBound) {
nextTick(() => {
document.querySelector('.about-item-scroll').addEventListener('scroll', scrollHande)
isScrollEventBound = true;
})
}
}
loading.value = false

}).catch(() => {
loading.value = false
})
}
onBeforeUnmount(() => {
if (document.querySelector('.about-item-scroll')) {
document.querySelector('.about-item-scroll').removeEventListener('scroll', scrollHande)
}
})

表格高度计算记录

1
2
3
4
5
// 推荐使用 flex: 1,或者 grid 布局
// 不推荐使用 el-table 属性 height 来控制,表格上方高度不固定时,无法正确计算
// 不推荐使用 offsetTop 获取表格顶部高度,由于有些 DOM 需要在请求后渲染,
// 但是在请求后就获取 offsetTop 有时候 DOM 没渲染完成 (图片加载其他的),
// 会获取错误(需要 setTimeout 、nextTick 获取)

执行文本函数 Function 和 eval

csdn 参考

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
  function executeFunctionWithContext(context, callback) {
try {
if (Object.prototype.toString.call(context) === "[object Object]") {
const fn = new Function(...Object.keys(context), `return ${callback}`);
return fn(...Object.values(context));
} else {
return undefined;
}
} catch (error) {
return undefined;
}
}

const obj = { age: 18, name: "jack" };
console.log(executeFunctionWithContext(obj, "console.log(age)")); // undefined
console.log(executeFunctionWithContext(obj, "name == 'jack'")); // true
console.log(executeFunctionWithContext(obj, "console.log(errorName)")); // undefined

// 注意:在 context 对象没有某个名称是会报错,如没有 id,但是表达式 !id 就会出现问题

// 解决1:匹配所有的变量,但是下面的写法不能使用 String Boolean 需要排除(下面排除了常用的)
function executeFunctionWithContext(context, callback) {
try {
if (Object.prototype.toString.call(context) !== '[object Object]') {
return undefined;
}

// 1. 移除所有字符串内容(单引号、双引号、模板字符串),处理转义字符
const stringLiteralRegex = /('([^'\\]|\\.)*'|"([^"\\]|\\.)*"|`([^`\\]|\\.)*`)/g;
const cleanedCallback = callback.replace(stringLiteralRegex, '');

// 2. 提取变量名,排除保留字
const variableRegex = /\b[a-zA-Z_$][\w$]*\b/g;
const variables = (cleanedCallback.match(variableRegex)) || [];
const reservedWords = new Set([
'true', 'false', 'null', 'undefined', 'this', 'function', 'if', 'else', 'return',
'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default',
'delete', 'do', 'export', 'extends', 'finally', 'for', 'import', 'in', 'instanceof',
'new', 'switch', 'throw', 'try', 'typeof', 'var', 'void', 'while', 'with', 'yield'
]);
const filteredVariables = variables.filter(v => !reservedWords.has(v));
console.log(filteredVariables)
// 3. 去重并生成函数
const uniqueVariables = [...new Set(filteredVariables)];
const fn = new Function(...uniqueVariables, `return ${callback}`);
const args = uniqueVariables.map(varName => context[varName]);

return fn(...args);
} catch (error) {
return undefined;
}
}

// 解决2:开始添加默认值
function executeFunctionWithContext(context, callback) {
let ctx = { id: undefined, ...context}
}

// 解决3:使用 form.xxx 或者 row、modle 等

setup 中使用 beforeRouteEnter

csdn 参考

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 方案一 (注意:需要 vue3.3以上版本,低版本需要安装 unplugin-vue-define-options 包)
<script setup lang="ts">
import { ref, ComponentPublicInstance } from 'vue'
// * defineExpose暴露出来的方法,接口实现
interface IInstance extends ComponentPublicInstance {
getData(): void
}
defineOptions({
name: '***',
beforeRouteEnter(_to, _from, next) {
next((vm) => {
const instance = vm as IInstance
instance.getData() // 刷新列表数据(不缓存)
})
}
})

// 获取表格数据(示例方法)
const listData = ref([])
const getData = async () => {
listData.value = []
}

// * beforeRouteEnter中要用到的方法,需要暴露出来
defineExpose({ getData })
</script>


// 方案二
<script lang="ts">
import { defineComponent, ComponentPublicInstance } from 'vue'

interface IInstance extends ComponentPublicInstance {
setPathFrom(from: string): void
}

export default defineComponent({
beforeRouteEnter(to, from, next) {
next((vm) => {
const instance = vm as IInstance
instance.setPathFrom(from.path)
})
},
})
</script>

<script lang="ts" setup>
let pathFrom: string
const setPathFrom = (path: string) => {
pathFrom = path
console.log('vue-route::from::', pathFrom)
}

defineExpose({ setPathFrom })
</script>

表格分页选择回显

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
// 回显使用 toggleRowSelection 方法
// 只能通过对应的 row 而不是 id 来选中,故每次请求时候设置

// 使用 select 和 select-all 来实现
//selection-change 暂时不能因为 toggleRowSelection 会触发该方法

<template>
<div>
<el-popover width="600px" trigger="click">
<template #reference>
<el-button icon="Avatar" type="primary" plain>选择</el-button>
</template>
<div class="user-main">
<el-table ref="tableRef" class="user-main-table" :data="tableData" border stripe style="width: 100%"
v-loading="tableLoading" row-key="id" @select="handleSelection" @select-all="handleAllChange">
<el-table-column type="selection" width="40" reserve-selection
:selectable="() => type !== 'check'" />
<el-table-column prop="userName" label="用户昵称" show-overflow-tooltip />
<el-table-column prop="deptName" label="所属部门" show-overflow-tooltip />
<el-table-column prop="jobName" label="岗位名称" show-overflow-tooltip />
</el-table>
<el-pagination background layout="prev, pager, next" :page-size="pagination.pageSize"
:total="pagination.total" @current-change="handleCurrent" class="user-main-pagination" />
</div>
</el-popover>
</div>
</template>

<script setup>
import { ref, reactive, nextTick, onMounted, computed, watch } from 'vue'
const tableDataList = [
[
{
id: 1,
userName: '张三',
deptName: '研发部',
jobName: '前端开发'
},
{
id: 2,
userName: '李四',
deptName: '研发部',
jobName: '后端开发'
},
],
[
{
id: 3,
userName: '王五',
deptName: '研发部',
jobName: 'UI设计'
},
{
id: 4,
userName: '赵六',
deptName: '研发部',
jobName: '产品经理'
},
],
[
{
id: 5,
userName: '钱七',
deptName: '研发部',
jobName: '测试'
},
{
id: 6,
userName: '孙八',
deptName: '研发部',
jobName: '运维'
},
]
]

const props = defineProps({
userIds: { default: () => [] }
})
const emit = defineEmits(['update:userIds'])

// 表格
const tableLoading = ref(false)
const tableData = ref([])
const tableRef = ref()

// 分页
const pagination = reactive({
pageSize: 2,
current: 1,
total: 0
})

// 请求参数
const reqParams = reactive({
current: 1,
size: 2,
})

// 是否有其他页选中
const hasSelectionOnOtherPages = computed(() => {
return props.userIds.some(id => !tableData.value.some(row => row.id === id))
})

// 选中处理
const handleSelection = (selection, row) => {
const userIds = [...props.userIds]
const index = userIds.indexOf(row.id)
if (selection.includes(row) && index === -1) {
userIds.push(row.id)
} else if (!selection.includes(row) && index > -1) {
userIds.splice(index, 1)
}
emit('update:userIds', userIds)
}

const handleAllChange = (selection) => {
let currentPage = selection.filter(item => tableData.value.find(i => i.id == item.id))
const currentPageIds = tableData.value.map(item => item.id)
const selectedIds = new Set(props.userIds)
if (currentPage.length > 0) {
currentPageIds.forEach(id => selectedIds.add(id))
} else {
currentPageIds.forEach(id => selectedIds.delete(id))
}
emit('update:userIds', Array.from(selectedIds))
}

// 分页
const handleCurrent = (current) => {
reqParams.current = current
init()
}

// 初始化
const init = () => {
tableLoading.value = true
tableData.value = tableDataList[reqParams.current - 1]
pagination.total = 6
setTimeout(() => {
tableLoading.value = false
handleEcho()
}, 500)
}

// 回显
const handleEcho = () => {
nextTick(() => {
// 关键:先清空
tableRef.value?.clearSelection()
// 回显当前页
tableData.value.forEach(row => {
if (props.userIds.includes(row.id)) {
tableRef.value.toggleRowSelection(row, true)
}
})

// 回显虚拟行(触发半选状态)
if (hasSelectionOnOtherPages.value) {
const virtualRow = { id: '__VIRTUAL__', __virtual: true }
tableRef.value.toggleRowSelection(virtualRow, true)
}
})
}

watch(() => props.userIds, () => {
console.log(props.userIds, tableRef.value)
if(!tableRef.value) return
handleEcho()
}, { deep: true })

onMounted(() => {
init()
})
</script>

echarts 线上显示空白

参考 csdn

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
34
// 原因:由于 echars 机制原因 再次进入页面创建新的 canvas 需要清空之前的echarts图,
// 如果未清除,echarts 就会显示失败

// 解决一:离开页面时候销毁
<div id="echarts" ></div>
onUnmounted(() => {
if (myChart) {
echarts.dispose(myChart)
// 或者 myChart.dispose()
}
})

// 注意:使用 getInstanceByDom、clear、resize 等无效
// 在开发环境下第二次进来 getInstanceByDom 获取 undefined 可能是开发环境会自动销毁
// 生产环境第二次进来却获取得到之前的实例
myChart = echarts.getInstanceByDom('dom') // 这里是 dom 选择器
if(!myChart) { echarts.init('dom') } // 没有创建过才重新创建实例
// 生产环境中,清空后再重新绘制还是无效果(下面的方案)
myChart.clear(); // 清空画布
myChart.setOption() // 重新绘画
myChart.resize() // 重新加载

// 解决二:使用 ref 或者动态 dom 属性等
<div id="echarts" :class="`${new Date().getTime()}`"></div>
// 生产环境第二次进来 echarts.getInstanceByDom(refName.value) 也为 undefined
// 这样在生产环境每次 init 的 DOM 相当于不是同一个
<div ref="refName">
echarts.init(refName.value)

// 总结:
// 页面在 onMounted 里面就应该 init,
// 不应该在请求完成后,这样在未完成请求切换页面就会导致没有元素报错
// 使用 id 必须销毁,暂没找到其他方法
// 使用 ref 来 init 获取元素,也应该销毁,可以释放内存

npm 全局更新包

参考

1
2
3
4
5
6
7
8
9
// 检查过时的安装包
npm outdated [packageName]
// Package 显示包名。若使用了 --long / -l 则还是显示这个包属于 dependencies 还是devDependency
// Current 当前依赖包安装版本
// Wanted 根据 package.json 包版本前缀规则可以更新的最新版本号
// Latest 最新包版本号【默认情况下是最新的,这取决于开发人员的包管理制度】
// Location 是该依赖包在所居于的依赖树中所在的位置
// 更新依赖
npm update packageName (-D | -S)

el-table 出现 fixed 后滚动失灵

github issue csdn 类似问题 解决参考

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-table 没有写 height,缩放页面会出现滚动失灵

解决:https://blog.csdn.net/Phoebe_helloworld/article/details/127264228
原因:出现竖向滚动是由于固定列的伪元素导致的,所以只需要调整伪元素即可。
*/

.el-table__body-wrapper tr td.el-table-fixed-column--left.is-first-column::before,
.el-table__body-wrapper tr td.el-table-fixed-column--left.is-last-column::before,
.el-table__body-wrapper tr td.el-table-fixed-column--right.is-first-column::before,
.el-table__body-wrapper tr td.el-table-fixed-column--right.is-last-column::before,
.el-table__body-wrapper tr th.el-table-fixed-column--left.is-first-column::before,
.el-table__body-wrapper tr th.el-table-fixed-column--left.is-last-column::before,
.el-table__body-wrapper tr th.el-table-fixed-column--right.is-first-column::before,
.el-table__body-wrapper tr th.el-table-fixed-column--right.is-last-column::before,
.el-table__footer-wrapper tr td.el-table-fixed-column--left.is-first-column::before,
.el-table__footer-wrapper tr td.el-table-fixed-column--left.is-last-column::before,
.el-table__footer-wrapper tr td.el-table-fixed-column--right.is-first-column::before,
.el-table__footer-wrapper tr td.el-table-fixed-column--right.is-last-column::before,
.el-table__footer-wrapper tr th.el-table-fixed-column--left.is-first-column::before,
.el-table__footer-wrapper tr th.el-table-fixed-column--left.is-last-column::before,
.el-table__footer-wrapper tr th.el-table-fixed-column--right.is-first-column::before,
.el-table__footer-wrapper tr th.el-table-fixed-column--right.is-last-column::before,
.el-table__header-wrapper tr td.el-table-fixed-column--left.is-first-column::before,
.el-table__header-wrapper tr td.el-table-fixed-column--left.is-last-column::before,
.el-table__header-wrapper tr td.el-table-fixed-column--right.is-first-column::before,
.el-table__header-wrapper tr td.el-table-fixed-column--right.is-last-column::before,
.el-table__header-wrapper tr th.el-table-fixed-column--left.is-first-column::before,
.el-table__header-wrapper tr th.el-table-fixed-column--left.is-last-column::before,
.el-table__header-wrapper tr th.el-table-fixed-column--right.is-first-column::before,
.el-table__header-wrapper tr th.el-table-fixed-column--right.is-last-column::before {
bottom: 0px !important;
}

行内 onclick 与 DOM 绑定区别

1
2
3
4
5
6
7
8
9
/* 
行内绑定是直接写函数里面的内容 (函数体)
绑定是写一个函数
区别:比如 this
<div onclick="handleClick(this)">点击</div>
const handleClick = function (that) {
console.log(this, that) // windos, div
}
*/

el-input textarea 回显出现滚动条

1
2
3
:deep(.el-textarea) .el-textarea__inner {
overflow: hidden !important;
}

视口 meta

1
2
3
4
5
6
7
8
9
10
<meta name="viewport" content="width=device-width, initial-scale=1.0">
以下为没有 initial-scale=1.0 时
1. 当没有 width=device-width 时候 html 的宽 980px 高 2118.4px
2. 当有 width=device-width 时候 html 的宽高为 设备宽高
页面显示的最小宽度也就是 html 的宽度,但是如果 html 里面的元素大于 hmlt 的宽,也会在页面显示出来,
具体的值好像是(1500左右)

initial-scale 没有设置就不会缩放,为 1 就是按照物理像素来的。

通过更改这个元素在当前页面样式是不会重新加载的,(移动端 刷新也不行,需要关闭页面重新打开)

vue3 watch 记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* ref
一、watch 监听 ref 定义对象:
1. 直接写 () => xxx.value 和 xxx,需要开启深度;
2. 直接写 xxx.value 不需要写 deep。

二、watch 监听 ref 定义原始值:
1. 直接写 xxx.value 监听失效(不能监听一个具体的字符串);
2. () => xxx.value 可以的。

三、reactive 里面有对象 let a = reactive({ b: {} }) 可以直接写 a.b,不需要deep,箭头函数需要。

总结:
直接传入一个响应式对象,会隐式地创建一个深层侦听器,
一个返回响应式对象的 getter 函数,只有在返回不同的对象时,才会触发回调

推荐监听对象里面的值一般写 getter ,需要开启则写上 deep,如果直接写是原始值就会出现警告(防止值不确定)
注意:ref 直接写,里面的是对象,默认不深度监听

flush: 'post' 侦听器回调中能访问被 Vue 更新之后的 DOM
*/

mitt 使用记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1. 使用 mitt on 传递数据
bus.on("onMitt", (callBack) => {
callBack('数据');
});
bus.emit("emitMitt", (res) => {
console.log(res); // 数据
});

// 2. mitt 注销事件
bus.off('name') // 如果不传第二个参数,则注销所有的同名事件
bus.off('name', hanlde) // 注销为 name 并且时当前处理的函数

// 3. mitt emit 传递多个值
// 不同于 vue 的组件 emit 直接传递多个,mitt 只能传递一个值 payload

css 英文自动换行

参考:掘金

  • white-space,控制空白字符的显示,同时还能控制是否自动换行。值:normal | nowrap | pre | pre-wrap | pre-line
  • word-break,控制单词如何被拆分换行。它有三个值:normal | break-all | keep-all
  • word-wrap(overflow-wrap)控制长度超过一行的单词是否被拆分换行,值:normal | break-word
1
2
3
4
5
6
7
8
div {
word-wrap:break-word;
word-break:break-all;
}

/* word-break:break-all 所有单词碰到边界一律拆分换行,
而 word-wrap:break-word 只有当一个单词一整行都显示不下时,才会拆分换行该单词。
*/

git rebase

参考:掘金 掘金

1
2
3
4
5
6
7
8
9
10
11
12
13
// 拉取
git pull -r 或者 git pull --rebase
// 冲突
git add .
git rebase --continue

// 注意:
rebase 并没有进行合并操作,只是提取了当前分支的修改,将其复制在了目标分支的最新提交后面
git rebase xxx 是将当前分支变基到 xxx 下会导致溯源不能追踪了,当前分支就不知道从哪里来的
git pull -r 里面的是 git rebase origin/master 将当前分支变成远端的基座,之后就 push 同步远端
不能将 master 直接变基到其他分支,分支之间最好使用 merge

// 在分支开发中,可以先在开发分支变基为 master,再切换为 master 合并 merge 开发分支。

vue3 属性穿透

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 在 vue3 中,子组件没有接收的 props 会自动挂载组件根节点上面。

// 深层传递坑
// App.vue
<TestFather :name="999" :age="18"></TestFather>
// TestFather.vue
<component :is="Test" :age="10" :name="name"></component>
defineProps({
name: Number
})
// Test.vue
<div class="test">Tset{{ age }} {{ name }}</div>
defineProps({
name: Number,
age: Number
})

// 如果 TestFather 没有接收的 props 里面的 age,会自动继承到 Test 组件并且覆盖自己传递的age
// Test.vue 渲染的是 18(不管 TestFather.vue 里面 component 是否有传递 age 都是最外面的)

v-for 使用 ref

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
34
35
36
<el-form v-for="(formItem, formIndex) in formItems" :key="formIndex" :ref="setFormRef(formIndex)" />
const formRefs = reactive([])
// 注意这里的写法是返回一个函数的函数,DOM 处相当于直接先调用了函数再就是 DOM 绑定的函数。
// 与 :ref="(el) => setFormRef(formIndex, el)" 效果相同(推荐)
const setFormRef = (index) => (el) => {
formRefs[index] = el;
};
// 校验
const validateAndSubmit = async () => {
let validatePromises = formRefs.map(formRef => {
return new Promise((resolve, reject) => {
formRef.validate((valid, fields) => {
if (valid) {
resolve();
} else {
reject(fields);
}
});
});
});
try {
await Promise.all(validatePromises);
// 所有表单校验通过的逻辑
// 在这里可以触发后续事件
} catch (fields) {
// 校验失败的逻辑
}
};

// 第二中写法 ( v3.2.25 以上)
<li v-for="item in list" ref="itemRefs"></li>

// 注意:上面的第二种写法 ref 数组并不保证与源数组相同的顺序。(暂未发现,注意 Key 值不同)
// 第一种有闭包则不会,该函数只有在改 DOM 组件更新时被调用。
//如果没有使用传递 index,举例:而是直接 push (操作数据会重复push) 则会可能出现顺序不一致的情况
// 参考:https://juejin.cn/post/7069301529437536270

css flex 中挤压其他元素空间

参考

1
// 解决:设置 width: 0

input 正则校验金额

1
2
3
4
5
6
7
<el-input v-model="xxx" oninput="value = inputNumberValidate(value)"></el-input>
const inputNumberValidate = (v) => {
return v.replace(/^\.|[^\d.]/g, '').replace(/(\..*)\./g, '$1').replace(/^0+(\d+)/g, '$1')
}

// 最多两位小数,最后面添加
.match(/^\d*(\.?\d{0,2})/g)?.[0] || ''

原生 body 添加 vue 组件渲染

1
2
3
4
5
// import componentName from '.../index.vue'
const parent = document.createElement('div')
const instance = createApp(componentName).mount(parent)
const dom = instance.$el
document.body.appendChild(dom)

文本省略 Clamp.js 原生

Clamp.js

1
2
3
4
5
6
7
8
9
10
11
<script src="js/clamp.min.js"></script>
1、 单行省略
$clamp(myHeader,{clamp:1})
2、多行省略
$clamp(myHeader,{clamp:3})
3、根据有效的高度自动省略
$clamp(myparagraph,{clamp:'auto'})
4、基于固定元素高度的省略
$clamp(myparagraph,{clamp:'35px'})

注意:文本需要使用p标签或div标签,span不行。

getBoundingClientRect api 以及定位记录

实现下拉组件的记录

1
2
3
4
5
6
7
8
9
10
// 定位记录
1. 外层元素overflow:auto/scroll导致absolute定位的子元素溢出部分被隐藏
(https://blog.csdn.net/Wind_waving/article/details/123473622)
2. 子元素定位没有设置宽度,内容(文字)不会超出父元素

// getBoundingClientRect
方法返回一个 DOMRect 对象,其提供了元素的大小及其相对于视口的位置。
(https://developer.mozilla.org/zh-CN/docs/Web/API/Element/getBoundingClientRect)

下拉框的位置可以使用 popper.js 库来解决

BroadCastChannel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 连接到指定频道
const bc = new BroadcastChannel('channelName');

// 发送消息可以是任何类型的 Object
bc.postMessage('message')

// 接收消息 onmessage 同样可以
const listen = new BroadcastChannel('channelName');
listen.addEventListener("message", (event) => {
console.log(event.data)
})

// 在无法反序列化的消息到达频道时触发 onmessageerror 同样可以
listen.addEventListener("messageerror", (event) => { })

// 在控制台打印频道名称
console.log(bc.name); // "test_channel"

// 当完成后,断开与频道的连接
bc.close();