背景
项目中,对于结构复杂的树形数据选择,一般会使用懒加载的方式。但是懒加载的同时,又需要实现远程搜索,这时候就需要对 el-tree-select 进行一些处理了。
注意,当页面上有多个 el-tree-select 组件时,如果没有配置 value-key,对一个组件进行搜索选中操作,然后点击第二个组件时,可能会导致上一个组件的显示值变成一个 id 值。
背景
项目中,对于结构复杂的树形数据选择,一般会使用懒加载的方式。但是懒加载的同时,又需要实现远程搜索,这时候就需要对 el-tree-select 进行一些处理了。
注意,当页面上有多个 el-tree-select 组件时,如果没有配置 value-key,对一个组件进行搜索选中操作,然后点击第二个组件时,可能会导致上一个组件的显示值变成一个 id 值。
背景
el-table 中,对于表格单元格的合并,文档中有介绍。原理就是通过设置span-method
方法,返回一个二维数组,数组中的每一项表示一行中每个单元格的合并情况。
但是这种方法针对已知表格中的某些单元格需要合并的情况,如果需要合并的单元格位置是动态的,那么这种方法就需要改造下了。
<template>
<div>
<el-table :data="tableData" :span-method="objectSpanMethod" border style="width: 100%; margin-top: 20px">
<el-table-column prop="id" label="ID" width="180" />
<el-table-column prop="name" label="Name" />
<el-table-column prop="amount1" label="Amount 1" />
<el-table-column prop="amount2" label="Amount 2" />
<el-table-column prop="amount3" label="Amount 3" />
</el-table>
</div>
</template>
<script setup>
//重点就是这个方法,返回一个数组,用来描述每行每个单元格的合并情况
const objectSpanMethod = ({ row, column, rowIndex, columnIndex }) => {
if (columnIndex === 0) {
if (rowIndex % 2 === 0) {
return {
rowspan: 2,
colspan: 1
}
} else {
return {
rowspan: 0,
colspan: 0
}
}
}
}
const tableData = [
{
id: '12987122',
name: 'Tom',
amount1: '234',
amount2: '3.2',
amount3: 10
},
{
id: '12987123',
name: 'Tom',
amount1: '165',
amount2: '4.43',
amount3: 12
},
{
id: '12987124',
name: 'Tom',
amount1: '324',
amount2: '1.9',
amount3: 9
},
{
id: '12987125',
name: 'Tom',
amount1: '621',
amount2: '2.2',
amount3: 17
},
{
id: '12987126',
name: 'Tom',
amount1: '539',
amount2: '4.1',
amount3: 15
}
]
</script>
背景
正在做一个考核管理系统,需要用到附件的上传以及回显。项目有 vue 做的 pc 端,以及 uniapp 做的 H5 移动端,做的时候发现二者的回显稍有不同,记录一下。因为附件只牵扯到 pdf 和图片,所以只写了这两种的回显。
function reviewFile (file) {
let fileType = ''
if (file.fileName.indexOf('.pdf') != -1) {
fileType = 'application/pdf'
} else {
fileType = 'image/png'
}
axios({
url: process.env.VUE_APP_BASE_URL + '/file/download/' + file.file,
method: 'get',
responseType: 'blob', // 一定要设置响应类型为 blob
headers: { token: token } // 对于有鉴权的接口,需要添加token
}).then(res => {
// 创建用于作为附件预览源的Blob
const blob = new Blob([res.data], { type: fileType });
// 创建用于作为PDF预览源的Blob的URL
let url = URL.createObjectURL(blob);
window.open(url)
})
},
背景
使用 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 了。