基于Vue3制作一个可以拖拽排列的卡片,支持nuxt3

部署运行你感兴趣的模型镜像
1. 前情交代和说明

首先感谢dwanda大佬的github项目cardDragger,github地址:
https://github.com/dwanda/dragComponent/tree/master

在使用以上github仓库代码时,发现在使用vue3 setup 语法糖,没找到支持的库,于是乎把代码clone下来重新做了改动。使用方法,参考如上github文档,核心功能没做改动。

效果视频:

1747307682547

改动的地方:
(1) 修改了支持vue3 setup 语法糖。
(2) 修改了原来计算卡片的位置 left, top 坐标的函数,如下:

function computeLeft(num) {
	// return (num-1) % props.colNum * props.cardOutsideWidth; // 老代码
	const interval = props.cardOutsideWidth - props.cardInsideWidth;
	const col = (num - 1) % props.colNum // num 计算是在第几列 是从1开始的
	const ret = interval + col * (props.cardOutsideWidth + interval)
	// console.log('computeLeft num=', num, ', left=', ret, ', intv=', interval, ', w1=', props.cardOutsideWidth, ', w2=', props.cardInsideWidth);
	return ret;
}

function computeTop(num) {
	// return (Math.ceil(num / props.colNum) - 1) * props.cardOutsideHeight; // 老代码
	const interval = props.cardOutsideHeight - props.cardInsideHeight;
	const row = Math.floor((num-1) / props.colNum); // num从1开始,计算在第几行
	const ret = interval + row * (props.cardOutsideHeight + interval);
	// console.log('computeTop num=', num, ', top=', ret, ', intv=', interval, ', h1=', props.cardOutsideHeight, ', h2=', props.cardInsideHeight);
	return ret;
}

(3) 封装成一个子组件,直接在项目组引用,并且在父组件窗体大小变化时,动态计算每行卡片的数量(colNum)并动态刷新排版。

2.  以下附上完整的代码:
  (1)父组件(NewsList.vue),调用cardDradder.vue的地方

<style>
.topMenuBox {
	display: flex;
	height: 40px;
	align-items: center;

	.menuTitle {
		padding: 10px;
	}
	.refreshBtn {
		margin-left: auto;
		right: 10px;
	}
}


</style>

<template>
	<div ref="myElementRef">
		<card-dragger
			ref="myCardDraggerRef"
			:data="pageData.cardList"
			:card-outside-width="pageData.cardOutsideWidth"
			:card-outside-height="pageData.cardOutsideHeight"
			:card-inside-width="pageData.cardInsideWidth"
			:card-inside-height="pageData.cardInsideHeight"
			:col-num="pageData.cardColNum"
		>
			<template v-slot:header="slotProps">
				<!--自定义内容-->
				<div class="topMenuBox" >
					<img class="iconHeader" :src="slotProps.item.headIcon" v-if="slotProps.item.headIcon !== ''" alt="headIcon" height="32" width="32" />
					<div class="menuTitle" v-if="slotProps.item.name">{{slotProps.item.name}}</div>
					<el-button class="refreshBtn" size="small" :icon="Refresh" round/>
				</div>
			</template>
		</card-dragger>>
	</div>
</template>

<script setup >

import cardDragger from "~/components/comment/cardDragger.vue";
import { Refresh } from '@element-plus/icons-vue';

const pageData = ref({
	cardOutsideWidth: 300,
	cardOutsideHeight: 200,
	cardInsideWidth: 280,
	cardInsideHeight: 180,
	cardColNum: 2,
	cardList: [
		{
			positionNum: 1,
			name: "演示卡片1",
			id: "card1",
			headIcon: 'https://www.w3school.com.cn/i/photo/tree.png',
		},
		{
			positionNum: 2,
			name: "演示卡片2",
			id: "card2",
			headIcon: '',
		},
		{
			positionNum: 3,
			name: "演示卡片3",
			id: "card3",
			headIcon: '',
		},
		{
			positionNum: 4,
			name: "演示卡片4",
			id: "card4",
			headIcon: 'https://www.w3school.com.cn/i/photo/tree.png',
		},
		{
			positionNum: 5,
			name: "演示卡片5",
			id: "card5",
			headIcon: '',
		},
		{
			positionNum: 6,
			name: "演示卡片6",
			id: "card6",
			headIcon: 'https://www.w3school.com.cn/i/photo/tree.png',
		},
		{
			positionNum: 7,
			name: "演示卡片7",
			id: "card7",
			headIcon: '',
		},
	]
})

const myElementRef = ref(null);

onMounted(() => {
	calcWindowInfo();
	window.addEventListener('resize', onWindowResize);
})

onUnmounted(() => {
	window.removeEventListener('resize', onWindowResize);
})

const myCardDraggerRef = ref(null);
function calcWindowInfo() {
	const width = myElementRef.value.offsetWidth;
	// console.log('onMounted width = ', pageData.value, ' colNum = ', pageData.value.cardColNum);
	// 计算每排可以容纳多少个卡片
	const oldColNum = pageData.value.cardColNum;
	const colNum = width / pageData.value.cardOutsideWidth;
	if (oldColNum !== colNum) {
		pageData.value.cardColNum = Math.floor(colNum);
	}

	console.log(`onMounted width = ${width}, colNum = ${pageData.value.cardColNum}`);
}

function onWindowResize() {
	console.log('onWindowResize')
	calcWindowInfo();
}

</script>

(2)子组件(cardDragger.vue)

<template>
	<div
		:style="{
        position:'relative',
        height:computeTop(data.length)+cardOutsideHeight+'px',
        width:cardOutsideWidth*colNum+'px'}"
	>
		<div
			class="d_cardBorderBox"
			v-for="item of data"
			:key="item.id"
			:id="item.id"
			:style="{ width:cardOutsideWidth+'px', height:cardOutsideHeight+'px'}"
		>
			<div
				class="d_cardInsideBox"
				:style="{ width:cardInsideWidth+'px', height:cardInsideHeight+'px'}"
			>
				<div @mousedown="touchStart($event,item.id)" class="d_topWrapBox">
					<slot name="header" v-bind:item="item">
						<div class="d_topMenuBox" >
							<div class="d_menuTitle" v-if="item.name">{{item.name}}</div>
							<div class="d_menuTitle" v-else> 默认标题 </div>
						</div>
					</slot>
				</div>
				<component :is="item.componentData" :itemData="item" v-if="item.componentData"></component>
				<slot name="content" v-bind:item="item" v-else>
					<div class="d_emptyContent">
						暂无内容
					</div>
				</slot>
			</div>
		</div>
	</div>
</template>

<script setup>

import { ref, nextTick } from 'vue'

const props = defineProps({
	data: {
		type:Array,
		default: function () {
			return []
		}
	},
	colNum: {
		type:Number,
		default:2
	},
	cardOutsideWidth: {
		type:Number,
		default:590
	},
	cardOutsideHeight: {
		type:Number,
		default:380
	},
	cardInsideWidth: {
		type:Number,
		default:560
	},
	cardInsideHeight: {
		type:Number,
		default:350
	}
});
const mousedownTimer = ref(null);

watch(
	() => [props.colNum, props.data],
	() => {
		console.log('watch colNum = ', props.colNum, ', data = ', props.data);
		addCardStyle(); // 执行刷新操作
	},
	{ immediate: true }
);

function computeLeft(num) {
	// return (num-1) % props.colNum * props.cardOutsideWidth; // 老代码
	const interval = props.cardOutsideWidth - props.cardInsideWidth;
	const col = (num - 1) % props.colNum // num 计算是在第几列 是从1开始的
	const ret = interval + col * (props.cardOutsideWidth + interval)
	// console.log('computeLeft num=', num, ', left=', ret, ', intv=', interval, ', w1=', props.cardOutsideWidth, ', w2=', props.cardInsideWidth);
	return ret;
}

function computeTop(num) {
	// return (Math.ceil(num / props.colNum) - 1) * props.cardOutsideHeight; // 老代码
	const interval = props.cardOutsideHeight - props.cardInsideHeight;
	const row = Math.floor((num-1) / props.colNum); // num从1开始,计算在第几行
	const ret = interval + row * (props.cardOutsideHeight + interval);
	// console.log('computeTop num=', num, ', top=', ret, ', intv=', interval, ', h1=', props.cardOutsideHeight, ', h2=', props.cardInsideHeight);
	return ret;
}

function addCardStyle(){
	nextTick(()=>{
		props.data.forEach(item=>{
			document.querySelector('#'+item.id).style.top = computeTop(item.positionNum)+'px'
			document.querySelector('#'+item.id).style.left = computeLeft(item.positionNum)+'px'
		})
	})
}

const emits = defineEmits(['startDrag', 'swicthPosition', 'swicthPosition']);

function touchStart(event, selectId) {
	if (mousedownTimer.value) {
		return false;
	}
	//若触发了点击事件,则返回一个暴露出一个方法
	//this.$emit('startDrag',event,selectId)
	emits('startDrag', event, selectId);

	let DectetTimer = null;

	let originTop = document.body.scrollTop === 0 ? document.documentElement.scrollTop : document.body.scrollTop;
	let scrolTop = originTop;
	//记录鼠标移动的距离
	let moveTop = 0;
	let moveLeft = 0;
	//起始组件位置
	let OriginObjPosition = {
		left: 0,
		top: 0,
		originNum: -1
	};
	// 起始鼠标信息
	let OriginMousePosition = {
		x: 0,
		y: 0
	};
	// 记录交换位置的号码
	let OldPositon = null;
	let NewPositon = null;
	// 选中的卡片的dom和数据
	let selectDom = document.getElementById(selectId);
	let selectMenuData = props.data.find(item => {
		return item.id === selectId;
	});

	OriginMousePosition.x = event.screenX;
	OriginMousePosition.y = event.screenY;

	selectDom.classList.add('d_moveBox')

	moveLeft = OriginObjPosition.left = parseInt(
		selectDom.style.left.slice(0, selectDom.style.left.length - 2)
	);
	moveTop = OriginObjPosition.top = parseInt(
		selectDom.style.top.slice(0, selectDom.style.top.length - 2)
	);

	document.addEventListener("mousemove", mouseMoveListener);
	document.addEventListener("mouseup", mouseUpListener);
	document.addEventListener("scroll", mouseScroll);

	function mouseMoveListener(event) {
		// console.log('mouseMoveListener event => ', event);
		moveTop = OriginObjPosition.top + ( event.screenY - OriginMousePosition.y );
		moveLeft = OriginObjPosition.left + ( event.screenX - OriginMousePosition.x );

		document.querySelector(".d_moveBox").style.left = moveLeft + "px";
		document.querySelector(".d_moveBox").style.top = moveTop + (scrolTop - originTop) + "px";  //这里要加上滚动的高度

		if (!DectetTimer) {
			DectetTimer = setTimeout(()=>{
				cardDetect(moveTop + (scrolTop - originTop),moveLeft)
				DectetTimer = null;
			}, 200);
		}
	}

	function mouseScroll(event) {
		// console.log('mouseScroll event => ', event);
		scrolTop =
			document.body.scrollTop === 0
				? document.documentElement.scrollTop
				: document.body.scrollTop;
		document.querySelector(".d_moveBox").style.top = moveTop + scrolTop - originTop + "px";
	}

	function cardDetect(moveItemTop, moveItemLeft){
		//计算当前移动卡片,可以覆盖的号码位置
		let newWidthNum = Math.round((moveItemLeft/ props.cardOutsideWidth))+1
		let newHeightNum = Math.round((moveItemTop/ props.cardOutsideHeight))

		if(newHeightNum>(Math.ceil(props.data.length / props.colNum) - 1)||
			newHeightNum < 0||
			newWidthNum <= 0||
			newWidthNum > props.colNum){
			return false
		}

		const newPositionNum = (newWidthNum) + newHeightNum * props.colNum
		if(newPositionNum!==selectMenuData.positionNum){
			let newItem = props.data.find(item=>{
				return item.positionNum === newPositionNum
			})
			if( newItem ){
				swicthPosition(newItem, selectMenuData);
			}
		}
	}

	function swicthPosition(newItem, originItem) {
		OldPositon = originItem.positionNum;
		NewPositon = newItem.positionNum;

		//that.$emit('swicthPosition',OldPositon,NewPositon,originItem)
		emits('swicthPosition', OldPositon,NewPositon,originItem);

		//位置号码从小移动到大
		if (NewPositon > OldPositon) {
			let changeArray = [];
			//从小移动到大,那小的号码就会空出来,其余卡片应往前移动一位
			//找出两个号码中间对应的卡片数据
			for (let i = OldPositon + 1; i <= NewPositon; i++) {
				let pushData = props.data.find(item => {
					return item.positionNum === i;
				});
				changeArray.push(pushData);
			}

			for (let item of changeArray) {
				//vue的$set使更改数据的同时实时刷新样式
				// that.$set(item, "positionNum", item.positionNum - 1);
				item['positionNum'] = item.positionNum - 1;
				document.querySelector('#'+item.id).style.top = computeTop(item.positionNum)+'px'
				document.querySelector('#'+item.id).style.left = computeLeft(item.positionNum)+'px'
			}
			// that.$set(originItem, "positionNum", NewPositon);
			originItem['positionNum'] = NewPositon;
		}

		//位置号码从大移动到小
		if (NewPositon < OldPositon) {
			let changeArray = [];
			//从大移动到小,那大的号码就会空出来,其余卡片应往后移动一位
			//找出两个号码中间对应的卡片数据
			for (let i = OldPositon - 1; i >= NewPositon; i--) {
				let pushData = props.data.find(item => {
					return item.positionNum === i;
				});
				changeArray.push(pushData);
			}

			for (let item of changeArray) {
				// that.$set(item, "positionNum", item.positionNum + 1);
				item['positionNum'] = item.positionNum + 1;
				document.querySelector('#'+item.id).style.top = computeTop(item.positionNum)+'px'
				document.querySelector('#'+item.id).style.left = computeLeft(item.positionNum)+'px'
			}
			// that.$set(originItem, "positionNum", NewPositon);
			originItem['positionNum'] = NewPositon;
		}
	}

	function mouseUpListener() {
		// console.log('mouseUpListener ...');
		//取消位于交换队列的检测事件、对位置进行最后一次检测
		clearTimeout(DectetTimer)
		DectetTimer = null
		cardDetect(moveTop + (scrolTop - originTop),moveLeft)

		document.querySelector(".d_moveBox").classList.add('d_transition');
		document.querySelector(".d_moveBox").style.top = computeTop(selectMenuData.positionNum) + "px";
		document.querySelector(".d_moveBox").style.left = computeLeft(selectMenuData.positionNum) + "px";
		// that.$emit('finishDrag',OldPositon,NewPositon,selectMenuData)
		emits('finishDrag', OldPositon,NewPositon,selectMenuData);

		mousedownTimer.value = setTimeout(() => {
			/*用0.3秒来过渡
			  mousedownTimer在一开始对点击事件进行了判断,若还在过渡则不能进行下一次点击
			*/
			document.querySelector(".d_moveBox").classList.remove('d_transition')
			document.querySelector(".d_moveBox").classList.remove('d_moveBox')
			clearTimeout(mousedownTimer.value);
			mousedownTimer.value = null;
		}, 300);

		document.removeEventListener("mousemove", mouseMoveListener);
		document.removeEventListener("mouseup", mouseUpListener);
		document.removeEventListener("scroll", mouseScroll);
	}
}

</script>
<style scoped>
.d_cardBorderBox {
	user-select: none;
	position: absolute;
	transition: all 0.3s;
	display: flex;
	justify-content: center;
	align-items: center;
}
.d_cardInsideBox {
	border-radius: 5px;
	box-shadow: 0 0 5px #cacaca;
	display: flex;
	flex-direction: column;
	overflow: hidden;
}
.d_menuTitle {
	pointer-events: none;
}
.d_topMenuBox {
	height: 50px;
	display: flex;
	align-items: center;
	font-size: 14px;
	color: #838383;
	background-color: white;
	padding: 0px 15px;
}
.d_moveBox {
	top:20px;
	left: 20px;
	z-index: 300;
	transition: none;
}
.d_topWrapBox {
	cursor: move;
	border-bottom: 1px solid #e0e0e0;
}
.d_emptyContent{
	width: 100%;
	height: 100%;
	font-size: 16px;
	color: #979797;
	display: flex;
	justify-content: center;
	align-items: center;
}
.d_transition{
	transition: all 0.3s;
}
</style>

您可能感兴趣的与本文相关的镜像

Wan2.2-T2V-A5B

Wan2.2-T2V-A5B

文生视频
Wan2.2

Wan2.2是由通义万相开源高效文本到视频生成模型,是有​50亿参数的轻量级视频生成模型,专为快速内容创作优化。支持480P视频生成,具备优秀的时序连贯性和运动推理能力

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值