仿墨刀,图表静态数据配置表格组件封装

4 篇文章 0 订阅
1 篇文章 0 订阅

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>

  • 11
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值