组件 defineModel 实现

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
const props = defineProps({
propData: {
type: null,
default: null
}
})

const emit = defineEmits(['update:propData'])

const internalData = ref(props.propData)
const data = computed({
get: () => internalData.value,
set: (val) => {
internalData.value = val
emit('update:propData', val)
}
})

// 监听 props.propData 的变化,如果有变化则更新 internalData
watch(
() => props.propData,
(newVal) => {
internalData.value = newVal
},
{ deep: true }
)

// ------- 使用 defineModel -------
const data = defineModel('propData')
// 注意:如果为 defineModel prop 设置了一个 default 值且父组件没有为该 prop 提供任何值,
// 会导致父组件与子组件之间不同步。
data.value = xxx

拖拽宽度

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
<style>
#box {
display: flex;
height: 500px;
overflow: hidden;
}

#left {
width: 250px;
height: 100%;
background: skyblue;
}

#resize {
width: 1px;
height: 100%;
padding: 0 5px;
margin: 0 -5px;
cursor: w-resize;
}

#right {
flex: 1;
height: 100%;
background: tomato;
}
</style>

<div id="box">
<div id="left"></div>
<div id="resize"></div>
<div id="right"></div>
</div>

<script>
window.onload = function () {
var resize = document.getElementById("resize");
var left = document.getElementById("left");
resize.onmousedown = function (e) {
var startX = e.clientX;
resize.left = resize.offsetLeft;
document.onmousemove = function (ee) {
var endX = ee.clientX;
let leftWidth = resize.left + (endX - startX)
let maxWidth = 400;
if (leftWidth < 200) leftWidth = 200
if (leftWidth > maxWidth) leftWidth = 400

resize.style.left = leftWidth;
left.style.width = leftWidth + 'px';
}

document.onmouseup = function () {
document.onmousemove = null;
document.onmouseup = null;
}
return false;
}
}
</script>

nvm 配置 settings.txt

1
2
node_mirror: https://npmmirror.com/mirrors/node/
npm_mirror: https://npmmirror.com/mirrors/npm/

node-sass 安装失败

1
2
3
4
// 设置代理
npm config set proxy http://127.0.0.1:7890
// 下载完成后删除 http 代理
npm config delete proxy

nvm 安装不上node npm

csdn 方案一 csdn 方案二

1
2
3
// 原因:nvm 版本为 1.2.2 安装不上 node 14
// 需要配置镜像
// 解决:安装 1.1.12,或者手动安装 node

vue router 覆盖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// vue3 router 4 中 路由覆盖
// 在第一个路由有 name 时候才会出现被覆盖的情况,如果没有则不会
// 第二个也必须要有 name 且和第一个相同(动态路由添加同理)
const routes = [
{
path: "/index",
name: "index",
component: () => import("@/views/index.vue"),
},
{
path: "/index",
name: "index",
component: () => import("@/views/index1.vue"),
}
];

// vue2 router 3 中覆盖
// 不会被覆盖,出现相同名称,会有警告

git 文件大小写

1
2
3
4
5
6
7
8
9
10
11
12
// 文件名称 file/tset.vue 添加后,修改为Test.vue 后 git 还是无效
git config core.ignorecase false
// 提交后线上有两份文件,之前的小写还在
git rm --cached file/tset.vue
git rm --cached file -r
git rm -r --cached .
// 其他方案:直接网上仓库原小写删除文件,
// git pull 后大写文件可能会被删除,放弃取消更改


// 注意:最好使用 git 命令修改文件大小写名称
git mv -f a.js A.js

el-cascader 虚拟滚动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 参考:https://juejin.cn/post/7041774671838052365
// 参考:https://juejin.cn/post/7070751092862091272

1.复制文件 https://github.com/raintoway/element/tree/cascader-virtual-scroll
2.修改 cascader-panel 路径引入
3.core-js 安装加 main.js 引入
4.scrollbar 全部注释,换成使用自己项目中的,或者 'element-ui/lib/scrollbar'
5.cascader-menu 中 virtual-list 添加 style={{"height": "200px","overflowY": "auto" }}
6.生成虚拟列表的key,当nodes变化时强制重新创建虚拟列表组件,解决vue-virtual-scroll-list在data-sources变化时内部缓存未正确重置的问题

virtualListKey() {
const nodes = this.nodes;
if (!nodes || nodes.length === 0) return `vl-${this.index}-empty`;
return `vl-${this.index}-${nodes[0].uid}-${nodes[nodes.length - 1].uid}-${nodes.length}`;
},

cascader-menu 中 virtual-list 添加 key={this.virtualListKey}
7.renderEmptyText 传递 h
8.使用 props 里面 virtualScrool 为 true

el-table showOverflowTooltip 复制

1
2
3
4
5
6
7
8
9
10
11
12
https://segmentfault.com/a/1190000044053500#comment-area

// 注意:
// fix: 快速一个单元格接着再快速移入另外的单元格最后再快速移除表格最后的一个提示不会移除
handleCellMouseLeave() {
if (tooltip) {
...
// 离开时先清理进入定时器,避免因之前排队的进入回调再次打开
clearTimeout(tooltip.timeoutEnter);
...
}
}

正则金额校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const regPositiveUpToTwoDecimals = /^(?!0+(\.0+)?$)([1-9]\d*|0)(\.\d{1,2})?$/; // >0 且最多两位小数
const regNonNegativeUpToTwoDecimals = /^(?:[1-9]\d*|0)(?:\.\d{1,2})?$/; // >=0 且最多两位小数
const regNonNegativeInteger = /^(?:[1-9]\d*|0)$/; // >=0 且为整数
const regPositiveInteger = /^[1-9]\d*$/; // >0 且为整数

function createNumberValidator(reg, message) {
return (rule, value, callback) => {
// 空值处理
if (value === '' || value === null || value === undefined) {
return callback(new Error('请输入'));
}
const str = String(value).trim();

if (!/^(?:0|[1-9]\d*)(?:\.\d+)?$/.test(str)) {
return callback(new Error('请输入正确的数字'));
}
// 正则校验
if (!reg.test(str)) {
return callback(new Error(message));
}
callback(); // 校验通过
};
}

Big.js 使用案例

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
import Big from "big.js";

// 合计
tool.sum = (params = []) => {
return +params.reduce((acc, val) => acc.plus(new Big(val || 0)), new Big(0)).toString();
};

// 乘(四舍五入)
tool.mul = (params = [], decimal = 2) => {
const result = params.reduce((acc, val) => acc.times(new Big(val || 0)), new Big(1));
return +result.round(decimal).toString();
};

// 乘后向下取整
tool.mul_down = (params = [], decimal = 2) => {
const result = params.reduce((acc, val) => acc.times(new Big(val || 0)), new Big(1));
const factor = new Big(10).pow(decimal);
const floored = result.times(factor).floor().div(factor);
return +floored.toString();
};

export function big(v) {
return new Big(v || 0);
}

// 转为数字,默认保留 2 位,type: roundDown = 0,roundHalfUp = 1,roundHalfEven = 2,roundUp = 3
// toFixed 同理
export function bigToNumber(value, decimals = 2, type = 'roundHalfUp') {
return big(value).round(decimals, Big[type]).toNumber();
}

JS 四舍五入、舍弃多余小数

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
// 1.tofixed 方案 bug
num.toFixed(2)
// (1.005).toFixed(2) // 会出现 1.00

// 2.Math.round
Math.round(num * 100) / 100;
// 1.005 会出现精度问题

// 3.Number.EPSILON
Math.round((num + Number.EPSILON) * 100) / 100;
// 5.015 出现错误

// 4.配合幂运算(推荐)
Number(Math.round(num + 'e2') + 'e-2')

// 5.使用字符串截取 (繁琐)
function roundByString(num, decimals = 2) {
if (typeof num !== 'number' || isNaN(num)) {
return NaN;
}

const isNegative = num < 0;
const absStr = String(Math.abs(num));
let [integerPart, decimalPart = ''] = absStr.split('.');

// 补足到 decimals + 1 位小数(用于判断进位)
decimalPart = (decimalPart + '0'.repeat(decimals + 1)).slice(0, decimals + 1);

// 拼接整数和小数部分(共 decimals+1 位小数)
let combined = integerPart + decimalPart; // e.g., "1005" for 1.005 with decimals=2

// 在倒数第1位前加小数点,变成 "100.5"
const dotIndex = combined.length - 1;
const numberWithDecimal = combined.slice(0, dotIndex) + '.' + combined.slice(dotIndex);
let numberToRound = Number(numberWithDecimal);

// 四舍五入(此时是对 .5 四舍五入)
numberToRound = Math.round(numberToRound);

// 格式化结果:确保有足够位数
let resultStr = String(numberToRound).padStart(integerPart.length + decimals, '0');
const newInteger = resultStr.slice(0, -decimals) || '0';
const newDecimal = resultStr.slice(-decimals).padEnd(decimals, '0');

const finalStr = (isNegative ? '-' : '') + newInteger + '.' + newDecimal;
return Number(finalStr);
}

// ---------
// 只保留两位小数没有四舍五入,多余的直接舍弃。
function toFixedTrunc(num, decimals = 2) {
if (typeof num !== 'number' || isNaN(num)) {
return '0.' + '0'.repeat(decimals);
}
const [integer, decimal = ''] = String(num).split('.');

const fixedDecimal = (decimal + '0'.repeat(decimals)).slice(0, decimals);
return `${integer}.${fixedDecimal}`;
}

// 使用 big
new Big(a).toFixed(2, 0)

// 添加百分比
Number(num + 'e2') + '%'

组件的 inheritAttrs

1
2
3
4
5
6
7
8
9
10
11
// 注意 class 和 style 是智能合并的,不受 inheritAttrs: false 影响
// 所有未被组件 props 声明的 attribute(属性),会自动应用到组件的 根元素上

<!-- 组件模板 -->
<template>
<button class="btn-default">Click</button>
</template>
<!-- 使用 -->
<my-button class="btn-primary"></my-button>
<!-- 渲染 -->
<button class="btn-default btn-primary">Click</button>

组件同步、异步加载

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
// 同步 vs 异步
import Comp from './Comp.vue'
components: { Comp }
// 优点:立即可用,mounted 可访问 $refs,无闪烁
// 缺点:打包进主 bundle,增大首屏体积

components: {
Comp: () => import('./Comp.vue')
}
// 优点:代码分割、按需加载、主包更小
// 缺点:首次渲染有延迟(可能闪烁),mounted 拿不到 $refs

// 1. 触发代码分割的前提:至少有一个() => import()(如路由懒加载)
// C 是首屏必需内容 → 同步引入,C 由交互触发(如弹窗)→ 异步引入

// 注意:不要同时写顶部 import + 异步注册,否则组件被同步打包,异步失效,还导致闪烁。

// 打包行为
// A、B 同步引入 C → × 不拆,C 打进app.js (无异步边界,splitChunks 不生效)
// A、B 异步路由 + 同步引入 C → √ 拆出 C(C 被多个异步 chunk 共享,自动提取)
// A、B 异步路由 + 异步引入 C → √ 拆出 C(显式动态导入,自然分离)

// 加载时机
// A 同步引入 C → A 与 C 并行加载,首屏一次性渲染
// A 异步引入 C → 先加载 A,用到 C 时才加载,可能有延迟或闪烁

浏览器缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 1. 强缓存
// • 由 Cache-Control 控制(优先级高于 Expires)
// • 常见值:
// max-age=N:缓存 N 秒,期间不发请求
// no-cache:跳过强缓存,走协商验证
// no-store:禁止缓存
// • 命中时无网络请求,状态为 200 (from cache)

// 2. 协商缓存
// • 强缓存未命中时触发
// • 请求带 If-None-Match(ETag)或 If-Modified-Since(Last-Modified)
// • 服务器返回 304 → 用本地缓存;返回 200 → 用新内容

// 3. 启发式缓存
// • 仅在无 Cache-Control/Expires 时启用
// • 有 Last-Modified:缓存 ≈ (Date - Last-Modified) × 10%
// • 无 Last-Modified:多数浏览器不缓存(或缓存极短时间)
// • 缓存过期后,如果本地缓存存有 ETag 或 Last-Modified,发起请求时会带上 If-None-Match 或 If-Modified-Since 走协商缓存;
// • 如果没有这些验证信息,则只能发起普通 GET 请求重新下载(返回 200)。

// 4. 刷新行为
// • 地址栏回车:完全遵循缓存策略,可能无任何请求
// • F5:HTML 发请求并带 Cache-Control: max-age=0,强制协商缓存;子资源仍可能命中强缓存(不发请求)
// • Ctrl+F5:所有资源发请求,带 Cache-Control: no-cache 和 Pragma: no-cache,强制对所有资源发起请求

实现防抖节流

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
function debounce(func, wait = 500, immediate = false) {
let timer;
const debounced = function (...args) {
if (immediate && !timer) func.apply(this, args);
clearTimeout(timer);
timer = setTimeout(() => {
if (!immediate) func.apply(this, args);
timer = null;
}, wait);
};
debounced.cancel = () => {
clearTimeout(timer)
timer = null;
};
return debounced;
}

function throttle(func, wait = 500, immediate = true) {
let timer;
const throttled = function (...args) {
if (timer) return;
if (immediate) func.apply(this, args);
timer = setTimeout(() => {
if (!immediate) func.apply(this, args);
timer = null;
}, wait);
};
throttled.cancel = () => {
clearTimeout(timer);
timer = null;
}
return throttled;
}

match matchAll exec

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
// match,带上 g
let myRe = /d(b+)(c*)d/g;
let str = 'cdbbcdbsbzdbd';
str.match(myRe);
// 执行结果 => ["dbbcd", "dbd"]
// 执行完后 myRe.lastIndex 为 0

let myRe = /d(b+)(c*)d/; // 不带上 g
let str = 'cdbbcdbsbzdbd';
str.match(myRe);
// 执行结果 => ["dbbcd", "bb", "c", index: 1, input: "cdbbcdbsbzdbd"]
// 执行完后 myRe.lastIndex 为 0

// matchAll,正则表达式必须携带 g 标志。
let myRe = /d(b+)(c*)d/g;
let str = 'cdbbcdbsbzdbd';

[...str.matchAll(myRe)];
// 执行结果 =>
// [
// ["dbbcd", "bb", "c", index: 1, input: "cdbbcdbsbzdbd"],
// ["dbd", "b", "", index: 10, input: "cdbbcdbsbzdbd"]
// ]
// 执行完后 myRe.lastIndex 为 0

// exec 函数
let myRe = /d(b+)(c*)d/;
let str = 'cdbbcdbsbzdbd';
myRe.exec(str);
// 第 1 次执行结果 => ["dbbcd", "bb", "c", index: 1, input: "cdbbcdbsbzdbd"]
// 执行完后 myRe.lastIndex 为 0,下一次 exec 将从原始字符串下标 0 开始检索
myRe.exec(str);
// 第 2 次执行结果 => ["dbbcd", "bb", "c", index: 1, input: "cdbbcdbsbzdbd"]
// 执行完后 myRe.lastIndex 为 0,下一次 exec 将从原始字符串下标 0 开始检索
myRe.exec(str);
// 第 3 次执行 => ["dbbcd", "bb", "c", index: 1, input: "cdbbcdbsbzdbd"]
// 执行完后 myRe.lastIndex 为 0,下一次 exec 将从原始字符串下标 0 开始检索

let myRe = /d(b+)(c*)d/g;
let str = 'cdbbcdbsbzdbd';
myRe.exec(str);
// 第 1 次执行结果 => ["dbbcd", "bb", "c", index: 1, input: "cdbbcdbsbzdbd"]
// 上面结果中的 "bb" 就是第一个括号捕获的内容,"c" 就是第二个括号捕获的内容
// 执行完后 myRe.lastIndex 为 6,下一次 exec 将从原始字符串下标 6 开始检索
myRe.exec(str);
// 第 2 次执行结果 => ["dbd", "b", "", index: 10, input: "cdbbcdbsbzdbd"]
// 上面结果中的 "b" 就是第一个括号捕获的内容,"" 就是第二个括号捕获的内容
// 执行完后 myRe.lastIndex 为 13,下一次 exec 将从原始字符串下标 13 开始检索
myRe.exec(str);
// 第 3 次执行 => null
// 执行完后 myRe.lastIndex 为 0,下一次 exec 将从原始字符串下标 0 开始检索
myRe.exec(str);
// 第 4 次执行结果 => ["dbbcd", "bb", "c", index: 1, input: "cdbbcdbsbzdbd"]
// 执行完后 myRe.lastIndex 为 6