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>

被折叠的 条评论
为什么被折叠?



