koa实现excel数据导入导出功能

nodejs yekong

数据可视化大屏项目中,后台管理数据往往需要导入导出的功能以供快速操作数据,今天我们以后端koa来实现导入导出的功能。

接收文件

app.js文件内,我们需要配置一下,文件接收

import koaBody from 'koa-body';

function applyBodyParse(app) {
    app.use(koaBody({
        multipart: true,
        formidable: {
            maxFileSize: 200 * 1024 * 1024 // 设置上传文件大小限制,例如200MB
        }
    }));
}


后端api接口

import { LinRouter, NotFound } from "lin-mizar";
import { PositiveIdValidator } from "../../validator/common";
import { CreateOrUpdateCompanyValidator } from "../../validator/company";
import { CompanyDao } from "../../dao/company";
import * as XLSX from 'xlsx';
import fs from 'fs';
import { Company } from '../../model/company';
const companyApi = new LinRouter({
    prefix: "/v1/company"
});

const companyDao = new CompanyDao();

const responseWrapper = (ctx, data, msg = '操作成功') => {
    ctx.body = {
        code: 1,
        data,
        msg
    };
};

const errorHandler = (ctx, error) => {
    console.error(error);
    ctx.body = {
        code: 0,
        data: null,
        msg: error.message || '操作失败'
    };
};

companyApi.get("/", async (ctx) => {
    try {
        const page = parseInt(ctx.query.page) || 1;
        const pageSize = parseInt(ctx.query.pageSize) || 10;
        const { items, total } = await companyDao.getCompanies(page, pageSize);
        responseWrapper(ctx, { list: items, total, page, pageSize }, '获取公司列表成功');
    } catch (error) {
        errorHandler(ctx, error);
    }
});
companyApi.post("/import", async (ctx) => {
    const file = ctx.request.files.file;
    if (!file) {
        throw new NotFound({ code: 10024, message: '没有上传文件' });
    }

    try {
        const fileContent = fs.readFileSync(file.filepath);
        const workbook = XLSX.read(fileContent, { type: 'buffer' });
        const sheetName = workbook.SheetNames[0];
        const worksheet = workbook.Sheets[sheetName];
        const rawData = XLSX.utils.sheet_to_json(worksheet);

        const parsedData = rawData.map(row => ({
            name: row['名称'] || '',
            industryChainName: row['产业链'] || '',
            about: row['简介'] || '',
            img: row['图片'] || '',
            latitude: row['纬度'] || null,
            longitude: row['经度'] || null
        }));

        const importResult = await Company.importCompanyData(parsedData);
        responseWrapper(ctx, importResult, '导入成功');
    } catch (error) {
        errorHandler(ctx, error);
    }
});

// 添加导出路由
companyApi.get("/export", async (ctx) => {
    try {
        const buffer = await Company.exportCompanyData();
        ctx.set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
        ctx.set('Content-Disposition', 'attachment; filename=companies.xlsx');
        ctx.body = buffer;
    } catch (error) {
        errorHandler(ctx, error);
    }
});

后端model

import {Model, DataTypes} from 'sequelize';
import sequelize from '../lib/db';
import * as XLSX from 'xlsx';

class Company extends Model {
    static associate(models) {
        Company.belongsTo(models.IndustryChain, {
            foreignKey: 'industry_chain_id',
            as: 'industryChain',
            onDelete: 'SET NULL',
            onUpdate: 'CASCADE'
        });
        Company.hasMany(models.Talent, {
            foreignKey: 'company_id',
            as: 'talents'
        });
        Company.hasMany(models.Platform, {
            foreignKey: 'company_id',
            as: 'platforms'
        });
    }

    static async importCompanyData(parsedData) {
        const transaction = await sequelize.transaction();
        try {
            const importedData = [];
            const failures = [];
            let successCount = 0;
            let failureCount = 0;

            for (const [index, data] of parsedData.entries()) {
                try {
                    let industryChain = null;
                    if (data.industryChainName) {
                        [industryChain] = await sequelize.models.IndustryChain.findOrCreate({
                            where: {name: data.industryChainName},
                            defaults: {name: data.industryChainName},
                            transaction
                        });
                    }

                    const companyData = {
                        name: data.name,
                        industry_chain_id: industryChain ? industryChain.id : null,
                        about: data.about,
                        img: data.img,
                        latitude: data.latitude,
                        longitude: data.longitude
                    };

                    const [company, created] = await Company.findOrCreate({
                        where: {name: data.name},
                        defaults: companyData,
                        transaction
                    });

                    if (!created) {
                        await company.update(companyData, {transaction});
                    }

                    importedData.push(company);
                    successCount++;
                } catch (error) {
                    console.error(`导入第 ${index + 1} 行时出错:`, error);
                    failures.push({row: index + 1, error: error.message});
                    failureCount++;
                }
            }

            await transaction.commit();
            return {successCount, failureCount, failures};
        } catch (error) {
            await transaction.rollback();
            throw error;
        }
    }

    static async exportCompanyData() {
        try {
            const companyData = await Company.findAll({
                include: [{model: sequelize.models.IndustryChain, as: 'industryChain', attributes: ['name']}]
            });

            const formattedData = companyData.map(data => ({
                名称: data.name || '',
                产业链: data.industryChain ? data.industryChain.name : '',
                简介: data.about || '',
                图片: data.img || '',
                纬度: data.latitude || '',
                经度: data.longitude || ''
            }));

            const workbook = XLSX.utils.book_new();
            const worksheet = XLSX.utils.json_to_sheet(formattedData);
            XLSX.utils.book_append_sheet(workbook, worksheet, "公司数据");
            const buffer = XLSX.write(workbook, {type: 'buffer', bookType: 'xlsx'});

            return buffer;
        } catch (error) {
            console.error('导出公司数据时发生错误:', error);
            throw error;
        }
    }
}

Company.init(
    {
        id: {
            type: DataTypes.INTEGER,
            primaryKey: true,
            autoIncrement: true
        },
        name: {
            type: DataTypes.STRING(100),
            allowNull: false
        },
        industry_chain_id: {
            type: DataTypes.INTEGER,
            allowNull: true
        },
        about: {
            type: DataTypes.TEXT,
            allowNull: true
        },
        img: {
            type: DataTypes.STRING(255),
            allowNull: true
        },
        latitude: {
            type: DataTypes.DECIMAL(10, 8),
            allowNull: true,
            comment: '纬度'
        },
        longitude: {
            type: DataTypes.DECIMAL(11, 8),
            allowNull: true,
            comment: '经度'
        }
    },
    {
        sequelize,
        tableName: 'company',
        modelName: 'Company'
    }
);

export {Company};

前端请求传文件

接口封装

// 导出
export function exportCompany() {
    return request({
        url: '/v1/company/export',
        method: 'get',
        responseType: 'blob' // 重要:指定响应类型为blob
    })
}

// 导入
export function importCompany(file) {
    const formData = new FormData()
    formData.append('file', file)
    return request({
        url: '/v1/company/import',
        method: 'post',
        data: formData,
        headers: {
            'Content-Type': 'multipart/form-data'
        }
    })
}

html

<div class="topInfoRight">
    <el-button class="mr10" type="success" @click="handleExport">导出数据</el-button>
    <el-upload
        class="upload-demo"
        action="#"
        :on-change="handleFileChange"
        :auto-upload="false"
        :show-file-list="false"
    >
      <el-button type="warning">导入数据</el-button>
    </el-upload>
  </div>

js逻辑代码

import {importCompany,exportCompany} from '@/api/api/company'

const handleExport = async () => {
  try {
    const response = await exportCompany()
    const blob = new Blob([response], {type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'})
    const link = document.createElement('a')
    link.href = window.URL.createObjectURL(blob)
    link.download = '公司管理.xlsx'
    link.click()
    window.URL.revokeObjectURL(link.href)
    ElMessage.success('导出成功')
  } catch (error) {
    console.error('导出失败:', error)
    ElMessage.error('导出失败')
  }
}

const handleFileChange = async (file) => {
  if (file.raw) {
    const response = await importCompany(file.raw)
    console.log(response)
    if (response.code === 1) {
      ElMessage.success(response.msg)
      fetchData() // 刷新数据
    } else {
      ElMessage.error(response.msg || '导入失败')
    }
  }
}

到这里,一个koa导出导入的逻辑就大概完成了。

喜欢