当前位置: 首页 > news >正文

基于el-table的表格点选和框选功能

开篇

本篇文章旨在实现一个基于el-table的表格点选和框选功能,除此之外,还支持多种模式的切换、自定义勾选日期等。且,该表格后续可能还会持续优化!

功能介绍

  • 表格点选和框选功能(没有点击ctrl键的情况下)
  • 表格点选和框选功能的互斥功能,意思是当现在选择了该单元格后,按住ctrl键再次点击,就会取消选择
  • 当前日期高亮
  • 周、双周、月份模式的切换以及自定义日期的选择
  • 对于可点选和框选功能的范围判断(表头和名称不可选择)

代码实现

index.vue

<template><div class="container"><!-- 顶部功能栏区域 --><data class="top-bar"><!-- 切换模式按钮 --><el-button size="mini" type="primary" @click="turnWeek"></el-button><el-button size="mini" type="primary" @click="turnTwoWeeks">两周</el-button><el-button size="mini" type="primary" @click="turnMonth"></el-button><!-- 自定义时间范围选择器 --><div class="time-range-picker"><span class="arrow-icon"><el-icon><ArrowLeftBold /></el-icon></span><!-- 起始日期 --><el-date-pickerclass="time-picker"v-model="dateForm.startDate"type="datetime":shortcuts="shortcuts"@change="(value) => handleDateChange('startDate', value)"/><span style="padding: 0 10px">-</span><!--结束日期 --><el-date-pickerclass="time-picker"v-model="dateForm.endDate"type="datetime":shortcuts="shortcuts"@change="(value) => handleDateChange('endDate', value)"/><span class="arrow-icon"><el-icon><ArrowRightBold /></el-icon></span></div></data><!-- 表格区域 --><div class="table-container"><!-- 月份选择 --><month-schedule-table:table-data="tableData":switch-model="switchModel":dateForm="dateForm":customDateFlag="customDateFlag"@updateDateForm="handleUpdateDateForm":table-width="tableWidth"></month-schedule-table></div></div>
</template><script>
import {defineComponent,onMounted,reactive,toRefs,ref,watchEffect,onUpdated,onUnmounted,nextTick,
} from "vue";
import MonthScheduleTable from "./components/MonthScheduleTable.vue";
export default defineComponent({name: "DepartmentScheduleConfig",components: {MonthScheduleTable,},setup() {const state = reactive({tableData: [],switchModel: "month",// 自定义日期选择相关dateForm: {startDate: "",endDate: "",},customDateFlag: false, // 是否在自定义切换日期的标识});const tableWidth = ref(0);onMounted(() => {// 先初始化一些假数据用着initData();nextTick(() => {getTableWidth();});});const getTableWidth = () => {// 求表格宽度let container = document.querySelector(".container");if (container) {const observer = new ResizeObserver(() => {tableWidth.value = container.clientWidth;});observer.observe(container);// 初始获取一次宽度tableWidth.value = container.clientWidth;}};// 每次更新表格时,也要重新获取表格宽度onUpdated(() => {getTableWidth();});window.addEventListener("resize", getTableWidth);// 页面关闭时,移除事件监听器onUnmounted(() => {window.removeEventListener("resize", getTableWidth);});// 切换成周模式const turnWeek = () => {state.switchModel = "week";state.customDateFlag = false;};// 切换成两周模式const turnTwoWeeks = () => {state.switchModel = "twoWeeks";state.customDateFlag = false;};// 切换成月模式const turnMonth = () => {state.switchModel = "month";state.customDateFlag = false;};// 初始化数据const initData = () => {const obj = {};for (let i = 0; i <= 31; i++) {obj[`date${i}`] = null;}const arr = [];for (let j = 1; j < 10; j++) {const tmpObj = { ...obj };tmpObj.id = j;tmpObj.name = `zzz${j}`;arr.push(tmpObj);}console.log("arr", arr);arr[0].date1 = "X";arr[3].date3 = "Y";state.tableData = arr;};/*** 自定义日期相关*/// 日期选择面板扩展const shortcuts = [{text: "今天",value: new Date(),},{text: "昨天",value: () => {const date = new Date();date.setDate(date.getDate() - 1);return date;},},{text: "一周前",value: () => {const date = new Date();date.setDate(date.getDate() - 7);return date;},},];// 日期change方法const handleDateChange = (key, value) => {state.dateForm[key] = new Date(value);console.log("dateForm", state.dateForm);state.customDateFlag = true;};// 更新日期选择器const handleUpdateDateForm = (val) => {console.log("val", val);state.dateForm["startDate"] = val.startDate;state.dateForm["endDate"] = val.endDate;};return {...toRefs(state),turnWeek,turnTwoWeeks,turnMonth,tableWidth,shortcuts,handleDateChange,handleUpdateDateForm,};},
});
</script><style style="scss" scoped>
.container {width: 100%;.top-bar {display: flex;.time-range-picker {margin-left: 5px;.arrow-icon {cursor: pointer;padding: 8px;}}}.table-container {width: 100%;margin-top: 5px;}
}
</style>

MonthScheduleTable.vue

<template><div class="container"><!-- 表格区域 --><data class="wrap"@mousedown="handleMouseDown"@mousemove="handleMouseMove"@mouseup="handleMouseUp"><!-- 框选矩形 --><div v-if="rectVisible" class="select-rect":style="{ left: rect.left + 'px', top: rect.top + 'px', width: rect.width + 'px', height: rect.height + 'px' }"></div><!-- 表格 --><div class="con"><el-table :data="tableData" border style="width: 100%" :cell-style="CellStyle"@cell-click="handleCellClick"><!-- 姓名 --><el-table-column prop="name" label="姓名" :width="getColumnWidth()"> </el-table-column><!-- 日期 这一块日期应该根据月、周、两周三种模式来动态显示,不应该写死--><el-table-columnclass="headerSelect":label-class-name="isWeekend(item.date, item.day)"v-for="(item, index) in currDateArr" :key="index" :label="item.date"><el-table-column :label-class-name="isWeekend(item.date, item.day)":class-name="isWeekend(item.date, item.day)":prop="getProp(index)" :label="item.day" :class="{'headerSelect': true}":width="getColumnWidth()"><template #default="scope"><div :data-row="scope.row.name" :data-column="getProp(index)" class="cell-content">{{ scope.row[getProp(index)] }}</div></template></el-table-column></el-table-column></el-table></div></data></div>
</template><script>
import { computed } from '@vue/reactivity';
import { defineComponent, onMounted, reactive, toRefs, watchEffect, ref, onUpdated } from 'vue'
export default defineComponent({name: 'MonthScheduleTable',emits: ['updateDateForm'],props: {tableData: {type: Array,default: () => []},switchModel: {type: String,default: 'month'},tableWidth: {type: Number,default: 0},dateForm: {type: Object,default: () => ({})},customDateFlag: {type: Boolean,default: false}},setup(props, { emit }) {const state = reactive({tableData: computed(() => props.tableData), // 数组数据源// 鼠标框选功能相关selectedCells: [], // 选择的单元格rectVisible: false, // 是否显示框选矩形rect: {left: 0, top: 0, width: 0, height: 0}, // 矩形的坐标信息downX: 0, // 鼠标按下时的X坐标downY: 0, // 鼠标按下时的Y坐标upX: 0, // 鼠标松开时的X坐标upY: 0, // 鼠标松开时的Y坐标isMouseDown: false, // 鼠标是否移动isCellClick: false,// 是否是单元格被点击了})// 给当前的单元格分配propconst getProp = idx => {return `date${idx}`}// 判断鼠标是否在可选区域内const isInSelectableArea = event => {const target = event.target;// 查找最近的表头元素(th 或 thead)const headerElement = target.closest('th, thead');// 如果目标元素位于表头中,返回 falseif (headerElement) return false;const headerSelect = document.querySelector('.con')if (!headerSelect) return false;const headerRect = headerSelect.getBoundingClientRect()const isInHeader = event.clientX >= headerRect.left && event.clientX <= headerRect.right &&event.clientY >= headerRect.top && event.clientY <= headerRect.bottom;const cell = target.closest('td, th');const columnIndex = cell ? cell.cellIndex : undefined;return isInHeader && columnIndex > 0; // 从第二列开始}// 判断当前是否只点击了一个单元格// 表格单元格点击事件const handleCellClick = (row, column, cell, event) => {if (!isInSelectableArea(event)) return;state.isCellClick = trueif (event.ctrlKey) { // 判断是否按下了Ctrl键// 当鼠标左键+ctrl同时按下时,实现多选功能/*** 若当前的cell.classList包含highlight类,则移除,并把该单元格的数据移除出数组;* 若不包含,证明之前并未选择过,则添加hightlight类,并把数据push进数组*/if (cell.classList.contains('highlight')) {cell.classList.remove('highlight')// 将该单元格的数据移出数组const index = state.selectedCells.findIndex(item => item.row === row.name && item.column === column.property)if (index > -1) {state.selectedCells.splice(index, 1)}} else {cell.classList.add('highlight')// 将数据加入数组state.selectedCells.push({ row: row.name, column: column.property, value: row[column.property] });}} else {// 普通高亮的逻辑// 清除所有已高亮的单元格const highlightedCells = document.querySelectorAll('.highlight')highlightedCells.forEach(cell => cell.classList.remove('highlight'))// 清空当前已选择的数组state.selectedCells = []// 将当前单元格高亮cell.classList.add('highlight')// 将数据加入数组state.selectedCells.push({ row: row.name, column: column.property, value: row[column.property] });}// 将单元格点击标识和鼠标移动标识置为falsestate.isCellClick = falsestate.isMouseDown = false}// 当鼠标落下时const handleMouseDown = event => {if (!isInSelectableArea(event)) return;// 判断是否在可选区域内state.isMouseDown = true/*** 在鼠标落下时,应当判断是ctrl+鼠标左键触发的事件,还是直接由鼠标左键触发的事件* 若是直接由鼠标左键点击触发的事件,则应该清空当前的selectedCells数组,并移除所有单元格的的高亮*/if (!event.ctrlKey) {const highlightedCells = document.querySelectorAll('.highlight')highlightedCells.forEach(cell => cell.classList.remove('highlight'))state.selectedCells = []}state.rectVisible = truestate.downX = event.clientXstate.downY = event.clientYstate.upX = event.clientXstate.upY = event.clientYstate.rect.left = document.querySelector('.wrap').getBoundingClientRect().leftstate.rect.top = document.querySelector('.wrap').getBoundingClientRect().topstate.rect.width = 0state.rect.height = 0}// 当鼠标移动时const handleMouseMove = event => {if (!state.rectVisible || !isInSelectableArea(event)) return;// 判断是否在可选区域内if (state.rectVisible) {const moveX = event.clientXconst moveY = event.clientY// 计算框选矩形的宽高state.rect.width = Math.abs(moveX - state.downX)state.rect.height = Math.abs(moveY - state.downY)state.rect.left = Math.min(moveX, state.downX) - event.currentTarget.getBoundingClientRect().leftstate.rect.top = Math.min(moveY, state.downY) - event.currentTarget.getBoundingClientRect().top}}// 当鼠标抬起时const handleMouseUp = (event) => {if (!state.rectVisible || !isInSelectableArea(event)) return;// 判断是否在可选区域内if (state.rectVisible) {state.rectVisible = false// 获取所有单元格const cells = document.querySelectorAll('.el-table__body-wrapper td')const rect = state.rect// 判断是否有一些单元格已经高亮let anyHighlighted = false// 用于存放被框选的单元格const selectedCells = []cells.forEach(cell => {const cellRect = cell.getBoundingClientRect()const tableRect = document.querySelector('.wrap').getBoundingClientRect()// 计算相对位置const cellLeft = cellRect.left - tableRect.leftconst cellTop = cellRect.top - tableRect.top// 判断单元格是否在框选区域内const cellInSelection = (cellLeft < rect.left + rect.width &&cellLeft + cellRect.width > rect.left &&cellTop < rect.top + rect.height &&cellTop + cellRect.height > rect.top)if (cellInSelection) {selectedCells.push(cell)}})if (selectedCells.length > 1) {selectedCells.forEach(sltCell => {// 判断单元格是否已经高亮const isHighlighted = sltCell.classList.contains('highlight')if (isHighlighted) {anyHighlighted = true}// 若使用ctrl+鼠标左键if (event.ctrlKey) {// 若被框选的单元格全都没有高亮,则将这些单元格高亮,并将数据push到数组中if (!anyHighlighted) {sltCell.classList.add('highlight')state.selectedCells.push(getCellData(sltCell))} else {/*** 若被框选的单元格中,有已经高亮的单元格,则需要把其中高亮的单元格取消高亮,并把这些被取消高亮* 的单元格的数据从数组中移除* 同时,把没有高亮的单元格高亮,并将数据push到数组中*/if (isHighlighted) {sltCell.classList.remove('highlight')const idxToRemove = state.selectedCells.findIndex(sc => sc.row === getCellData(sltCell).row && sc.column === getCellData(sltCell).column)if (idxToRemove > -1) {state.selectedCells.splice(idxToRemove, 1)}} else {// 若当前没有高亮的,则高亮,并把数据添加到数组中sltCell.classList.add('highlight')state.selectedCells.push(getCellData(sltCell))}}} else {// 普通点击框选事件sltCell.classList.add('highlight')state.selectedCells.push(getCellData(sltCell))}                            })}}}// 获取单元格数据const getCellData = cell => {const cellContent = cell.querySelector('.cell-content')if (cellContent) {const row = cellContent.dataset.rowconst column = cellContent.dataset.columnconst value = cellContent.textContentreturn { row, column, value }}}// 根据当前的模式,动态获取数据const daysOfWeek = ['日', '一', '二', '三', '四', '五', '六' ]// 月份模式const getMonthDays = () => {const days = []let currYear = new Date().getFullYear()let currMonth = new Date().getMonth()// 获取当前月的第一天const startDate  = new Date(currYear, currMonth, 2)startDate.setHours(0, 0, 0, 0)  // 确保是当天的0点// 获取当前月的最后一天const endDate = new Date(currYear, currMonth + 1, 0)endDate.setHours(23, 59, 59, 999)  // 确保是当天的最后一毫秒const date = new Date(new Date(currYear, currMonth, 1))while(date.getMonth() === currMonth) {days.push({day: date.getDate().toString(),date: daysOfWeek[date.getDay()]})date.setDate(date.getDate() + 1)}// 转化为时间选择器可以使用的格式const minDateFormatted = startDate.toISOString().slice(0, 10)const maxDateFormatted = endDate.toISOString().slice(0, 10)emit('updateDateForm', { startDate: minDateFormatted, endDate: maxDateFormatted })return days}// 一周模式const getWeekDays = () => {const days = []const currDay = new Date().getDay()const startDate = new Date(new Date()) // 当选择了这些模式之后,应该把开始日期和结束日期传给父组件,以便父组件上的时间选择器来展示// 找到最小和最大的日期startDate.setDate(new Date().getDate() - currDay + 1) // 获取当前周的周一// 获取当前格式的当前周的周日const endDate = new Date(startDate)endDate.setDate(startDate.getDate() + 6)for (let i = 0; i < 7; i++) {const d = new Date(startDate)d.setDate(startDate.getDate() + i)days.push({day: d.getDate().toString(),date: daysOfWeek[d.getDay()]})}// 转化为时间选择器可以使用的格式const minDateFormatted = startDate.toISOString().slice(0, 10)const maxDateFormatted = endDate.toISOString().slice(0, 10)emit('updateDateForm', { startDate: minDateFormatted, endDate: maxDateFormatted })return days}// 两周模式const getTwoWeeksDays = () => {const days = []const currDay = new Date().getDay()const startDate = new Date(new Date()) startDate.setDate(new Date().getDate() - currDay + 1) // 获取当前周的周一// 获取当前格式的当前周的周日const endDate = new Date(startDate)endDate.setDate(startDate.getDate() + 13)for (let i = 0; i < 14; i++) {const d = new Date(startDate)d.setDate(startDate.getDate() + i)days.push({day: d.getDate().toString(),date: daysOfWeek[d.getDay()]})}// 转化为时间选择器可以使用的格式const minDateFormatted = startDate.toISOString().slice(0, 10)const maxDateFormatted = endDate.toISOString().slice(0, 10)emit('updateDateForm', { startDate: minDateFormatted, endDate: maxDateFormatted })return days}// 自定义选择日期模式const getCustomDateRange = (startDate, endDate) => {const days = []const start = new Date(startDate)const end = new Date(endDate)const date = new Date(start)while (date <= end) {days.push({day: date.getDate().toString(),date: daysOfWeek[date.getDay()]})date.setDate(date.getDate() + 1)}return days}// 获取当前日期const isCrrentDay = () => {let d = new Date().getDate()return d.toString()}// 判断是否是周末const isWeekend = (date, day) => {if (day === isCrrentDay()) return 'currDay';if (date === '六' || date === '日') return 'weekend';else return ''}const headerCellStyle = (row, column, rowIndex, columnIndex) => {// console.log('row', row);}const CellStyle = (row, column) => { // console.log('row', row);if (row.column.className === 'weekend') {return {backgroundColor: 'rgb(116, 107, 230)'}}}const currDateArr = computed(() => {if (!props.customDateFlag && props.switchModel === 'month') {return getMonthDays()} else if (!props.customDateFlag && props.switchModel === 'week') {return getWeekDays()} else if (!props.customDateFlag && props.switchModel === 'twoWeeks') {return getTwoWeeksDays()} else if (props.customDateFlag) {return getCustomDateRange(props.dateForm.startDate, props.dateForm.endDate)}})var currWidth = ref(0)watchEffect(() => {currWidth.value = computed(() => props.tableWidth).value// 根据当前日期,给表头设置背景if (currDateArr.value.length > 0) {const ths = document.querySelectorAll('.el-table__header .el-table__cell')if (ths.length > 0) {// 获取当前日期let date = new Date().getDay()console.log('date', date);}}})onUpdated(() => {// 根据当前日期,给表头设置背景if (currDateArr.value.length > 0) {const ths = document.querySelectorAll('.el-table__header .el-table__cell')if (ths.length > 0) {// 获取当前日期let date = new Date().getDay()}}})// 动态设置列的宽度const getColumnWidth = () => {const containerWidth = currWidth.value // 减去name列的值const columnCount = currDateArr.value.length + 1return `${Math.floor(containerWidth / columnCount)}px`}return {...toRefs(state),handleCellClick,currDateArr,getProp,handleMouseDown,handleMouseMove,handleMouseUp,getColumnWidth,isWeekend,headerCellStyle,CellStyle}}
})
</script><style style="scss" scoped>
/* 当单元格被选择时,高亮 */
::v-deep .el-table td.highlight {background-color: yellow!important;color: red;
}::v-deep .el-table thead th.weekend {background-color: rgb(116, 107, 230)!important;
}::v-deep .el-table th.currDay {background-color: green!important;
}::v-deep .el-table .headerSelect {background-color: green!important;
}.container {width: 100%;.wrap {width: 100%;height: 100vh;position: relative;display: flex;/* 子项超出容器宽度时自动换行 */flex-wrap: wrap;/* 禁止用户复制文本 */user-select: none;.select-rect {position: absolute;border: 1px dashed #999;background-color: rgba(0,0,0,0.1);z-index: 1000;pointer-events: none;}.con {max-width: 100%;}}
}
</style>

效果演示

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

因为此表格还在持续开发中,所以现在代码还有很多有待优化和完善的地方,还请谅解。
希望本文对您能有所帮助!
感谢阅读!

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 测试面试宝典(四十六)— 在项目中如何保证软件质量?
  • 数组的复制
  • C#初级——List 容器
  • C/C++开发,opencv光流法跟踪特征点
  • 17085 工作分配问题(优先做)
  • C# 设计模式之抽象工厂模式
  • 定时器知识点
  • Go语言加Vue3零基础入门全栈班15 gin+gorm+vue3用户管理系统实战录播课 2024年08月04日 课程笔记
  • Python爬虫与MongoDB的完美结合
  • 《零散知识点 · 自定义 HandleMapping》
  • 鸿蒙媒体开发【相机数据采集保存】拍照和图片
  • 大模型术语表
  • 24年第五届“华数杯”数学建模竞赛浅析
  • 利用ffmpeg转码视频为gif图片,调整gif图片的大小
  • 全球氢燃料电池汽车市场规划预测:未来六年CAGR为44.4%
  • [分享]iOS开发-关于在xcode中引用文件夹右边出现问号的解决办法
  • 《深入 React 技术栈》
  • 【从零开始安装kubernetes-1.7.3】2.flannel、docker以及Harbor的配置以及作用
  • Apache Zeppelin在Apache Trafodion上的可视化
  • co.js - 让异步代码同步化
  • emacs初体验
  • ES10 特性的完整指南
  • LeetCode29.两数相除 JavaScript
  • react 代码优化(一) ——事件处理
  • SpringBoot几种定时任务的实现方式
  • swift基础之_对象 实例方法 对象方法。
  • 对JS继承的一点思考
  • 个人博客开发系列:评论功能之GitHub账号OAuth授权
  • 警报:线上事故之CountDownLatch的威力
  • 驱动程序原理
  • 深入体验bash on windows,在windows上搭建原生的linux开发环境,酷!
  • 智能合约开发环境搭建及Hello World合约
  • 看到一个关于网页设计的文章分享过来!大家看看!
  • Semaphore
  • Unity3D - 异步加载游戏场景与异步加载游戏资源进度条 ...
  • 阿里云IoT边缘计算助力企业零改造实现远程运维 ...
  • ​【经验分享】微机原理、指令判断、判断指令是否正确判断指令是否正确​
  • ​Benvista PhotoZoom Pro 9.0.4新功能介绍
  • ​卜东波研究员:高观点下的少儿计算思维
  • #laravel 通过手动安装依赖PHPExcel#
  • (11)工业界推荐系统-小红书推荐场景及内部实践【粗排三塔模型】
  • (4)STL算法之比较
  • (javaweb)Http协议
  • (多级缓存)缓存同步
  • (附源码)ssm码农论坛 毕业设计 231126
  • (力扣题库)跳跃游戏II(c++)
  • (七)Appdesigner-初步入门及常用组件的使用方法说明
  • (七)微服务分布式云架构spring cloud - common-service 项目构建过程
  • (三)docker:Dockerfile构建容器运行jar包
  • (十三)Flink SQL
  • (一)Docker基本介绍
  • (转)winform之ListView
  • *p++,*(p++),*++p,(*p)++区别?
  • *算法训练(leetcode)第四十七天 | 并查集理论基础、107. 寻找存在的路径
  • .gitignore文件忽略的内容不生效问题解决