基于Element-Plus实现的Excel表格(读取、编辑等功能)

前言

有许多库可以可以实现excel表格的预览、编辑等功能,比如Handsontable、Ag-Grid、Luckysheet等等。但是为什么个人要自己手动实现呢?原因是因为这些库要么停止维护了,要么有些库有问题,用起来很麻烦,更更更重要的是有些库是要付费的(资深白嫖党绝对不能接受!!!),再加上自己的需求和页面风格等等,最后决定自己手动实现。

废话少说,先上图,看完图后不想复制代码的可以关闭该标签页了:
在这里插入图片描述
好了,当你看到这句话时就表明你很大概率想复制代码了,那就开始动手实现吧。

引入依赖

npm install exceljs #该库用来解析excel文档

该库相关介绍就不说了,如果想仔细去了解的话,点击:exceljs的github地址

文档解析

文档上传使用的是Element-Plus的el-upload组件,当文件状态改变时会执行on-change函数,所以我们可以在该函数内解析excel文档。

interface ExcelTableProps {
    [tag: string]: {
        headers: any[],
        bodyData: any[][]
    }
}

这是文档解析后的数据类型,因为一个excel文档可能有多张表,所以用一个对象来保存,其中对象的键值tag是表名;每张表由表头headers和具体内容bodyData组成。因为表头指的是第一行,所以用一个数组就行,而具体内容则对应的是二维数组;

读取文档内容(代码讲解看注释):


const fileContent = ref<ExcelTableProps>({} as ExcelTableProps);

const handleChange = async (uploadFile: UploadFile) => {
  fileContent.value = {};
  const workbook = new ExcelJS.Workbook();
  const buffer = await uploadFile.raw?.arrayBuffer();
  if (!buffer)  return;
  //加载文件内容
  await workbook.xlsx.load(buffer);
  //遍历文档中没一张表
  workbook.eachSheet((worksheet, sheetId) => {
    const headers: any[] = [];//表头的内容,也就是第一行
    const bodyData: any[][] = [];//表里具体的内容
    //行读取
    worksheet.eachRow((row, rowNumber) => {
      const rowValues = (row.values as any[]).slice(1);//获取每行数据后删掉第一个,第一个一般都是empty值
      if (rowNumber === 1) {
        headers.push(...rowValues);
      } else {
        bodyData.push(rowValues);
      }
    });
    //以表名作为键值,没有则手动生成一个
    if (worksheet.name) {
      fileContent.value[worksheet.name] = {
        headers,
        bodyData,
      };
    } else {
      fileContent.value[`Sheet${sheetId}`] = {
        headers,
        bodyData,
      };
    }
  })
}

到这里为止,一张excel文档就被解析完成了,接下来就是展示文档的内容。这里要用到Element-Plus的el-tabs组件来模拟excel中表名的列表,但不同的是,excel中的表名是在下面,而el-tabs的表名是在上面,所以需要做样式穿透,具体代码如下(代码讲解看注释):

ExcelTable组件:

<script setup lang="ts">
import {ref, watch} from "vue";

interface PropsObj {
  data: ExcelTableProps
}

const props = defineProps<PropsObj>();

const editableTabsValue = ref(Object.keys(props.data)[0] || '');

/** 表格数据 */
const tableData = ref<any[][]>([]);
const headerData = ref<any[]>([]);
const sheets = ref<string[]>(Object.keys(props.data));

function setData() {
  if (editableTabsValue.value !== '') {
    const {bodyData, headers} = props.data[editableTabsValue.value];
    tableData.value = [...bodyData];
    headerData.value = [...headers];
  }
}

setData();


/** 每列的宽度(初始化为 150px) */
const columnWidths = ref<number[]>(headerData.value.map(() => 150));

/** 当前调整的列 */
const resizingColumn = ref<number | null>(null); // 当前正在调整的列
const startX = ref<number>(0); // 鼠标按下的起始位置
const startWidth = ref<number>(0); // 列的初始宽度

/** 监听表头变化,动态初始化列宽 */
watch(
    () => headerData,
    (newHeaders) => {
      columnWidths.value = newHeaders.value.map(() => 150); // 每列初始化为 150px 宽
    }
);

/** 开始调整列宽 */
const startResize = (event: MouseEvent, colIndex: number) => {
  resizingColumn.value = colIndex;
  startX.value = event.clientX;
  startWidth.value = columnWidths.value[colIndex];

  document.addEventListener("mousemove", handleMouseMove);
  document.addEventListener("mouseup", stopResize);
};

/** 处理鼠标拖动事件 */
const handleMouseMove = (event: MouseEvent) => {
  if (resizingColumn.value !== null) {
    const delta = event.clientX - startX.value; // 鼠标移动的距离
    columnWidths.value[resizingColumn.value] = Math.max(
        startWidth.value + delta,
        50 // 设置列宽最小值
    );
  }
};

/** 停止调整列宽 */
const stopResize = () => {
  resizingColumn.value = null;
  document.removeEventListener("mousemove", handleMouseMove);
  document.removeEventListener("mouseup", stopResize);
};

/** 更新单元格数据 */
const updateCell = (rowIndex: number, colIndex: number, event: Event) => {
  tableData.value[rowIndex][colIndex] = (event.target as HTMLElement).innerText;
};

const updateHeadCell = (colIndex: number, event: Event) => {
  headerData.value[colIndex] = (event.target as HTMLElement).innerText;
}
</script>

<template>
  <div class="excel">
    <div class="table-container">
      <el-tabs
          v-model="editableTabsValue"
          type="card"
          class="demo-tabs"
          @tab-change="setData">
        <el-tab-pane v-for="(item,index) in sheets" :key="`tab-${index}`" :label="item" :name="item">
          <table border="1">
            <thead>
            <tr>
              <th></th>
              <th
                  v-for="(header, colIndex) in headerData"
                  :key="colIndex"
                  :style="{ width: columnWidths[colIndex] + 'px' }"
                  contenteditable="true"
                  @input="updateHeadCell( colIndex, $event)"
              >
                <div class="header-content">
                  {{ header }}
                  <div
                      class="resize-handle"
                      @mousedown="startResize($event, colIndex)"
                  ></div>
                </div>
              </th>
            </tr>
            </thead>
            <tbody>
            <tr v-for="(row, rowIndex) in tableData" :key="`t-${rowIndex}`">
              <td>{{ rowIndex + 1 }}</td>
              <td
                  v-for="(cell, colIndex) in row"
                  :key="`${rowIndex}-${colIndex}`"
                  contenteditable="true"
                  @input="updateCell(rowIndex, colIndex, $event)"
              >
                {{ cell }}
              </td>
            </tr>
            </tbody>
          </table>
        </el-tab-pane>
      </el-tabs>

    </div>
  </div>
</template>

<style scoped lang="scss">
.excel {
  height: calc(100% - 40px);
  padding: 10px;
  background-color: $pure-white;
  margin: 10px;
}
.table-container {
 height: 100%;
  overflow: auto;

  &::-webkit-scrollbar {
    width: 6px;
    height: 6px;
    background: #ffffff;
  }

  &::-webkit-scrollbar-thumb {
    width: 6px;
    height: 6px;
    background: rgba(100, 100, 100, .5);
  }
}

table {
  border-collapse: collapse;
  margin-bottom: 10px;
  border: 1px solid #ddd;
}

th:not([contenteditable="true"]), td:not([contenteditable="true"]) {
  background-color: #f4f4f4;
  width: 70px;
  text-align: center;
}

th[contenteditable="true"],
td[contenteditable="true"] {
  padding: 8px;
  text-align: left;
  position: relative;
  box-sizing: border-box;
  outline: none;

  &:focus {
    background-color: #ededed;
  }
}

.header-content {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.resize-handle {
  width: 5px;
  cursor: col-resize;
  position: absolute;
  right: 0;
  top: 0;
  bottom: 0;
  background-color: transparent;
}

:deep(.el-tabs) {
  display: block !important;


  .is-active {
    background-color: #67C23A;
    color: #FFFFFF;
  }

  .is-icon-close {
    display: none;
  }

  .el-tabs__header {
    background: #ffffff;
    position: absolute;
    width: calc(100% - 47px);
    top: calc(100% - 61px);
    left: 19px;

    border: 1px solid #dbdcdf;
    margin-bottom: 0;
  }
  .el-tabs__content{
    margin-bottom: 31px !important;
  }
}
</style>

最后的效果没有像前面的图片那样,可能是因为该组件的父元素需要配一些样式。比如:表明是固定在下面的,所以父元素需要加上position: relative;等属性;反正样式自己调吧,代码复制粘贴就能用。

最后,经过这两天的时间终于把它弄出来了,所以就分享出来给你们,因为我开心,哈哈哈哈!!!

实现通过el-upload组件上传Excel文件,并在上传到服务器之前进行文件格式判断的功能,可以按照以下步骤进行操作: 1. 在页面上定义一个上传按钮,使用el-upload组件,并设置相关属性,如action属性指定上传文件的接口地址,on-change属性指定文件变化时的回调函数,auto-upload属性设置为false以便手动触发上传,show-file-list属性设置为false隐藏文件列表,accept属性指定文件类型为.xls和.xlsx格式。 2. 在回调函数onChange中,获取到上传的文件,进行文件校验,判断文件是否是Excel文件。首先创建一个FormData对象,用于存储上传的文件。然后通过FormData的append方法将文件对象添加到FormData中。接着,通过file.name获取文件名,并使用.split(".")将文件名拆分为数组,取数组的第二个元素判断文件的后缀名。如果后缀名为xls、xlsx或csv,说明是Excel文件,将state.imgs设置为true表示文件格式正确,返回file对象。否则,返回false表示文件格式不正确。 3. 在确定导入未使用信息的方法onSubmit中,首先通过ruleFormRef.value.validate方法对表单进行验证。如果验证通过,调用unitregisterImport方法上传文件,并将state.unitId和state.formData作为参数传递给接口。接口返回结果为true时,表示导入成功,可以进行相关操作,如显示成功提示信息,刷新页面等。如果返回结果为false,表示导入失败。 下面是el-upload上传Excel表格的代码示例: ```javascript <template> <el-upload action="/上传文件的接口" :on-change="onChange" :auto-upload="false" :show-file-list="false" accept=".xls, .xlsx" > <el-button v-waves size="mini" type="warning" icon="el-icon-folder-add"> 上传 </el-button> </el-upload> </template> <script> export default { methods: { onChange(file) { let formData = new FormData(); formData.append("file", file); const Xls = file.name.split("."); if (Xls === "xls" || Xls === "xlsx" || Xls === "csv") { state.imgs = true; return file; } else { return false; } }, onSubmit() { ruleFormRef.value.validate((valid) => { if (valid) { unitregisterImport(state.unitId, state.formData).then((res) => { if (res == true) { state.innerVisible = true; methods.getDetails(); // ElMessage.success("导入成功"); } else { // ElMessage.warning("导入失败"); } }); } else { return false; } }); }, }, }; </script> ``` 请注意,上述代码仅为示例,具体根据你的项目需求进行适当调整。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [elemetUi 组件–el-upload实现上传Excel文件的实例](https://download.csdn.net/download/weixin_38574410/13192019)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [使用el-upload上传excel文件并读取显示到el-table上](https://blog.csdn.net/weixin_43258184/article/details/121850027)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [vue3+element-plus的el-upload上传excel表格](https://blog.csdn.net/weixin_50041614/article/details/126724906)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值