1.使用背景:
vue2+elementUI
<template>
<!-- 仿墨刀 图表静态数据配置 -->
<div class="table-out">
<div class="dialog-content">
<div class="dialog-table-parent">
<table class="table-use dialog-table">
<thead class="thead">
<tr>
<th v-for="(item, index) in dialogTableHead" :key="index">
<div :class="{'header-th': true, 'header-th-last': index == dialogTableHead.length - 1}">
<input type="text" :value="item" :disabled="!item && index == 0" @change="headEdit($event, item, index)" @click.stop="inputActive(item, index)" @click.right="iptRight($event, item, index, null, 'head')" :class="{'inputUse':true, 'active': headActiveIndex == index}"/>
</div>
</th>
</tr>
</thead>
<tbody class="tbody">
<tr v-for="(item, index) in dialogTableData" :key="index">
<!-- eslint-disable-next-line vue/no-use-v-if-with-v-for -->
<td v-for="(item1, key, index1) in item" :key="index1" v-if="key != 'color' && key != 'type'">
<div :class="{'tbody-td': true, 'tbody-td-bottom-last': index == dialogTableData.length - 1, 'tbody-td-right-last': index1 == Object.keys(item).length - 1}">
<el-color-picker v-model="item.color" size="mini" v-if="key == 'dataName'" :predefine="colorArray" style="position: absolute;"></el-color-picker>
<svg class="parLine" v-if="key == 'dataName' && chartType == 'line'" @click.stop="lineClick(item, item1, key, index, index1)" width="15" height="28" viewBox="0 0 1024 1024" :fill="item.type == 'line' ? '#d81e06' : '#8a8a8a'"><path d="M991.573333 957.44H66.56V32.426667C66.56 15.36 51.2 0 34.133333 0 15.36 0 0 15.36 0 32.426667v954.026666c0 10.24 5.12 18.773333 11.946667 25.6 5.12 6.826667 15.36 11.946667 23.893333 11.946667H989.866667c18.773333 0 32.426667-15.36 32.426666-32.426667 1.706667-18.773333-13.653333-34.133333-30.72-34.133333z" p-id="7630"></path><path d="M223.573333 482.986667c-17.066667 0-30.72 13.653333-30.72 30.72V887.466667c0 17.066667 13.653333 30.72 30.72 30.72h87.04c17.066667 0 30.72-13.653333 30.72-30.72V513.706667c0-17.066667-13.653333-30.72-30.72-30.72h-87.04zM592.213333 411.306667h-76.8c-20.48 0-35.84 17.066667-35.84 35.84v435.2c0 20.48 17.066667 35.84 35.84 35.84h76.8c20.48 0 35.84-17.066667 35.84-35.84V447.146667c0-20.48-15.36-35.84-35.84-35.84zM878.933333 298.666667h-80.213333c-18.773333 0-34.133333 15.36-34.133333 34.133333v549.546667c0 18.773333 15.36 34.133333 34.133333 34.133333h80.213333c18.773333 0 34.133333-15.36 34.133334-34.133333V332.8c0-18.773333-15.36-34.133333-34.133334-34.133333zM168.96 455.68c11.946667 0 22.186667-5.12 30.72-13.653333l215.04-213.333334 148.48 143.36c17.066667 17.066667 44.373333 17.066667 61.44 0L913.066667 92.16c17.066667-17.066667 17.066667-44.373333 0-63.146667-17.066667-17.066667-44.373333-17.066667-63.146667 0l-256 249.173334L443.733333 134.826667c-17.066667-17.066667-44.373333-17.066667-61.44 0L138.24 378.88c-17.066667 17.066667-17.066667 46.08 0 63.146667 8.533333 8.533333 20.48 13.653333 30.72 13.653333z" :fill="item.type == 'line' ? '#d81e06' : '#8a8a8a'"></path></svg>
<input type="text" :value="item1" @change="tbodyEdit($event, item, item1, key, index, (index1 - 1))" @click.stop="tbodyInputActive(item, item1, key, index, index1)" @click.right="iptRight($event, item, index, index1, 'tbody')" :class="{'inputUse':true, 'dialogInput': true, 'active': tbodyActiveIndex == `${index}-${index1}`}"/>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="right-add" @click="rightAdd">+</div>
<div class="bottom-add" @click="bottomAdd">+</div>
</div>
<!-- 右键点击事件封装 -->
<right-menu :rightClickInfo="rightClickInfo"
@insertTopRow="insertTopRow"
@insertBottomRow="insertBottomRow"
@deleteRow="deleteRow"
@insertLeftColumn="insertLeftColumn"
@insertRightColumn="insertRightColumn"
@deleteColumn="deleteColumn"
></right-menu>
</div>
</template>
<script>
import rightMenu from './rightMenu'
export default {
components: { rightMenu },
props: {
chartType: { // 柱状图的时候 指定某一条数据为线
type: String,
default: 'line'
}
},
data () {
return {
dialogTableHead: ['', '值1'], // 列表头部
dialogTableData: [ // 列表内容
{ color: '#409EFF', dataName: '数据1', val1: 10 }
],
headActiveIndex: null, // 头部选中
tbodyActiveIndex: null, // 内容选中
colorArray: ['#c23531', '#2f4554', '#61a0a8', '#d48265', '#91c7ae', '#749f83', '#ca8622', '#bda29a', '#6e7074', '#546570', '#c4ccd3'],
rightClickInfo: {}, // 右键
finallyTableData: [] // 最终结果
}
},
watch: {
dialogTableHead: { // 表格头部
handler: function () {
this.realTimeChange()
},
deep: true
},
dialogTableData: { // 表格内容
handler: function () {
this.realTimeChange()
},
deep: true
}
},
mounted(){
// 取消全部选中
this.$nextTick(() => {
const dialogDom = document.querySelector('.table-out')
dialogDom.addEventListener('click', (e) => {
e.stopPropagation()
this.headActiveIndex = null
this.tbodyActiveIndex = null
})
})
},
methods: {
// 右侧添加按钮
rightAdd () {
this.dialogTableHead.push(`值${this.dialogTableHead.length}`)
this.dialogTableData.forEach(v => {
this.$set(v, 'val' + (this.dialogTableHead.length - 1), '100')
})
},
// 底部添加按钮
bottomAdd () {
const obj = {
dataName: `数据${this.dialogTableData.length}`,
color: ''
}
this.dialogTableHead.forEach((v, index) => {
if (index < this.dialogTableHead.length - 1) {
obj['val' + (index + 1)] = '100'
}
})
obj.color = this.colorArray[Math.floor(Math.random() * this.colorArray.length)] // 颜色随机
this.dialogTableData.push(obj)
},
// 表头选中
inputActive (item, index) {
this.tbodyActiveIndex = null
this.headActiveIndex = index
},
// 内容选中
tbodyInputActive (item, item1, key, index, index1) {
this.headActiveIndex = null
this.tbodyActiveIndex = `${index}-${index1}`
},
// 表头修改
headEdit (e, item, index) {
this.$set(this.dialogTableHead, index, e.target.value)
this.headActiveIndex = null
},
// 内容修改
tbodyEdit (e, item, item1, key, index) {
this.$set(this.dialogTableData[index], `${key}`, e.target.value)
this.tbodyActiveIndex = null
},
// 线形式的选中
lineClick (item, item1, key, index) {
if (this.dialogTableData[index].type != 'line') {
this.$set(this.dialogTableData[index], `type`, 'line')
} else {
this.$delete(this.dialogTableData[index], `type`)
}
},
// 右键点击事件
iptRight (event, item, index, index1, type) {
event.preventDefault() // 阻止默认的鼠标右击事件
if (type == 'head' && !item && index == 0) { return }
this.rightClickInfo = {
position: {
x: event.clientX,
y: event.clientY
},
menulists: [
{
fnName: 'insertTopRow',
params: { item, index, index1, type, event },
// icoName: 'el-icon-document-copy',
btnName: '上方插入一行',
disabled: type == 'head'
},
{
fnName: 'insertBottomRow',
params: { item, index, index1, type, event },
btnName: '下方插入一行'
},
{
fnName: 'deleteRow',
params: { item, index, index1, type, event },
btnName: '删除行',
disabled: type == 'head' || this.dialogTableData.length == 1
},
{
fnName: 'insertLeftColumn',
params: { item, index, index1, type, event },
btnName: '左侧插入一列',
disabled: type == 'tbody' && index1 == 0
},
{
fnName: 'insertRightColumn',
params: { item, index, index1, type, event },
btnName: '右侧插入一列'
},
{
fnName: 'deleteColumn',
params: { item, index, index1, type, event },
btnName: '删除列',
disabled: (type == 'tbody' && index1 == 0) || this.dialogTableHead.length == 2
}
]
}
},
// 行上方插入
insertTopRow ({ item, index }) {
const obj = JSON.parse(JSON.stringify(item))
obj.color = this.colorArray[Math.floor(Math.random() * this.colorArray.length)] // 颜色随机
this.dialogTableData.splice(index, 0, obj)
this.dialogTableData.forEach((v, idx) => {
this.$set(v, 'dataName', `数据${idx + 1}`)
})
},
// 行下方插入
insertBottomRow ({ item, index }) {
const obj = JSON.parse(JSON.stringify(item))
obj.color = this.colorArray[Math.floor(Math.random() * this.colorArray.length)] // 颜色随机
this.dialogTableData.splice(index + 1, 0, obj)
this.dialogTableData.forEach((v, idx) => {
this.$set(v, 'dataName', `数据${idx + 1}`)
})
},
// 删除当前行
deleteRow ({ index }) {
this.dialogTableData.splice(index, 1)
this.dialogTableData.forEach((v, idx) => {
this.$set(v, 'dataName', `数据${idx + 1}`)
})
},
// 插入列的时候处理数据
updateDataArray (dataArray, key, opType) {
const keyIndex = parseInt(key.replace('val', ''), 10)
dataArray.forEach(item => {
// 获取所有以'val'开头的键,并转换为其索引
let valKeys = Object.keys(item).filter(k => k.startsWith('val')).map(k => parseInt(k.replace('val', ''), 10))
let maxValIndex = Math.max(...valKeys) // 获取最大的索引值
if (opType === 'add') {
// 添加操作
if (keyIndex === maxValIndex + 1) {
item[key] = item[`val${maxValIndex}`] // 直接设置新键的值
} else {
for (let i = maxValIndex; i >= keyIndex; i--) {
item[`val${i + 1}`] = item[`val${i}`] // 递增现有键
}
item[key] = item[`val${keyIndex}`]
}
} else if (opType === 'del') {
// 删除操作
if (item[key] !== undefined) {
delete item[key]
for (let i = keyIndex + 1; i <= maxValIndex; i++) {
item[`val${i - 1}`] = item[`val${i}`] // 递减后面的键
}
delete item[`val${maxValIndex}`] // 删除最后一个键
}
}
})
return dataArray
},
// 左侧插入列
insertLeftColumn ({ index, index1, type }) {
if (type == 'head') {
this.dialogTableHead.splice(index, 0, `值${index}`)
this.dialogTableData = this.updateDataArray(this.dialogTableData, `val${index}`, 'add')
} else {
this.dialogTableHead.splice(index1, 0, `值${index1}`)
this.dialogTableData = this.updateDataArray(this.dialogTableData, `val${index1}`, 'add')
}
},
// 右侧插入列
insertRightColumn ({ index, index1, type }) {
if (type == 'head') {
this.dialogTableHead.splice(index + 1, 0, `值${index + 1}`)
this.dialogTableData = this.updateDataArray(this.dialogTableData, `val${index + 1}`, 'add')
} else {
this.dialogTableHead.splice(index1 + 1, 0, `值${index1}`)
this.dialogTableData = this.updateDataArray(this.dialogTableData, `val${index1}`, 'add')
}
},
// 删除当前列
deleteColumn ({ index, index1, type }) {
if (type == 'head') {
this.dialogTableHead.splice(index, 1)
this.dialogTableData = this.updateDataArray(this.dialogTableData, `val${index}`, 'del')
} else {
this.dialogTableHead.splice((index1 - 1), 1)
this.dialogTableData = this.updateDataArray(this.dialogTableData, `val${index1 - 1}`, 'del')
}
},
// 内修改变动时需要组件实时显示数据变化
realTimeChange () {
// 最终结果finallyTableData
this.finallyTableData = [
{
categoryArray: this.dialogTableHead.filter((v, index) => index > 0),
dataArray: this.dialogTableData
}
]
// 赋值(更新)
this.$emit('tableChange', this.finallyTableData)
}
}
}
</script>
<style scoped lang="less">
.table-use{
width: 100%;
overflow: hidden;
}
.thead{
border: 1px solid #ebeef5;
padding: 0;
background-color: #f5f7fa;
}
.thead tr, .thead th, .tbody tr, .tbody td{
border: 0;
padding: 0;
box-sizing: border-box;
}
.header-th{
min-width: 70px;
height: 30px;
font-size: 14px;
color: #909399;
font-weight: 500;
text-align: center;
line-height: 30px;
border-right: 1px solid #ebeef5;
}
.header-th-last{
border-right: 0;
}
.tbody{
border: 1px solid #ebeef5;
padding: 0;
background-color: #fff;
}
.tbody-td{
min-width: 70px;
height: 30px;
font-size: 14px;
color: #606266;
text-align: right;
line-height: 30px;
border-right: 1px solid #ebeef5;
border-bottom: 1px solid #ebeef5;
position: relative;
}
.tbody-td{
/deep/.el-color-picker__trigger{
border: none;
padding: 0;
}
/deep/.el-color-picker--mini .el-color-picker__trigger{
width: 12px;
}
}
.tbody-td-bottom-last{
border-bottom: 0;
}
.tbody-td-right-last{
border-right: 0;
}
/* 表格样式 */
.dialog-content{
width: 100%;
position: relative;
overflow: hidden;
}
.dialog-table-parent{
width: calc(100% - 22px);
max-height: 400px;
overflow: auto;
font-size: 0;
}
.dialog-content .dialog-table{
width: 100%;
}
.dialog-content .right-add{
position: absolute;
right: 0;
top: 0;
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: calc(100% - 22px);
border: 1px solid #ebeef5;
background-color: #fff;
cursor: pointer;
}
.dialog-content .right-add:hover, .dialog-content .bottom-add:hover{
background-color: #f5f7fa;
}
.dialog-content .bottom-add{
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 20px;
border: 1px solid #ebeef5;
background-color: #fff;
cursor: pointer;
margin-top: 2px;
}
.inputUse{
border: 1px solid transparent;
background-color: transparent;
width: 100%;
height: 100%;
text-align: center;
box-sizing: border-box;
}
.dialogInput{
text-align: right;
padding-right: 4px;
}
.active{
border: 1px solid #409EFF;
}
.parLine{
position: absolute;
left: 14px;
font-size: 12px;
height: 28px;
cursor: pointer;
}
</style>
2.鼠标右键封装组件: rightMenu
<template>
<ul class="table-right-menu">
<!-- 循环菜单项,事件带参数抛出 -->
<li
v-for="item in rightClickInfo.menulists"
:key="item.btnName"
:class="{'table-right-menu-item': true, 'right-menu-disabled': item.disabled }"
@click.stop="fnHandler(item)"
>
<div class="table-right-menu-item-btn">
<!-- 图标和按钮名 -->
<!-- <i :class="item.icoName" class="iii" /> -->
<span>{{ item.btnName }}</span>
</div>
</li>
</ul>
</template>
<script>
export default {
name: 'rightMenu',
props: {
// 接收右键点击的信息
rightClickInfo: {
type: Object,
default: () => {
return {
position: {
// 右键点击的位置
x: null,
y: null
},
menulists: [
{
fnName: '', // 点击菜单项的事件名
params: {}, // 点击的参数
btnName: '' // 按钮名
}
]
}
}
}
},
watch: {
// 监听右键点击时点击位置的变化,只要变化了,就弹出右键菜单供用户点击操作
'rightClickInfo.position' (val) {
let x = val.x // 获取x轴坐标
let y = val.y // 获取y轴坐标
let innerWidth = window.innerWidth // 获取页面可是区域宽度,即页面的宽度
let innerHeight = window.innerHeight // 获取可视区域高度,即页面的高度
/**
* 注意,这里要使用getElementsByClassName去选中对应dom,因为右键菜单组件可能被多处使用
* classIndex标识就是去找到对应的那个右键菜单组件的,需要加的
* */
let menu = document.getElementsByClassName('table-right-menu')[0]
menu.style.display = 'block'
let menuHeight = this.rightClickInfo.menulists.length * 30 // 菜单容器高
let menuWidth = 180 // 菜单容器宽
// 菜单的位置计算
menu.style.top = (y + menuHeight > innerHeight ? innerHeight - menuHeight : y) + 'px'
menu.style.left = (x + menuWidth > innerWidth ? innerWidth - menuWidth : x) + 'px'
// 因为菜单还要关闭,就绑定一个鼠标点击事件,通过e.button判断点击的是否是左键,左键关闭菜单
document.addEventListener('mouseup', this.hide, false)
}
},
methods: {
hide (e) {
if (e.button === 0) {
// 0是左键、1是滚轮按钮或中间按钮(若有)、2鼠标右键
let menu = document.getElementsByClassName('table-right-menu')[0] // 同样的精确查找
menu.style.display = 'none' // 菜单关闭
document.removeEventListener('mouseup', this.hide) // 及时解绑监听事件
}
},
fnHandler (item) {
if (!item.disabled) {
this.$emit(item.fnName, item.params)
}
}
}
}
</script>
<style lang='less' scoped>
.table-right-menu {
background: rgb(51, 51, 51);
border-radius: 4px;
list-style-type: none;
box-shadow: rgba(0, 0, 0, 0.3) 0px 2px 8px;
font-size: 12px;
font-weight: 500;
box-sizing: border-box;
padding: 4px 0;
// 固定定位,抬高层级,初始隐藏,右击时置为display:block显示
position: fixed;
z-index: 3000;
display: none;
.table-right-menu-item {
box-sizing: border-box;
padding: 4px 20px;
transition: all 0.36s;
cursor: pointer;
color: #fff;
.table-right-menu-item-btn {
.iii {
margin-right: 4px;
}
}
}
.table-right-menu-item:hover {
background-color: rgb(204, 204, 204);
}
.right-menu-disabled{
color: rgb(162, 163, 163);
cursor: not-allowed;
}
.right-menu-disabled:hover{
background-color: transparent;
}
}
</style>