跳转到内容

指令

图片懒加载

html
<img v-lazy="imageSrc" :alt="imageAlt" class="lazy-image" />
js
/**
 * 图片懒加载指令 (v-lazy)
 *
 * 该指令用于实现图片的懒加载功能,提高页面性能和用户体验。
 * 原理:使用 IntersectionObserver API 监测图片元素是否进入视口,
 * 只有当图片进入可视区域时才会加载真实图片,从而减少不必要的网络请求。
 *
 * 特点:
 * 1. 自动监测元素是否进入视口
 * 2. 提供加载占位图和加载失败的错误图
 * 3. 支持加载动画效果
 * 4. 使用现代的 IntersectionObserver API,性能更好
 *
 * 使用方法:v-lazy="图片路径"
 * 示例:<img v-lazy="'/path/to/image.jpg'" alt="懒加载图片" />
 */
import placeholder from '@/assets/vue.svg';
export default {
	/**
	 * 指令挂载时的处理函数
	 *
	 * @param {HTMLElement} el - 指令绑定的元素,通常是<img>标签
	 * @param {Object} binding - 包含指令相关信息的对象
	 * @param {string} binding.value - 指令的值,这里是图片的真实路径
	 */
	mounted(el, binding) {
		/**
		 * 创建 IntersectionObserver 实例
		 * 用于监测元素是否进入视口
		 *
		 * @param {Function} callback - 当被观察元素的可见性变化时调用的函数
		 * @param {Object} options - 观察器的配置选项
		 */
		const observer = new IntersectionObserver(
			(entries) => {
				entries.forEach((entry) => {
					// 当元素进入视口时
					if (entry.isIntersecting) {
						// 获取目标元素和图片真实路径
						const img = entry.target;
						const src = binding.value;

						// 创建一个新的图片对象来预加载
						// 这样可以在图片完全加载好之后再显示,避免显示过程中的闪烁
						const loadImg = new Image();
						loadImg.src = src;

						/**
						 * 图片加载成功的处理
						 * 1. 将预加载的图片路径赋值给实际显示的图片元素
						 * 2. 添加已加载的CSS类,可用于添加过渡动画
						 */
						loadImg.onload = () => {
							// 图片加载完成后设置src
							img.src = src;
							img.classList.add('lazy-loaded');
						};

						/**
						 * 图片加载失败的处理
						 * 显示一个默认的错误占位图
						 */
						loadImg.onerror = () => {
							// 图片加载失败时可以设置一个默认图片
							img.src = '/src/assets/images/error.png';
						};

						// 图片已经开始加载,停止观察该元素
						// 这样可以节省性能,避免重复触发加载
						observer.unobserve(img);
					}
				});
			},
			{
				// 配置项
				rootMargin: '0px', // 根元素的外边距,可用于提前或延迟触发
				threshold: 0.1, // 当元素10%进入视口时触发,可根据需要调整
			}
		);

		/**
		 * 初始化处理
		 * 1. 设置占位图
		 * 2. 开始观察元素
		 *
		 * 注意:这里只处理IMG标签,如果要支持其他元素(如背景图),
		 * 需要扩展这部分逻辑
		 */
		if (el.tagName === 'IMG') {
			// 设置初始占位图,在真实图片加载前显示
			el.src = placeholder;
			// 开始观察元素
			observer.observe(el);
		}
	},
};
js
import lazyLoad from './lazyLoad.js';
import draggable from './draggable.js';

export default {
	install(app) {
		// 注册图片懒加载指令
		app.directive('lazy', lazyLoad);
		app.directive('draggable', draggable);
	},
};
javascript
import directives from '@/directives/index.js';
// 注册自定义指令
app.use(directives);

移动端拖拽元素

vue
<template>
	<div>
		<div v-draggable class="box"></div>
	</div>
</template>

<script setup></script>

<route lang="json5">
	{
		meta:{
			title: '拖拽指令',
			order: 4
		},
	}
</route>

<style scoped>
.box {
	width: 100px;
	height: 100px;
	background-color: red;
}
</style>
js
// draggable-float.js
export default {
	mounted(el) {
		// 初始化默认位置
		const resetPosition = () => {
			const screenWidth = window.innerWidth;
			const screenHeight = window.innerHeight;
			const elementWidth = el.offsetWidth;
			const elementHeight = el.offsetHeight;

			el.style.left = screenWidth - elementWidth - 10 + 'px'; // 右侧贴边(留10px间距)
			el.style.top = (screenHeight - elementHeight) / 2 + 'px'; // 居中对齐
		};

		// 暴露给外部使用
		el.resetPosition = resetPosition;

		// 初始位置
		resetPosition();

		el.style.position = 'absolute';
		el.style.cursor = 'grab';
		el.style.transition = 'left 0.3s ease-out, top 0.3s ease-out';

		// 事件处理函数封装
		const touchStartHandler = (event) => {
			event.preventDefault();
			el.style.transition = 'none';
			const touch = event.touches[0];

			const startX = touch.clientX - el.offsetLeft;
			const startY = touch.clientY - el.offsetTop;

			const moveHandler = (moveEvent) => {
				moveEvent.preventDefault();
				const moveTouch = moveEvent.touches[0];

				let newX = moveTouch.clientX - startX;
				let newY = moveTouch.clientY - startY;

				const maxY = window.innerHeight - el.offsetHeight;
				newY = Math.max(0, Math.min(newY, maxY));

				el.style.left = newX + 'px';
				el.style.top = newY + 'px';
			};

			const endHandler = () => {
				document.removeEventListener('touchmove', moveHandler);
				document.removeEventListener('touchend', endHandler);

				const screenWidth = window.innerWidth;
				const elementWidth = el.offsetWidth;

				el.style.transition = 'left 0.3s ease-out, top 0.3s ease-out';
				el.style.left = screenWidth - elementWidth - 10 + 'px'; // 贴右边
			};

			document.addEventListener('touchmove', moveHandler, { passive: false });
			document.addEventListener('touchend', endHandler);
		};

		el._touchStartHandler = touchStartHandler; // 缓存引用,用于解绑
		el.addEventListener('touchstart', touchStartHandler, { passive: false });
	},

	unmounted(el) {
		if (el._touchStartHandler) {
			el.removeEventListener('touchstart', el._touchStartHandler);
			delete el._touchStartHandler;
		}
	},
};

Will Try My Best.