使用的库: vue3-selecto
文档地址: selecto文档
效果
下面是代码
// common.js
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn'; // 引入中文语言包,用于显示中文星期
// 生成当月详细的天数
export const initDays = () => {
dayjs.locale('zh-cn'); // 设置语言为中文
const currentMonth = dayjs().month() + 1;
// 获取当月的第一天
const startOfMonth = dayjs().startOf('month');
// 获取当月的最后一天
const endOfMonth = dayjs().endOf('month');
// 用于存储当月所有日期及星期的数组
const datesWithWeek = [];
// 循环生成当月所有日期及星期
let currentDate = startOfMonth;
while (currentDate <= endOfMonth) {
const fullDate = currentDate.format('YYYY-MM-DD');
const dateStr = currentDate.format('DD');
const weekStr = currentDate.format('dddd'); // 获取星期几,中文显示
const weekIndex = currentDate.day();
datesWithWeek.push({
date: parseFloat(dateStr),
fullDate,
week: weekStr,
weekIndex,
});
currentDate = currentDate.add(1, 'day');
}
return { currentMonth, endOfMonth, datesWithWeek };
};
export const hasOwnProperty = (obj, key) => {
return Object.prototype.hasOwnProperty.call(obj, key);
};
.vue文件
<template>
<div class="personnel-scheduling">
<el-table
ref="tableRef"
:data="tableData"
@cell-dblclick="cellDblClick"
:cell-class-name="cellClassName"
>
<el-table-column
label="a"
width="90"
prop="windowName"
></el-table-column>
<template v-for="(item, index) in datesWithWeek" :key="index">
<el-table-column
:label-class-name="
[0, 6].includes(item.weekIndex) ? 'is-week' : ''
"
>
<template #header>
<span v-if="item.date === 1">
{{ `${currentMonth}月` }}
</span>
<span> {{ item.date }}</span>
</template>
<el-table-column
prop="date"
:label="item.week.replace('星期', '')"
:label-class-name="
[0, 6].includes(item.weekIndex) ? 'is-week' : ''
"
>
<template #default="{ row }">
<div
:data-date="item.fullDate"
:data-window-id="row.windowId"
>
{{
row.date[index] &&
row.date[index][item.fullDate]
}}
</div>
</template>
</el-table-column>
</el-table-column>
</template>
<el-table-column label="操作">
<el-button type="primary" link size="small">按钮</el-button>
</el-table-column>
</el-table>
<Selecto
ref="selectoRef"
dragContainer=".el-table__body"
:selectableTargets="['.is-selecto']"
:hitRate="0"
:selectByClick="true"
:selectFromInside="true"
:continueSelect="false"
:continueSelectWithoutDeselect="true"
toggleContinueSelect="shift"
:toggleContinueSelectWithoutDeselect="[['ctrl'], ['meta']]"
:ratio="0"
:scrollOptions="{
container: scrollerRef,
getScrollPosition: () => {
return [scrollerRef.scrollLeft, scrollerRef.scrollTop];
},
throttleTime: 30,
threshold: 0,
}"
@dragStart="onDragStart"
@selectEnd="onSelectEnd"
@select="onSelect"
@scroll="onScroll"
@innerScroll="onInnerScroll"
></Selecto>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, toRaw } from 'vue';
import { ElMessage } from 'element-plus';
import Selecto from 'vue3-selecto';
import { initDays, hasOwnProperty } from '../common';
const emits = defineEmits(['dblclick']);
onMounted(() => {
scrollerRef.value = tableRef.value.$el.querySelector(
'.el-scrollbar__wrap '
);
getTableData();
document.addEventListener('keydown', keydownHandler);
document.addEventListener('paste', pasteHandler);
});
onUnmounted(() => {
document.removeEventListener('keydown', keydownHandler);
document.removeEventListener('paste', pasteHandler);
});
const { currentMonth, endOfMonth, datesWithWeek } = initDays();
const tableRef = ref(null);
const tableData = ref([]);
const cellDblClick = (row, column, cell, event) => {
if (column.isSubColumn) {
console.log('双击了子列');
}
};
const cellClassName = (row, column, rowIndex, columnIndex) => {
if (![0, 32].includes(row.columnIndex)) {
return 'is-selecto';
}
};
let selectData = [];
const selectoRef = ref(null);
const scrollerRef = ref(null);
const onDragStart = (e) => {
return !(e.inputEvent.target.nodeName === 'BUTTON');
};
const onSelect = (e) => {
e.added.forEach((el) => {
el.classList.add('selected');
});
e.removed.forEach((el) => {
el.classList.remove('selected');
});
};
const onScroll = ({ direction }) => {
scrollerRef.value.scrollBy(direction[0] * 10, direction[1] * 10);
};
const onInnerScroll = ({ container, direction }) => {
container.scrollBy(direction[0] * 10, direction[1] * 10);
};
const onSelectEnd = (e) => {
selectData = [];
const els = selectoRef.value.getSelectedTargets();
els.forEach((el) => {
const { date, windowId } = el.children[0].children[0].dataset;
selectData.push({ date, windowId });
});
};
const getTableData = () => {
let data = [
{
date: [{ '2025-01-01': '张三,王五' }, { '2025-01-02': '李四' }],
windowName: 'a1',
windowId: '1',
},
{
date: [],
windowName: 'a2',
windowId: '2',
},
{
date: [],
windowName: 'a3',
windowId: '3',
},
{
date: [],
windowName: 'a4',
windowId: '4',
},
{
date: [],
windowName: 'a5',
windowId: '5',
},
{
date: [],
windowName: 'a1',
windowId: '6',
},
{
date: [],
windowName: 'a2',
windowId: '7',
},
{
date: [],
windowName: 'a3',
windowId: '8',
},
{
date: [],
windowName: 'a4',
windowId: '9',
},
{
date: [],
windowName: 'a5',
windowId: '10',
},
];
data.forEach((item) => {
if (item.date.length !== endOfMonth) {
const days = datesWithWeek.map((item) => item.fullDate);
if (!Object.keys(item.date).length) {
days.forEach((citem) => {
item.date.push({ [citem]: '' });
});
} else {
const dates = [];
Object.values(item.date).forEach((dItem) => {
dates.push(...Object.keys(dItem));
});
days.forEach((dItem) => {
if (!dates.includes(dItem)) {
item.date.push({ [dItem]: '' });
}
});
}
}
});
tableData.value = data;
};
const keydownHandler = (event) => {
const { ctrlKey, keyCode } = event;
if (ctrlKey && keyCode === 67) {
const els = selectoRef.value.getSelectedTargets();
let strArr = [];
els.forEach((el) => {
if (el.children[0].children[0].innerText) {
strArr.push(el.children[0].children[0].innerText);
}
});
if (navigator.clipboard) {
navigator.clipboard.writeText(strArr.join(',')).then(() => {
console.log('复制成功');
strArr = [];
ElMessage.success('复制成功');
});
}
}
if (keyCode === 8) {
selectData.forEach((item) => {
tableData.value.forEach((cItem) => {
if (cItem.windowId === item.windowId) {
cItem.date.forEach((dItem) => {
console.log(dItem, item.date);
if (hasOwnProperty(dItem, item.date)) {
dItem[item.date] = '';
}
});
}
});
});
}
};
const pasteHandler = (event) => {
// 阻止默认行为,如果需要的话
event.preventDefault();
// 获取粘贴板的数据
var clipboardData = event.clipboardData || window.clipboardData;
var pastedData = clipboardData.getData('Text');
// if (!pastedData || pastedData === 'pastedData') {
// return false;
// }
selectData.forEach((item) => {
tableData.value.forEach((cItem) => {
if (cItem.windowId === item.windowId) {
if (!cItem.date.length) {
cItem.date[0] = { [item.date]: pastedData };
} else {
cItem.date.forEach((dItem) => {
if (!dItem[item.date]) {
dItem[item.date] = pastedData;
} else {
if (hasOwnProperty(dItem, item.date)) {
dItem[item.date] = pastedData;
}
}
});
}
}
});
});
};
</script>
<style lang="less" scoped>
.personnel-scheduling {
:deep(.el-table) {
.el-table__inner-wrapper {
.el-table__header-wrapper {
.el-table__header {
.el-table__cell {
&.is-week {
color: #f56c6c;
}
}
}
}
.el-table__body-wrapper {
.el-table__body {
.el-table__cell {
&.selected {
color: #fff;
background: #409eff;
}
}
}
}
}
}
}
</style>