自学内容网 自学内容网

vue3+ts+element-ui实现的可编辑table表格组件 插入单行多行 组件代码可直接使用

最近需求越来越离谱,加班越来越严重,干活的牛马也越来越卑微。写了一个可编辑表格,并已封装好组件,可直接使用。

基于这位大佬的 动态表格自由编辑 方法和思路,于是参考和重写了表格,在基础上增加和删除了一些功能点。

实现功能如下:

1、双击单元格可编辑格子内容,输入框和文本的高度自适应。
2、右击表格弹出菜单,可插入单行、多行和删除行。
3、可配置字段是否可以编辑。

效果图

在这里插入图片描述

组件参数截图

在这里插入图片描述

使用组件

在这里插入图片描述

使用组件代码
<!-- 可编辑表格 -->
<EditTableForm 
 :list="state.questionChoiceVOlist" 
 :headerList="columnList" 
 :selectUserList="scorerUserList" 
 :forbidPop="['itemScore','scorerUserIdList','expirationDay']" />
组件EditTableForm完整代码
<template>
  <el-table
  :data="tableData" 
  @cell-dblclick="cellDblclick" 
  @row-contextmenu="cellRightClick"
  :row-class-name="tableRowClassName" border>
    <el-table-column
      type="index"
      label="序号"
      align="center"
      :resizable="false"
      width="70"
    />

    <el-table-column
      :resizable="false"
      align="left"
      v-for="(col, idx) in columnList"
      :key="col.prop"
      :prop="col.prop"
      :label="col.label"
      :index="idx"
    >
     <template #default="scope">
        <el-input-number
          v-if="col.type === 'input-number'"
          v-model.number="scope.row[col.prop]"
          style="width: 120px;"
          :min="0"
          :max="100"
          :step="1"
        />
        <el-select 
          v-if="col.type && col.type === 'select'" 
          v-model="scope.row[col.prop]" 
          multiple
          :multiple-limit="1"
          filterable 
          clearable
          collapse-tags
          collapse-tags-tooltip
          placeholder="请选择">
          <el-option
            v-for="item in props.selectUserList"
            :key="item.id"
            :label="item.nickname"
            :value="item.id"
          />
        </el-select>
        <el-select 
          v-if="col.type && col.type === 'select-day'" 
          v-model="scope.row[col.prop]" 
          clearable
          collapse-tags
          placeholder="截止日期">
          <el-option
            v-for="item in 15"
            :key="item"
            :label="item + '号'"
            :value="item"
          />
        </el-select>
        <div
          class="cell-text"
          v-if="!scope.row[`${col.prop}_ifWrite`] && !isPop(scope.column)" 
          v-html="filterHtml(scope.row[col.prop])">
        </div>
        <el-input 
          v-show="scope.row[`${col.prop}_ifWrite`]" 
          :ref="setInputRef(scope.$index, col.prop)"
          type="textarea" 
          autosize
          :maxRows="4"
          v-model="scope.row[col.prop]" 
          @blur="scope.row[`${col.prop}_ifWrite`] = false" />
     </template>
    </el-table-column>
  </el-table>

  <!-- 右键菜单框 -->
  <div v-show="showMenu" id="contextmenu" @mouseleave="menuMouseleave">
    <el-button type="primary" @click="addRow(false)" v-show="!curTarget.isHead">上方插入一行</el-button>
    <el-button @click="openAddMore(false)" v-show="!curTarget.isHead">上方插入多行</el-button>
    <el-button type="primary" @click="addRow(true)" v-show="!curTarget.isHead">下方插入一行</el-button>
    <el-button @click="openAddMore(true)" v-show="!curTarget.isHead">下方插入多行</el-button>
    <el-button type="danger" @click="delRow" v-show="!curTarget.isHead" >删除当前行</el-button>

    <el-dialog
    v-model="visible"
    title="请输入行数"
    width="200"
    align-center
    draggable
  >
    <el-input-number style="width: 100%;" v-model="addMoreNumber" :min="1" :max="20" />
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="visible = false" style="width: 49%;">取消</el-button>
        <el-button type="primary" style="width: 49%;" @click="addMoreRow(addMorelater,addMoreNumber)">
          确定
        </el-button>
      </div>
    </template>
  </el-dialog>
  </div>
</template>

<script lang="ts" setup>
defineOptions({ name: 'EditTableForm' })

const props = defineProps({
  // 表格数据
  list: {
    type: Array as PropType<any[]>,
    default: () => [],
  },
  // 表格表头
  headerList: {
    type: Array as PropType<{ prop: string; label: string; type?: string }[]>,
    default: () => [],
  },
  // 禁止填写的字段
  forbidPop: {
    type: Array as PropType<string[]>,
    default: () => [],
  },
  // 评分人列表
  selectUserList: {
    type: Array as PropType<{ id: number; nickname: string }[]>,
    default: () => [],
  },
})

// const itemBox = {
//   id: 1,
//   date: '',
//   name: '',
//   address: '',
// }
// tableData.forEach(item => {
//   columnList.forEach(col => {
//     item[col.prop + '_ifWrite'] = false;
//     item[col.prop + '_ref'] = null;
//   })
// })

interface Target {
rowIdx: number | null;
colIdx: number | null;
val: string | null;
isHead: boolean | undefined;
}

const state = reactive({
  tableData: [] as any[],
  columnList: props.headerList as any[],
  itemBox: getItemBox(props.list) as {},
showMenu: false, // 显示右键菜单
  curTarget: {
// 当前目标信息
rowIdx: null, // 行下标
colIdx: null, // 列下标
val: null, // 单元格内容/列名
isHead: undefined, // 当前目标是表头?
} as Target,
});
const { tableData,columnList,showMenu,curTarget } = toRefs(state);
const inputRefs = ref<{ [key: string]: any }>({});
const visible = ref(false)
const addMoreNumber = ref(1);
const addMorelater= ref(false);

/**监听表头的变化 */
watch(
  () => props.headerList,
  (val:any) => {
    if (!val) return
    state.columnList = val;
  },
  {
    deep: true,
    immediate: true
  }
)
/**监听表格数据的变化 */
watch(
  () => props.list,
  async (val: any) => {
    if (!val || val.length === 0) return;

    // 使用 nextTick 确保 DOM 更新后进行操作,防止 offsetHeight 报错
    await nextTick();

    // 在此处进行安全的表格数据更新
    // console.log('表格数据已更新:', val);
    state.tableData = val;
  },
  { deep: true, immediate: true }
)

/**设置添加的行元素 */
function getItemBox(data:Array<any>) {
  let obj:any = {};
  if(data.length > 0){
    let item = data[0];
    for(let key of Object.keys(item)){
      obj[key] = '';
    }
    for (const col of props.headerList) {
      obj[col.prop + '_ifWrite'] = false;
      obj[col.prop + '_ref'] = null;
    }
  }
  return obj;
}


// 打开添加行弹窗
const openAddMore = (val:boolean) => {
  visible.value = true;
  addMorelater.value = val;
}

// 鼠标移出菜单
const menuMouseleave = ()=>{
  showMenu.value = false;
  visible.value = false;
}

// 这个方法动态为每个 el-input 实例设置 ref,并将其存储在 inputRefs 中,以便后续访问。
const setInputRef = (rowIdx: number, colProp: string) => (el: any) => {
  inputRefs.value[`${rowIdx}-${colProp}`] = el;
};

// 添加表格行下标和控制每个row的显示隐藏字段
const tableRowClassName = ({ row, rowIndex }: { row: any; rowIndex: number }) => {
row.row_index = rowIndex;
};

// 控制某字段不能打开弹框
const isPop = (column: { index: null; property: string; label: string }) => {
// return column.property === "itemScore" || column.property === "scorerUserIdList" || column.property === "expirationDay";
  return props.forbidPop.includes(column.property);
};

// 双击左键输入框
const cellDblclick = (
row: { [x: string]: any; row_index: any },
column: any,
cell: HTMLTableCellElement,
event: MouseEvent
) => {
  // 如果禁填项,不执行后续代码
if (isPop(column)) return;
  // 显示输入框的控制
  Object.keys(row).forEach(key => {
    // endsWith判断 以_ifWrite结束的
    if (key.endsWith('_ifWrite')) {
      row[key] = false;
    }
  })
  row[`${column.property}_ifWrite`] = true;
  // 获取input焦点
  nextTick(() => {
    const inputKey = `${row.row_index}-${column.property}`;
    const inputComponent = inputRefs.value[inputKey];
    if (inputComponent) {
      inputComponent.focus();
    }
  });
};

// 单元格右击事件 - 打开菜单
const cellRightClick = (row: any, column: any, event: MouseEvent) => {
  // 阻止事件的默认行为(禁止浏览器右键菜单)
event.preventDefault();
showMenu.value = false;
  // 定位菜单/编辑框
locateMenuOrEditInput('contextmenu', -500, event); // 右键输入框
showMenu.value = true;
  // 获取当前单元格的值
curTarget.value = {
rowIdx: row ? row.row_index : null,
colIdx: column.index,
val: row ? row[column.property] : column.label,
isHead: !row,
};
};


// 新增行
const addRow = (later: boolean) => {
showMenu.value = false;
const idx = later ? curTarget.value.rowIdx! + 1 : curTarget.value.rowIdx!;
let obj: any = {
    ...state.itemBox,
id: Math.floor(Math.random() * 100000),
  };
state.tableData.splice(idx, 0, obj);
};

// 新增多行
const addMoreRow = (later: boolean,lineNum: number) => {
showMenu.value = false;
const idx = later ? curTarget.value.rowIdx! + 1 : curTarget.value.rowIdx!;
  // 创建一个包含要插入数据的新数组
  let newRows: any[] = [];
  for (let i = 0; i < lineNum; i++) {
      let obj: any = {
          ...state.itemBox,
          id: Math.floor(Math.random() * 100000),
      };
      newRows.push(obj);
  }
  // 使用Vue的响应式更新方法来插入新数据
  // 这里假设tableData是通过reactive函数创建的响应式数据
  state.tableData.splice(idx, 0,...newRows);
};

// 删除行
const delRow = () => {
  ElMessageBox.confirm(`此操作将永久删除该行, 是否继续 ?`, {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
      showMenu.value = false;
    curTarget.value.rowIdx !== null && state.tableData.splice(curTarget.value.rowIdx!, 1);
ElMessage.success('删除成功');
})
.catch(() => ElMessage.info('已取消删除'));
};

// 定位菜单/编辑框
const locateMenuOrEditInput = (eleId: string, distance: number, event: MouseEvent) => {
if (window.innerWidth < 1130 || window.innerWidth < 660)
return ElMessage.warning('窗口太小,已经固定菜单位置,或请重新调整窗口大小');
const ele = document.getElementById(eleId) as HTMLElement;
const x = event.pageX;
const y = event.clientY + 200; //右键菜单位置 Y
let left = x + distance + 200; //右键菜单位置 X
let top;
if (eleId == 'editInput') {
// 左键
top = y + distance;
left = x + distance + 50;
} else {
// 右键
top = y + distance + 180;
    left = x + distance + 270;
}
ele.style.left = `${left}px`;
ele.style.top = `${top}px`;
};

// 缓存每个分组的合并信息
const mergeCache = ref<Record<number, [number, number]>>({});

// 合并单元格的方法
const objectSpanMethod = ({
  row,
  column,
  rowIndex,
  columnIndex,
}) => {
  if (columnIndex === 1) {
    // 检查缓存中是否已经存在该行的合并数据
    if (mergeCache.value[rowIndex]) {
      return mergeCache.value[rowIndex];
    }

    // 计算需要的合并行数
    let rowspan = 1;
    for (let i = rowIndex + 1; i < tableData.value.length; i++) {
      if (tableData.value[i].bigTargetClassify === row.bigTargetClassify) {
        rowspan++;
      } else {
        break;
      }
    }

    // 更新缓存
    mergeCache.value[rowIndex] = [rowspan, 1];

    // 对于合并行中的其他行,缓存 [0, 0] 表示隐藏
    for (let i = 1; i < rowspan; i++) {
      mergeCache.value[rowIndex + i] = [0, 0];
    }

    return [rowspan, 1];
  }
};

// 当数据变化时清除缓存
watch(tableData, () => {
  mergeCache.value = {};
});


// 字符串格式转换
function filterHtml(text){
  if(text) {
    let text1 = String(text);
    return text1.replace(/(\r\n|\n)/g, '<br/>')
  }else{
    return text
  }
}

</script>
<style lang="scss" scoped>
:deep(.el-textarea__inner){
  padding: 0;
}
.cell-text{
  width: 100%;
  min-height: 100%;
  // min-height: 42px;
}
/* 右键 */
#contextmenu {
  position: absolute;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  z-index: 999;
  top: 0;
  left: 0;
  height: auto;
  width: 180px;
  border-radius: 3px;
  border: #e2e2e2 1px solid;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
  background-color: #fff;
  border-radius: 6px;
  padding: 15px 10px 14px 12px;

  button {
    display: block;
    margin: 0 0 5px;
    width: 100%;
  }
}
.dialog-footer{
  display: flex;
  justify-content: space-between;
}
</style>


原文地址:https://blog.csdn.net/qq_31676725/article/details/143641149

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!