CSV文件导入功能

基于vue2+Element UI 的csv数据导入和展示组件,主要功能包括:

1、文件上传:用户可以通过点击按钮选择并上传CSV文件

2、CSV解析:自动解析csv文件内容,提取表头和数据

3、数据展示:将解析后的数据显示在Element UI表格中,效果与原始csv文件完全一致

4、数据导出:提供解析后的结构化数据,便于后续业务处理


具体代码:

1

复制代码
utils/importCsv.js

/**
 * CSV文件解析工具函数
 * 使用xlsx库解析CSV文件并返回结构化数据
 */

import * as XLSX from 'xlsx';

/**
 * 解析CSV文件的Blob或File对象
 * @param {Blob|File} file - CSV文件对象
 * @param {Object} options - 解析选项
 * @param {number} options.maxRows - 最大解析行数,默认10000
 * @param {boolean} options.hasHeader - 是否包含表头,默认true
 * @returns {Promise<Object>} 解析结果包含headers和data
 */
export async function parseCsvFile(file, options = {}) {
  const {
    maxRows = 10000,
    hasHeader = true
  } = options;

  return new Promise((resolve, reject) => {
    const reader = new FileReader();

    reader.onload = (e) => {
      try {
        const data = new Uint8Array(e.target.result);
        const workbook = XLSX.read(data, { 
          type: 'array', 
          sheetRows: maxRows 
        });

        const firstSheetName = workbook.SheetNames[0];
        const worksheet = workbook.Sheets[firstSheetName];

        // Convert to JSON array with raw values
        const jsonData = XLSX.utils.sheet_to_json(worksheet, { 
          header: 1,
          raw: true,
          defval: ''
        });

        if (jsonData.length === 0) {
          resolve({ headers: [], data: [], rawData: [] });
          return;
        }

        let headers = [];
        let dataRows = [];

        if (hasHeader && jsonData.length > 0) {
          // First row as headers
          headers = jsonData[0].map((header, index) => {
            return String(header || `列${index + 1}`);
          });
          dataRows = jsonData.slice(1);
        } else {
          // No headers, generate column names
          const colCount = Math.max(...jsonData.map(row => row.length));
          headers = Array.from({ length: colCount }, (_, i) => `列${i + 1}`);
          dataRows = jsonData;
        }

        // Format data for table display
        const formattedData = dataRows.map(row => {
          const rowData = {};
          headers.forEach((_, index) => {
            rowData['col' + index] = row[index] !== undefined ? row[index] : '';
          });
          return rowData;
        });

        resolve({
          headers,
          data: formattedData,
          rawData: dataRows
        });
      } catch (error) {
        console.error('CSV解析错误:', error);
        reject(new Error('CSV文件解析失败'));
      }
    };

    reader.onerror = () => {
      reject(new Error('文件读取失败'));
    };

    reader.readAsArrayBuffer(file);
  });
}

/**
 * 将CSV字符串解析为结构化数据
 * @param {string} csvString - CSV格式的字符串
 * @param {Object} options - 解析选项
 * @returns {Object} 解析结果包含headers和data
 */
export function parseCsvString(csvString, options = {}) {
  const {
    hasHeader = true,
    delimiter = ','
  } = options;

  try {
    // Create a temporary workbook from CSV string
    const workbook = XLSX.read(csvString, { 
      type: 'string',
      cellDates: true
    });

    const firstSheetName = workbook.SheetNames[0];
    const worksheet = workbook.Sheets[firstSheetName];

    const jsonData = XLSX.utils.sheet_to_json(worksheet, { 
      header: 1,
      raw: true,
      defval: ''
    });

    if (jsonData.length === 0) {
      return { headers: [], data: [], rawData: [] };
    }

    let headers = [];
    let dataRows = [];

    if (hasHeader && jsonData.length > 0) {
      headers = jsonData[0].map((header, index) => {
        return String(header || `列${index + 1}`);
      });
      dataRows = jsonData.slice(1);
    } else {
      const colCount = Math.max(...jsonData.map(row => row.length));
      headers = Array.from({ length: colCount }, (_, i) => `列${i + 1}`);
      dataRows = jsonData;
    }

    const formattedData = dataRows.map(row => {
      const rowData = {};
      headers.forEach((_, index) => {
        rowData['col' + index] = row[index] !== undefined ? row[index] : '';
      });
      return rowData;
    });

    return {
      headers,
      data: formattedData,
      rawData: dataRows
    };
  } catch (error) {
    console.error('CSV字符串解析错误:', error);
    throw new Error('CSV字符串解析失败');
  }
}

export default {
  parseCsvFile,
  parseCsvString
};

2

复制代码
components/CsvImport.vue

<template>
  <div class="csv-import-container">
    <!-- Upload button -->
    <el-upload
      ref="upload"
      :action="uploadAction"
      :auto-upload="false"
      :show-file-list="false"
      :on-change="handleFileChange"
      :accept="'.csv'"
      class="csv-upload"
    >
      <el-button type="primary" icon="el-icon-upload">
        {{ uploadButtonText }}
      </el-button>
    </el-upload>

    <!-- Display parsed CSV data as table -->
    <div v-if="csvData.length > 0" class="csv-table-container">
      <el-table
        :data="csvData"
        border
        style="width: 100%"
        :header-cell-style="{ background: '#f5f7fa', color: '#606266', 'text-align': 'center' }"
        :cell-style="{ 'text-align': 'left' }"
      >
        <el-table-column
          v-for="(header, index) in headers"
          :key="index"
          :prop="'col' + index"
          :label="header"
          :width="getColumnWidth(header)"
        >
        </el-table-column>
      </el-table>
    </div>

    <!-- Empty state -->
    <div v-else class="empty-state">
      <i class="el-icon-document"></i>
      <p>请上传CSV文件查看数据</p >
    </div>
  </div>
</template>

<script>
import * as XLSX from 'xlsx';

export default {
  name: 'CsvImport',
  props: {
    uploadButtonText: {
      type: String,
      default: '导入CSV表格'
    }
  },
  data() {
    return {
      uploadAction: '#', // Prevent actual upload
      csvData: [],
      headers: []
    };
  },
  methods: {
    handleFileChange(file, fileList) {
      const fileName = file.name;
      const fileExtension = fileName.substring(fileName.lastIndexOf('.')).toLowerCase();

      if (fileExtension !== '.csv') {
        this.$message.error('请上传CSV格式文件');
        this.$refs.upload.clearFiles();
        return;
      }

      // Read the file
      const reader = new FileReader();
      reader.onload = (e) => {
        try {
          const data = new Uint8Array(e.target.result);
          const workbook = XLSX.read(data, { type: 'array', sheetRows: 10000 }); // Limit rows for performance
          const firstSheetName = workbook.SheetNames[0];
          const worksheet = workbook.Sheets[firstSheetName];

          // Convert to JSON array
          const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });

          if (jsonData.length === 0) {
            this.$message.warning('CSV文件为空');
            this.csvData = [];
            this.headers = [];
            return;
          }

          // Extract headers (first row)
          this.headers = jsonData[0].map((header, index) => {
            return header || `列${index + 1}`;
          });

          // Process data rows
          const dataRows = jsonData.slice(1);
          this.csvData = dataRows.map(row => {
            const rowData = {};
            this.headers.forEach((_, index) => {
              rowData['col' + index] = row[index] || '';
            });
            return rowData;
          });

          // Emit the parsed data for parent component usage
          this.$emit('csv-imported', {
            headers: this.headers,
            data: this.csvData,
            rawData: jsonData.slice(1)
          });

          this.$message.success('CSV文件导入成功');
        } catch (error) {
          console.error('CSV解析错误:', error);
          this.$message.error('CSV文件解析失败,请检查文件格式');
          this.csvData = [];
          this.headers = [];
        }
      };

      reader.onerror = () => {
        this.$message.error('文件读取失败');
        this.csvData = [];
        this.headers = [];
      };

      reader.readAsArrayBuffer(file.raw);
    },

    getColumnWidth(header) {
      // Calculate column width based on header length, with reasonable limits
      const baseWidth = 100;
      const charWidth = 12;
      const headerLength = String(header).length;
      const calculatedWidth = baseWidth + (headerLength * charWidth);
      return Math.min(Math.max(calculatedWidth, 80), 300); // Min 80px, Max 300px
    },

    // Method to clear imported data
    clearData() {
      this.csvData = [];
      this.headers = [];
      if (this.$refs.upload) {
        this.$refs.upload.clearFiles();
      }
    },

    // Method to get current CSV data
    getCsvData() {
      return {
        headers: this.headers,
        data: this.csvData
      };
    }
  }
};
</script>

<style scoped lang="scss">
.csv-import-container {
  .csv-upload {
    margin-bottom: 20px;

    ::v-deep .el-upload {
      display: inline-block;
    }
  }

  .csv-table-container {
    margin-top: 16px;

    ::v-deep .el-table {
      font-size: 12px;

      .el-table__header th {
        font-weight: bold;
      }

      .el-table__body td {
        word-break: break-word;
        white-space: pre-wrap;
      }
    }
  }

  .empty-state {
    text-align: center;
    padding: 40px 0;
    color: #909399;

    i {
      font-size: 48px;
      margin-bottom: 16px;
      color: #c0c4cc;
    }

    p {
      margin: 0;
      font-size: 14px;
    }
  }
}
</style>

3

复制代码
views/CsvImportDemo.vue

<template>
  <div class="csv-import-demo">
    <h2>CSV表格导入示例</h2>
    <p>点击下方按钮上传CSV文件,页面将显示与CSV表格中完全一致的数据效果</p >

    <CsvImport 
      ref="csvImport"
      upload-button-text="选择CSV文件导入"
      @csv-imported="handleCsvImported"
    />

    <!-- Additional info panel -->
    <div v-if="importInfo" class="import-info">
      <el-card class="box-card">
        <div slot="header" class="clearfix">
          <span>导入信息</span>
        </div>
        <div>
          <p><strong>文件列数:</strong> {{ importInfo.headers.length }}</p >
          <p><strong>数据行数:</strong> {{ importInfo.data.length }}</p >
          <p><strong>列名:</strong> {{ importInfo.headers.join(', ') }}</p >
        </div>
        <div style="margin-top: 16px;">
          <el-button type="danger" size="small" @click="clearImport">清空数据</el-button>
        </div>
      </el-card>
    </div>
  </div>
</template>

<script>
import CsvImport from '@/components/CsvImport.vue';

export default {
  name: 'CsvImportDemo',
  components: {
    CsvImport
  },
  data() {
    return {
      importInfo: null
    };
  },
  methods: {
    handleCsvImported(data) {
      this.importInfo = data;
      console.log('CSV导入完成:', data);
    },

    clearImport() {
      this.importInfo = null;
      this.$refs.csvImport.clearData();
    }
  }
};
</script>

<style scoped lang="scss">
.csv-import-demo {
  padding: 20px;

  h2 {
    margin-bottom: 16px;
    color: #303133;
  }

  p {
    margin-bottom: 24px;
    color: #606266;
    line-height: 1.5;
  }

  .import-info {
    margin-top: 24px;

    ::v-deep .el-card__body {
      padding: 16px;

      p {
        margin: 8px 0;
        font-size: 14px;
      }
    }
  }
}
</style>
相关推荐
evans在进步24 分钟前
Java 常用设计模式(二):装饰器、适配器、责任链、模板方法、策略与观察者
java·开发语言·设计模式
梨想橙汁33 分钟前
JS 核心语法:运算符流程控制、函数、数组与对象实战详解
前端·javascript
七牛开发者34 分钟前
拆解 dsh:Turn 与 Step 如何组织 Agent 主循环
前端·javascript·人工智能
空想兔34 分钟前
前端网络请求的前世今生:从 Ajax、XHR 到 Fetch 和 Axios
javascript
梨想橙汁35 分钟前
JavaScript 零基础入门:引入方式、变量与数据类型,吃透原始与引用类型
前端·javascript
WebInfra1 小时前
Rslib 1.0 正式发布:面向多场景的 JavaScript 库开发工具
前端·javascript·github
小雨笙笙1 小时前
C语言:指针
c语言·开发语言
旧梦95271 小时前
Java EnumMap 详解:原理、用法与实战
java·开发语言
汉堡大王95271 小时前
面试官:讲讲归并排序 —— 为什么它能在面试里反复出现?
前端·javascript·面试