Vue
通过响应数据动态修改css的变量值
方法一:
vue
<template>
<div class="root">
<!-- Your component content here -->
<div class="right" :style="{ '--foo': color }"></div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const color = ref<string>('#FF0000');
</script>
<style scoped lang="less">
.root {
.right {
flex: 1;
background-color: var(--foo, #7f583f);//--foo变名,#7f583f是默认值
}
}
/* Your styles here */
</style>方法二(vue3中)通过v-bind:
vue
<template>
<div class="root">
<!-- Your component content here -->
<div class="right"></div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const color = ref<string>('#FF0000');
</script>
<style scoped lang="less">
.root {
.right {
flex: 1;
background-color: v-bind(color);
}
}
/* Your styles here */
</style>vue3中通过ref获取元素的原始信息比如宽
vue
<template>
<div ref="demo"> </div>
</template>
<script setup>
const demo = ref(null)
demo.value.$el.offsetWidth //元素的宽度
</script>formatComponentInstance 函数通过创建包装组件和使用 Map 来缓存和管理这些组件实例
vue
<template>
<div class="index">
<div class="right">
<div class="routerView">
<router-view v-slot="{ Component, route }">
<keep-alive :include="['/task']">
<component :is="formatComponentInstance(Component, route)" :key="route.fullPath" />
</keep-alive>
</router-view>
</div>
</div>
</div>
<!-- 创建任务弹窗 -->
</template>
<script setup>
// 用来存已经创建的组件
const wrapperMap = new Map();
// 将router传个我们的组件重新换一个新的组件,原组件包里面
function formatComponentInstance(component, route) {
let wrapper;
if (component) {
const wrapperName = route.path;
if (wrapperMap.has(wrapperName)) {
wrapper = wrapperMap.get(wrapperName);
} else {
wrapper = {
name: wrapperName,
render() {
return h(component);
},
};
wrapperMap.set(wrapperName, wrapper);
}
return h(wrapper);
}
}
</script>
<style lang="scss" scoped>
</style>formatComponentInstance 函数的主要作用是通过对 Vue 路由组件进行包装,以实现组件缓存的优化和管理。具体来说,它创建一个新的组件实例来包装原始的路由组件,并使用一个 Map 来存储这些包装过的组件,从而避免每次路由切换时都创建新的组件实例。
详细解释 formatComponentInstance 的作用
- 缓存组件实例: 这个函数使用
wrapperMap(一个Map对象)来存储已经创建的组件包装实例。每个路由路径(route.path)对应一个包装组件。这种方法可以避免重复创建相同路由的组件实例,提高性能。 - 组件包装: 当
formatComponentInstance被调用时,它会检查当前路由路径是否已经有一个包装组件。如果有,它会直接从wrapperMap中获取这个包装组件。如果没有,它会创建一个新的包装组件并存储在wrapperMap中。 - 使用
h函数动态渲染组件:h(component)是 Vue 3 中的一个函数,用来创建 VNode。通过h(component),包装组件可以动态渲染传入的路由组件。这种包装组件的创建方式允许对组件进行定制,比如添加额外的逻辑或状态。 - 返回包装后的组件: 最后,
formatComponentInstance返回一个包装后的组件实例,供router-view使用。这样,当用户切换到不同的路由时,keep-alive组件可以根据wrapperMap进行组件实例的缓存和复用,而不是每次都重新创建。
代码的执行流程
- 路由访问: 当用户访问特定路由时,
router-view触发并提供当前路由对应的Component和route。 - 调用
formatComponentInstance: 使用Component和route调用formatComponentInstance,根据route.path来判断是否已经存在包装组件。 - 返回或创建包装组件: 如果包装组件已经存在,则直接返回;如果不存在,则创建新的包装组件,并将其存储在
wrapperMap中。 - 渲染包装组件: 使用
keep-alive包裹,确保需要缓存的路由组件在切换时不会被销毁,从而保留其状态。
总结
formatComponentInstance 函数通过创建包装组件和使用 Map 来缓存和管理这些组件实例,帮助实现路由组件的缓存和状态保留。这种设计提高了应用的性能,特别是在需要保留组件状态的复杂场景中。
useDefer() 解决vue白屏时间
js
import {ref} from 'vue'
export function useDefer(maxCount = 100){ //最大帧率
const frameCount = ref(0) //目前渲染了多少帧
let rafId
function updateFrameCount(){
rafId = requestAnimationFrame(()=>{
frameCount.value++
if(frameCount.value >= maxCount) return
updateFrameCount()
})
}
updateFrameCount()
onUnMounted(()=>{
cancelAnimationFrame(rafId)
})
return function defer(n){
return frameCount.value >= n
}
}组件中使用
vue
<template>
<div>
<div v-for="(item,index) in 100">
<Hello v-if="defer(n)"></Hello>
</div>
</div>
</template>
<script setup>
import { useDefer } from '../../hooks/useDefer'
import { Hello } from '../../components/Hello'
const defer = useDefer()
</script>
<style lang="scss" scoped></style>Hooks
usePagination 分页
js
import { reactive } from 'vue';
import _ from 'lodash';
const defaultPaginationData = {
pageNo: 1,
pageSize: 10,
total: 0,
pageSizes: [5, 10, 15],
layout: 'total, sizes, prev, pager, next, jumper',
};
/**
* @description 分页hooks
* @param {defaultPaginationData} [_paginationData] 分页配置项
* @param {function } [fn] pageNo/pageSize改变的时候触发函数
*/
export function usePagination(_paginationData = {}, fn) {
const paginationData = reactive({ ...defaultPaginationData, ..._paginationData });
const handleCurrentChange = (value) => {
paginationData.pageNo = value;
fn && fn();
};
const handleSizeChange = _.debounce((value) => {
paginationData.pageNo = 1;
paginationData.pageSize = value;
fn && fn();
}, 500);
return {
paginationData,
handleCurrentChange,
handleSizeChange,
};
}useMessageBox 用于提示用户操作
需要配合element-plus
js
import { ElMessageBox, ElMessage } from 'element-plus';
export function useMessageBox() {
/**
* @description 确认弹窗
* @param {object} options
* @param {string} [options.content] 提示内容
* @param {string} [options.cancelAlert] 取消后的提示内容
* @param {string} [options.confirmAlert] 确认后的提示的内容
* @param {function} [options.success] 操作成功的回调函数
* @param {function} [options.cancle] 操作取消的回调函数
*/
function dialogShow(options) {
ElMessageBox.confirm(options.content || '确认当前操作', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
'lock-scroll': false, //保持body标签的宽度不变
})
.then(() => {
options.success && options.success();
ElMessage({
type: 'success',
message: options.confirmAlert || '操作成功',
});
})
.catch(() => {
options.cancle && options.cancle();
ElMessage({
type: 'info',
message: options.cancelAlert || '操作取消',
});
});
}
return {
dialogShow,
};
}useLoading 全局加载动画
需要配element-plus,建议在app.vue中引入,用provide提供全局确保loading是同一个作用域下的
js
import { ElLoading } from 'element-plus';
export function useLoading() {
const loading = ref(null);
/**
* @description 加载loading
* @param {object} options loading配置项
* @param {string} options.target 需要加载的目标
* @param {boolean} options.body 需要加载的目标
* @param {boolean} options.fullscreen 全屏
* @param {string} options.text 加载文字
* @param {string} options.background 背景颜色
* @param {string} options.customClass 自定义类名
* @param {string} options.spinner 自定义图标
* @param {function} callBack 回调
* @param {ElLoading} callBack.value 拿到ElLoading实列
*/
function loadingShow(options, callBack) {
loading.value = ElLoading.service({
lock: true,
text: 'Loading',
background: 'rgba(0, 0, 0, 0.7)',
...options,
});
callBack && callBack(loading.value);
}
return { loadingShow, loading };
}创建计算属性
js
import { computed } from 'vue'
export function useComputed(fn) {
const map = new Map();
return function (...args) {
const key = JSON.stringify(args);
if(map.has(key)) return map.get(key)
const result = computed(() => fn(...args))
map.set(key, result)
return result
}
}Vue自定义指令
注册指令
js
//main.js
import './assets/main.css';
import { createApp } from 'vue';
import App from './App.vue';
import 'normalize.css';
import 'element-plus/dist/index.css';
import router from './router';
import pinia from './stores';
import lazyLoad from './directives/lazyLoad';
import loadMore from './directives/loadMore';
const app = createApp(App);
app.directive('lazyload', lazyLoad);
app.directive('loadMore', loadMore);
app.use(router).use(pinia).mount('#app');v-lazy
js
// lazyLoad.js
// 图片懒加载
export default {
mounted(el, binding) {
const observer = new IntersectionObserver(([{ isIntersecting }]) => {
if (isIntersecting) {
// 懒加载
el.src = binding.value;
// 取消监听
observer.unobserve(el);
}
});
observer.observe(el);
},
};
//main.jsv-loadMore
js
let observer;
export default {
mounted(el, binding) {
observer = new IntersectionObserver(
([{ isIntersecting }]) => {
if (isIntersecting) {
// 懒加载
binding.value();
}
},
{
threshold: 0,
},
);
observer.observe(el);
},
beforeUnmount(el, binding, vnode) {
observer.unobserve(el);
},
};使用
vue
<template>
<div class="drawer">
<el-drawer v-model="drawer" direction="rtl" :lock-scroll="false">
<el-scrollbar height="100%">
<ul class="preview">
<li v-for="item in images" :key="item.fileName">
<div class="img">
<img v-lazyload="item.filePath" src="@renderer/assets/images/empty.png" alt="" />
</div>
<div class="title"></div>
</li>
</ul>
<!-- 当该元素出现的时候执行loading函数 -->
<div class="loading" v-if="loading" v-loadMore="load"><text style="color: black">加载中...</text></div>
</el-scrollbar>
</el-drawer>
</div>
</template>
<script>
const loading = ref(true);
const load = () => {
console.log('load');
if (pageSize * (pageNo - 1) < detailData.value.assetsList.length) {
loading.value = true;
images.value.push(...detailData.value.assetsList.slice((pageNo - 1) * pageSize, pageNo *pageSize));
console.log(images.value);
pageNo++;
} else {
loading.value = false;
}
};
</script>录音
HTML
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>录音功能</title>
</head>
<body>
<h1>录音功能示例</h1>
<button id="startRecord">开始录音</button>
<button id="stopRecord" disabled>停止录音</button>
<audio id="audioPlayback" controls></audio>
<script src="record.js"></script>
</body>
</html>
<script>
let mediaRecorder;
let audioChunks = [];
document.getElementById('startRecord').addEventListener('click', async () => {
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream);
mediaRecorder.ondataavailable = (event) => {
audioChunks.push(event.data);
};
mediaRecorder.onstop = () => {
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
const audioUrl = URL.createObjectURL(audioBlob);
const audio = document.getElementById('audioPlayback');
audio.src = audioUrl;
audioChunks = [];
};
mediaRecorder.start();
document.getElementById('startRecord').disabled = true;
document.getElementById('stopRecord').disabled = false;
} catch (err) {
console.error('录音失败:', err);
alert('无法访问麦克风,请检查权限设置。');
}
} else {
alert('您的浏览器不支持录音功能');
}
});
document.getElementById('stopRecord').addEventListener('click', () => {
mediaRecorder.stop();
document.getElementById('startRecord').disabled = false;
document.getElementById('stopRecord').disabled = true;
});
</script>echarts立体柱状图
方法一:
js
const data = [220, 182, 191, 234, 290, 330, 310, 100];
let option = {
tooltip: {
trigger: 'axis',
formatter: '{b} : {c}',
axisPointer: {
// 坐标轴指示器,坐标轴触发有效
type: 'shadow', // 默认为直线,可选为:'line' | 'shadow'
},
},
grid: {
top: 30,
bottom: 40,
},
xAxis: {
data: ['高等植物', '哺乳动物', '鸟类', '两栖动物', '陆生昆虫', '水生生物', '爬行动物', '大型真菌'],
//坐标轴
axisLine: {
lineStyle: {
color: '#3eb2e8',
},
},
//坐标值标注
axisLabel: {
show: true,
color: '#F1F7F4',
rotate: -30,
},
},
yAxis: {
name: '单位:种',
nameTextStyle: {
color: '#F1F7F4',
fontSize: 12,
padding: [0, 0, 0, -10], //name文字位置 对应 上右下左
},
//坐标轴
axisLine: {
show: false,
},
//坐标值标注
axisLabel: {
show: true,
color: '#F1F7F4',
},
//分格线
splitLine: {
lineStyle: {
color: '#4784e8',
},
},
},
series: [
{
// /数据图
data: data,
type: 'bar',
barWidth: 10,
label: {
show: true,
color: '#fff',
},
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [
{ offset: 0, color: '#36d2ff' },
{ offset: 1, color: '#d0dd5f' },
]),
},
z: 1,
},
{
// /数据图
data: data,
type: 'bar',
barWidth: 10,
barGap: 0,//两条柱状图之间的距离
label: {
show: true,
color: '#fff',
},
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [
{ offset: 0, color: '#36d2ff' },
{ offset: 1, color: '#d0dd5f' },
]),
},
z: 1,
},
{
// 最上面菱形
data: data,
type: 'pictorialBar',
symbol: 'diamond',
symbolSize: ['20', '10'],
symbolPosition: 'end',
symbolOffset: ['', '-50%'],
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: '#d0dd5f' },
{ offset: 1, color: '#939c3d' },
]),
},
z: 2,
},
{
// 最下面的菱形
data: data,
type: 'pictorialBar',
symbol: 'diamond',
symbolSize: ['20', '10'],
symbolPosition: 'start',
symbolOffset: ['', '50%'],
itemStyle: {
color: '#36d2ff',
},
z: 2,
},
],
}一条数据绘制两条柱状和上下面的菱形,注意第二条柱状图的barGap: 0,不然就不会挨在一起。这种方式实际效果两条柱状图之间还是有点缝隙。效果不佳
方法二:
js
const data = [220, 182, 191, 234, 290, 330, 310, 100];
let {
tooltip: {
trigger: 'axis',
formatter: '{b} : {c}',
axisPointer: {
// 坐标轴指示器,坐标轴触发有效
type: 'shadow', // 默认为直线,可选为:'line' | 'shadow'
},
},
grid: {
top: 30,
bottom: 40,
},
xAxis: {
data: ['高等植物', '哺乳动物', '鸟类', '两栖动物', '陆生昆虫', '水生生物', '爬行动物', '大型真菌'],
//坐标轴
axisLine: {
lineStyle: {
color: '#3eb2e8',
},
},
//坐标值标注
axisLabel: {
show: true,
color: '#F1F7F4',
rotate: -30,
},
},
yAxis: {
name: '单位:种',
nameTextStyle: {
color: '#F1F7F4',
fontSize: 12,
padding: [0, 0, 0, -10], //name文字位置 对应 上右下左
},
//坐标轴
axisLine: {
show: false,
},
//坐标值标注
axisLabel: {
show: true,
color: '#F1F7F4',
},
//分格线
splitLine: {
lineStyle: {
color: '#4784e8',
},
},
},
series: [
{
// /数据图
data: data,
type: 'bar',
barWidth: 20,
label: {
show: true,
color: '#fff',
},
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [
{ offset: 0, color: '#36d2ff' },
{ offset: 1, color: '#d0dd5f' },
]),
},
z: 1,
},
{
// 最上面菱形
data: data,
type: 'pictorialBar',
symbol: 'diamond',
symbolSize: ['20', '10'],
symbolPosition: 'end',
symbolOffset: ['', '-50%'],
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: '#d0dd5f' },
{ offset: 1, color: '#939c3d' },
]),
},
z: 2,
},
{
// 最下面的菱形
data: data,
type: 'pictorialBar',
symbol: 'diamond',
symbolSize: ['20', '10'],
symbolPosition: 'start',
symbolOffset: ['', '50%'],
itemStyle: {
color: '#36d2ff',
},
z: 2,
},
],
}这种方式效果较好 但是立体的侧边颜色无法单独调