背景
使用 vue3 + 若依框架,点击切换左侧菜单,页面偶尔白屏,并且不报错。刷新页面后可以正常显示。继续点击,还是偶尔会出现相同的问题。
网上查找资料,发现出现这种问题,有可能是页面组件嵌套,重复使用 transition 引起的。在 components 文件夹下的 layout 文件夹中,AppMain 中使用了 transition,而 keep-alive 包裹的组件中又使用了 transition,导致页面出现白屏。
学习笔记
背景
使用 vue3 + 若依框架,点击切换左侧菜单,页面偶尔白屏,并且不报错。刷新页面后可以正常显示。继续点击,还是偶尔会出现相同的问题。
网上查找资料,发现出现这种问题,有可能是页面组件嵌套,重复使用 transition 引起的。在 components 文件夹下的 layout 文件夹中,AppMain 中使用了 transition,而 keep-alive 包裹的组件中又使用了 transition,导致页面出现白屏。
背景
使用 vue3 + elementPlus 做了一个树形表格,数据是懒加载的,发现删除子节点,页面不刷新。即使重新获取表格数据,一层一层展开,子节点还是存在,怀疑是缓存的问题。
网上查询后,发现有一个属性,可以获取树形表格缓存的数据,打印看一下。
背景
使用 vue3 + Echarts,在进行页面监听,重新渲染 Echarts 的时候,出现 Cannot read properties of undefined (reading 'type') 错误。
由于之前 vue2 中会在 data 中定义 Echarts 实例,并且通过 this.echarts.xxx() 来调用 API 并未出现异常,所以在 Vue3 惯性使用 ref 定义实例。但是 Vue3 使用了 proxy,Echarts 无法从中获取内部变量
背景
项目中要搞一个季度选择器,但是 element-ui 没有,所以网上搜索了一个大神写的,记录一下,方便以后使用。
<template>
<div class="el-quarter-picker">
<el-popover
v-model="visible"
:disabled="!canPopover"
:tabindex="null"
placement="bottom-start"
transition="el-zoom-in-top"
trigger="click">
<div class="el-date-picker">
<div class="el-picker-panel__body">
<div
class="el-date-picker__header el-date-picker__header--bordered"
style="margin:0px; line-height:30px">
<button
type="button"
@click="clickLast"
aria-label="前一年"
class="el-picker-panel__icon-btn el-date-picker__prev-btn el-icon-d-arrow-left"></button>
<span
role="button"
class="el-date-picker__header-label"
@click="clickYear"
>{{ title }}</span
>
<button
type="button"
@click="clickNext"
aria-label="后一年"
class="el-picker-panel__icon-btn el-date-picker__next-btn el-icon-d-arrow-right"></button>
</div>
<div class="el-picker-panel__content" style="margin:0px; width:100%">
<table class="el-month-table" style="">
<tbody>
<tr v-for="line in lineCount" :key="line">
<td
v-for="index in line * 4 <= viewList.length
? 4
: viewList.length - (line - 1) * 4"
:key="index"
:class="{
today: viewList[(line - 1) * 4 + index - 1].current,
current: viewList[(line - 1) * 4 + index - 1].active,
}">
<div>
<a
class="cell"
@click="clickItem(viewList[(line - 1) * 4 + index - 1])"
>{{ viewList[(line - 1) * 4 + index - 1].label }}</a
>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<el-input
slot="reference"
@change="changeText"
@mouseenter.native="mouseEnter"
@mouseleave.native="mouseLeave"
:placeholder="placeholder"
v-model="text"
:size="size"
:readonly="!canEdit"
:disabled="disabled">
<i slot="prefix" class="el-input__icon el-icon-date"></i>
<i
slot="suffix"
class="el-input__icon el-icon-circle-close"
v-show="showClear"
style="cursor:pointer"
@click.stop="clear"></i>
</el-input>
</el-popover>
</div>
</template>
<script>
export default {
name: 'ElQuarterPicker',
props: {
placeholder: {
type: String,
default: '',
},
size: {
type: String,
default: '',
},
readonly: {
type: Boolean,
default: false,
},
clearable: {
type: Boolean,
default: true,
},
editable: {
type: Boolean,
default: true,
},
disabled: {
type: Boolean,
default: false,
},
format: {
type: String,
default: 'yyyy年第Q季度',
},
valueFormat: {
type: String,
default: 'yyyy-qq',
},
value: {
type: String,
default: '',
},
},
model: {
prop: 'value',
event: 'change',
},
watch: {
value(val) {
// console.log('change-------', val)
this.changeValue(val);
},
readonly(val) {
this.canEdit = !val && this.editable;
this.canPopover = !this.disabled && !val;
},
editable(val) {
this.canEdit = !this.readonly && val;
},
disabled(val) {
this.canPopover = !val && !this.readonly;
},
},
data() {
return {
visible: false,
showClear: false, // 控制清空按钮展示
canEdit: true, // 是否可编辑
canPopover: true, // 选择器弹出是否可用
text: '', // 文本框值
viewType: 1, // 视图类型,1季度,2年度
viewYear: 0, // 当前年份
viewList: [], // 数据列表
lineCount: 0, // 数据行数
title: '', // 选择器标题
data: [0, 0], // 当前选择年度-季度
};
},
mounted() {
// console.log('mounted--------', this.value)
this.changeValue(this.value);
// 设置文本框是否可编辑
this.canEdit = !this.readonly && this.editable;
this.canPopover = !this.disabled && !this.readonly;
// 监听按键(上下左右键可以切换季度)
document.onkeydown = (event) => {
if (this.visible) {
const data = [this.data[0], this.data[1]];
if (data[0] < 1 || data[1] < 1) {
// 以当前季度为标准
const curDate = new Date();
data[0] = curDate.getFullYear();
data[1] = parseInt(curDate.getMonth() / 3) + 1;
}
if (event.code === 'ArrowLeft') {
// 上一个季度
if (data[1] === 1) {
data[0] = data[0] - 1;
data[1] = 4;
} else {
data[1] = data[1] - 1;
}
} else if (event.code === 'ArrowRight') {
// 下一个季度
if (data[1] === 4) {
data[0] = data[0] + 1;
data[1] = 1;
} else {
data[1] = data[1] + 1;
}
} else if (event.code === 'ArrowUp') {
// 上一年季度
data[0] = data[0] - 1;
} else if (event.code === 'ArrowDown') {
// 下一年季度
data[0] = data[0] + 1;
} else {
return;
}
// 超过年限的不处理
if (data[0] < 1000 || data[0] > 9999) {
return;
}
this.data = data;
this.viewType = 1;
this.viewYear = data[0];
this.$emit('change', this.formatTo(data, this.valueFormat));
}
};
},
destroyed() {
document.onkeydown = null;
},
methods: {
// 季度文本变更
changeText() {
if (this.checkFormat(this.format, this.text)) {
// 设置值
this.formatFrom(this.text, this.format);
this.$emit('change', this.formatTo(this.data, this.valueFormat));
} else {
// 输入了无效的格式,还原回原来的值
if (this.data[0] < 1 || this.data[1] < 1) {
this.text = '';
} else {
this.text = this.formatTo(this.data, this.format);
}
}
this.visible = false;
},
// 鼠标进入
mouseEnter() {
if (
!this.disabled &&
!this.readonly &&
this.clearable &&
this.text !== ''
) {
this.showClear = true;
}
},
// 鼠标离开
mouseLeave() {
if (!this.disabled && this.clearable) {
this.showClear = false;
}
},
// 清除季度
clear() {
this.showClear = false;
this.visible = false;
this.$emit('change', '');
},
// 季度值变更
changeValue(val) {
this.viewType = 1;
if (val) {
// 反向格式化
this.formatFrom(val, this.valueFormat);
this.text = this.formatTo(this.data, this.format);
this.viewYear = this.data[0];
} else {
this.text = '';
this.data = [0, 0];
this.viewYear = new Date().getFullYear();
}
this.initView();
},
// 初始化视图数据
initView() {
const list = [];
const curDate = new Date();
const curYear = curDate.getFullYear();
const curQuarter = parseInt(curDate.getMonth() / 3) + 1;
if (this.viewType === 1) {
let index = 0;
for (const i of '一二三四') {
index++;
const item = {
label: '第' + i + '季度',
year: this.viewYear,
quarter: index,
current: false,
active: false,
};
if (this.viewYear === curYear && index === curQuarter) {
item.current = true;
} else if (this.viewYear === this.data[0] && index === this.data[1]) {
item.active = true;
}
list.push(item);
}
this.title = this.viewYear + ' 年';
} else {
const start = parseInt(this.viewYear / 10) * 10;
this.viewYear = start;
for (let i = 0; i < 10; i++) {
const year = start + i;
const item = {
label: year + '',
year: year,
current: false,
active: false,
};
if (year === curYear) {
item.current = true;
} else if (year === this.data[0]) {
item.active = true;
}
list.push(item);
}
this.title = start + ' 年 - ' + (start + 9) + ' 年';
}
this.viewList = list;
this.lineCount = parseInt(list.length / 4);
if (list.length % 4 > 0) {
this.lineCount++;
}
},
// 校验季度格式是否正确
checkFormat(pattern, val) {
// 格式转成正则表达式
let text = '';
for (const char of pattern) {
const dict = '\\^$.+?*[]{}!';
if (dict.indexOf(char) === -1) {
text += char;
} else {
text += '\\' + char;
}
}
text = text.replace('yyyy', '[1-9]\\d{3}');
text = text.replace('qq', '0[1-4]');
text = text.replace('q', '[1-4]');
text = text.replace('Q', '[一二三四]');
text = '^' + text + '$';
const patt = new RegExp(text);
return patt.test(val);
},
// 格式化季度到指定格式
formatTo(data, pattern) {
let text = pattern.replace('yyyy', '' + data[0]);
text = text.replace('qq', '0' + data[1]);
text = text.replace('q', '' + data[1]);
text = text.replace('Q', '一二三四'.substr(data[1] - 1, 1));
return text;
},
// 以指定格式解析季度
formatFrom(str, pattern) {
const year = this.findText(str, pattern, 'yyyy');
const quarter = this.findText(str, pattern, ['qq', 'q', 'Q']);
this.data = [year, quarter];
},
// 查找文本数值
findText(str, pattern, find) {
if (find instanceof Array) {
for (const f of find) {
const val = this.findText(str, pattern, f);
if (val !== -1) {
return val;
}
}
return -1;
}
const index = pattern.indexOf(find);
if (index === -1) {
return index;
}
const val = str.substr(index, find.length);
if (find === 'Q') {
return '一二三四'.indexOf(val) + 1;
} else {
return parseInt(val);
}
},
// 年份点击
clickYear() {
if (this.viewType !== 1) {
return;
}
// 切换年度选择器
this.viewType = 2;
this.initView();
},
// 季度选择
clickItem(item) {
// console.log('select--------', item)
if (this.viewType === 1) {
// 选择季度
this.$emit(
'change',
this.formatTo([item.year, item.quarter], this.valueFormat)
);
this.visible = false;
} else {
// 选择年度
this.viewType = 1;
this.viewYear = item.year;
this.initView();
}
},
// 上一年
clickLast() {
if (this.viewYear > 1000) {
if (this.viewType === 1) {
this.viewYear--;
this.initView();
} else {
this.viewYear = this.viewYear - 10;
this.initView();
}
}
},
// 下一年
clickNext() {
if (this.viewYear < 9999) {
if (this.viewType === 1) {
this.viewYear++;
this.initView();
} else {
this.viewYear = this.viewYear + 10;
this.initView();
}
}
},
},
};
</script>
<style>
.el-quarter-picker {
width: 220px;
display: inline-block;
}
</style>
背景
项目中要搞一个季度选择器,但是 element-plus 没有,所以网上搜索了一个大神写的,记录一下,方便以后使用。
<template>
<div class="el-quarter-wrap">
<el-popover title="" content="" width="320" v-model="visible">
<template #reference>
<el-input
v-model="quarterDate"
placeholder="请选择季度"
clearable
:prefix-icon="Calendar"
readonly
@click.stop.native="visible = true"
@change="quarterDateChange">
<template #suffix>
<el-icon
v-if="quarterDate"
class="el-quarter-clear"
@click="clearData">
<Close />
</el-icon>
</template>
</el-input>
</template>
<div class="el-quarter__header">
<span
class="el-quarter-btn el-quarter-btn__pre"
@click="changeShowYear(-1)">
<el-icon>
<DArrowLeft />
</el-icon>
</span>
<div class="el-quarter__header-text" @click="showYearList">
{{ quarterTitle }}
</div>
<span
class="el-quarter-btn el-quarter-btn__next"
@click="changeShowYear(1)">
<el-icon>
<DArrowRight />
</el-icon>
</span>
</div>
<div class="el-quarter__content" v-if="!isEditYear">
<div class="el-quarter__row">
<span
class="quarter-index"
:class="{
'is-active': showYear === pickerYear && quarterIndex === 1,
}"
@click="pickerQuarte(1)"
>第一季度</span
>
<span
class="quarter-index"
:class="{
'is-active': showYear === pickerYear && quarterIndex === 2,
}"
@click="pickerQuarte(2)"
>第二季度</span
>
</div>
<div class="el-quarter__row">
<span
class="quarter-index"
:class="{
'is-active': showYear === pickerYear && quarterIndex === 3,
}"
@click="pickerQuarte(3)"
>第三季度</span
>
<span
class="quarter-index"
:class="{
'is-active': showYear === pickerYear && quarterIndex === 4,
}"
@click="pickerQuarte(4)"
>第四季度</span
>
</div>
</div>
<div class="el-year__content" v-else>
<div class="el-year-item" v-for="item in yearList">
<div
class="cell"
:class="{ 'is-active': showYear == item }"
@click="selectYear(item)">
{{ item }}
</div>
</div>
</div>
</el-popover>
</div>
</template>
<script lang="ts" setup>
import {
DArrowLeft,
DArrowRight,
Close,
Calendar,
} from '@element-plus/icons-vue';
import { computed, onMounted, reactive, ref } from 'vue';
let visible = ref(false);
const props = defineProps({
modelValue: {
type: String,
default: '',
},
});
const emits = defineEmits(['update:modelValue', 'change']);
// 绑定日期
let quarterDate = ref('');
// 选择的年
let pickerYear = ref('') as any;
// 展示的年
let showYear = ref('') as any;
// 选择的季度
let quarterIndex = ref(0);
// 是否展示年份列表
let isEditYear = ref(false);
// 年份列表开始年份
let startYear = ref('') as any;
// 年份列表
let yearList = reactive([] as any);
const quarterTitle = computed(() => {
if (isEditYear.value) {
return startYear.value + '年 - ' + (startYear.value + 9) + '年';
} else {
return showYear.value + '年';
}
});
// 选择某季度
function pickerQuarte(index: number) {
quarterIndex.value = index;
pickerYear.value = showYear.value;
let oldValue = quarterDate.value; // 记录上一次数据
quarterDate.value = pickerYear.value + '-Q' + index;
emits('update:modelValue', quarterDate.value);
// 新老数据不一致,触发change时间
if (oldValue !== quarterDate.value) {
emits('change', quarterDate.value);
}
}
// 更改展示的年
function changeShowYear(num: number) {
if (isEditYear.value) {
startYear.value = startYear.value + num * 10;
// console.log('startYear.value', startYear.value)
changeYearList();
} else {
showYear.value = showYear.value + num;
}
}
// 清空选择的数据
function clearData() {
quarterDate.value = '';
pickerYear.value = '';
showYear.value = new Date().getFullYear();
quarterIndex.value = 0;
}
// 选择的数据
function quarterDateChange(value: any) {
const splitArray = value.split('-Q');
if (splitArray.length < 2) {
pickerYear.value = '';
showYear.value = new Date().getFullYear();
quarterIndex.value = 0;
} else {
pickerYear.value = splitArray[0];
showYear.value = splitArray[0];
quarterIndex.value = splitArray[1];
}
}
// 更改年份列表函数
function changeYearList() {
yearList = [];
let year = startYear.value;
for (let i = 0; i < 10; i++) {
yearList.push(year++);
}
}
// 切换展示年份列表 和 季度
function showYearList() {
if (!isEditYear.value) {
startYear.value = Number(Math.floor(showYear.value / 10) + '0');
changeYearList();
isEditYear.value = true;
} else {
isEditYear.value = false;
}
}
// 选中某个年份列表
function selectYear(item: any) {
showYear.value = item;
isEditYear.value = false;
}
onMounted(() => {
// 初始化展示的年为当前年份
showYear.value = new Date().getFullYear();
startYear.value = Number(Math.floor(showYear.value / 10) + '0');
changeYearList();
});
</script>
<style lang="scss">
.el-quarter__header {
padding-bottom: 12px;
border-bottom: 1px solid #ebeef5;
display: flex;
align-items: center;
justify-content: space-between;
.el-quarter-btn {
font-size: 12px;
}
.el-quarter__header-text {
font-size: 16px;
font-weight: 500;
text-align: center;
cursor: pointer;
}
}
.el-quarter__content {
min-height: 100px;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: space-around;
.el-quarter__row {
display: flex;
justify-content: space-around;
.quarter-index {
display: flex;
padding: 4px 10px;
width: fit-content;
cursor: pointer;
&:hover {
color: #337ecc;
}
}
.is-active {
color: #409eff;
}
}
}
.el-quarter-clear {
position: relative;
color: #909399;
display: none;
height: 12px;
width: 12px;
cursor: pointer;
&::after {
content: '';
position: absolute;
height: 14px;
width: 14px;
margin: auto;
border-radius: 50%;
border: 1px solid #909399;
}
}
.el-input {
&:hover {
.el-quarter-clear {
display: flex;
}
}
}
.el-year__content {
min-height: 100px;
display: flex;
padding: 10px 0;
flex-wrap: wrap;
.el-year-item {
width: calc(100% / 4);
display: flex;
align-items: center;
justify-content: center;
.cell {
padding: 4px 10px;
width: fit-content;
cursor: pointer;
cursor: pointer;
white-space: nowrap;
&:hover {
color: #337ecc;
}
}
.is-active {
color: #409eff;
}
}
}
</style>
背景
想为答题小程序中的模拟考试功能,增加一个弹出显示考试成绩海报的功能,可以让用户下载图片。使用了原生的 canvas 绘制,但是发现图片模糊。
参考了网上其他网友的方法,在画布上进行绘制的时候,先放大尺寸,然后在 scale 缩小,或者 setTransform 缩小,发现都没解决。另辟蹊径,最终使用 image 图片显示,解决了这个问题。
这就牵扯出一个概念,物理分辨率 和 逻辑分辨率。现在设备基本上都是高分屏,通常说的我们说的某个手机分辨率是物理分辨率,比如 iPhone 13 Pro 的物理分辨率是 1170 x 2532,但是逻辑分辨率是 390 x 844,也就是说,物理分辨率是 3 倍于逻辑分辨率( dpr = 3)。我们一般操作都是基于逻辑分辨率,但是实际显示的时候,会根据 dpr 进行放大或者缩小。
背景
一段时间没有使用电脑上的 Docker,今天启动的时候,报了 “Docker Desktop -Virtual Machine Platform not enabled Virtual Machine Platform is not enabled. Enable it using the PowerShell script (in anadministrative PowerShell) and restart your computer before using...” 的错误。网上查了下,是没有启用 虚拟机平台 , 有可能是之前更新系统,默认给关闭了。启动一下,就可以正常使用 Docker 了。
背景
给一个旧的后台管理项目,增加一个从详情页返回,不刷新页面的功能。使用 vue 的 keep-alive 组件 include 属性,但是发现不生效,页面刷新了。网上查了下,这个 include,是根据组件的 name 属性来匹配的,需要把组件的 name 属性加上去改成与 include 中的一致。顺便再复习下 keep-alive 的用法。
keep-alive 是 Vue 提供的一个内置组件,用于缓存组件的状态,避免组件在切换时重新渲染。它通常用于路由组件的缓存,以提升用户体验。
背景
春节过后,deepseek 大火,导致线上的服务经常崩溃,于是想着自己在本地部署一个,方便自己使用。
现在网上大部分方案,都是通过 Ollama 来实现的。但是这个方案需要安装 Ollama,要想聊天界面更友好,还要安装一个 chatbox,并且每次启动都要敲命令。我自己的电脑用的是 intel 的独立显卡,要想使用独显去跑模型,还得装个转译的包,还是挺麻烦的。
下边介绍一个比较简单的方式。
背景
给一个老项目添加功能,修改后保存,直接报 Module Warning(from./node modules/eslint-loader/index.js):error:Missing space beforefunction parentheses(space-before-function-paren)。
原因是我的 vscode 默认使用 prettier,prettier 格式化 javaScript 代码之后,默认不会在函数与 () 添加空格,而 eslint 默认情况下则要求函数与 () 之间必须有一个空格。处理起来大概有这么几种方案: