「简记往来」开发历程系列:数据导出功能的技术实现——CSV生成与下载

一、需求:用户想导出备份

简记往来上线后,有用户反馈:"我想把礼金记录导出到Excel,方便备份和二次分析。"

数据导出是一个重要的功能------它让用户感觉"数据是自己的",而不是被工具锁住的。

二、数据查询与组装

导出功能的第一步:查询数据并组装成CSV格式。

javascript 复制代码
async function exportRecords(bookId) {
    // 1. 查询账本的所有记录
    const records = await db.records.find({ bookId }).toArray()
    
    // 2. 查询所有联系人
    const contacts = await db.contacts.find({ bookId }).toArray()
    const contactMap = {}
    contacts.forEach(c => { contactMap[c.id] = c.name })
    
    // 3. 组装数据
    const rows = records.map(r => ({
        date: r.date,
        type: r.type === 'receive' ? '收礼' : '送礼',
        name: contactMap[r.contactId] || '未知',
        amount: r.amount,
        note: r.note || '',
        createdAt: r.createdAt
    }))
    
    return rows
}

三、CSV格式生成

CSV的生成需要注意几个问题:

  1. 中文乱码:需要添加BOM(Byte Order Mark)
  2. 字段中包含逗号:需要用双引号包裹
  3. 字段中包含换行:需要用双引号包裹
javascript 复制代码
function generateCSV(rows) {
    // 1. 定义表头
    const headers = ['日期', '类型', '姓名', '金额', '备注', '创建时间']
    
    // 2. 构建CSV内容
    let csv = headers.join(',') + '\n'
    
    for (const row of rows) {
        const fields = [
            row.date,
            row.type,
            `"${row.name}"`,  // 姓名可能包含逗号,用引号包裹
            row.amount,
            `"${row.note}"`,  // 备注可能包含逗号或换行
            row.createdAt
        ]
        csv += fields.join(',') + '\n'
    }
    
    // 3. 添加BOM(解决中文乱码)
    return '\uFEFF' + csv
}

四、浏览器下载触发

在微信小程序中,CSV文件通过以下方式下载:

javascript 复制代码
// 1. 生成CSV内容
const csv = generateCSV(rows)

// 2. 保存到本地文件
const fs = wx.getFileSystemManager()
const filePath = `${wx.env.USER_DATA_PATH}/礼金记录_${Date.now()}.csv`
fs.writeFileSync(filePath, csv, 'utf-8')

// 3. 触发分享/保存
wx.shareFileMessage({
    filePath: filePath,
    fileName: `礼金记录_${Date.now()}.csv`,
    success: () => {
        wx.showToast({ title: '导出成功', icon: 'success' })
    },
    fail: () => {
        wx.showToast({ title: '导出失败', icon: 'none' })
    }
})

五、完整代码

javascript 复制代码
async function exportBookData(bookId) {
    try {
        // 1. 查询数据
        const records = await db.records.find({ bookId }).toArray()
        const contacts = await db.contacts.find({ bookId }).toArray()
        const contactMap = {}
        contacts.forEach(c => { contactMap[c.id] = c.name })
        
        // 2. 组装行数据
        const rows = records.map(r => ({
            date: r.date,
            type: r.type === 'receive' ? '收礼' : '送礼',
            name: contactMap[r.contactId] || '未知',
            amount: r.amount,
            note: r.note || '',
            createdAt: r.createdAt
        }))
        
        // 3. 生成CSV
        const headers = ['日期', '类型', '姓名', '金额', '备注', '创建时间']
        let csv = headers.join(',') + '\n'
        for (const row of rows) {
            csv += [
                row.date,
                row.type,
                `"${row.name}"`,
                row.amount,
                `"${row.note}"`,
                row.createdAt
            ].join(',') + '\n'
        }
        csv = '\uFEFF' + csv  // 添加BOM
        
        // 4. 保存并分享
        const fs = wx.getFileSystemManager()
        const filePath = `${wx.env.USER_DATA_PATH}/礼金记录_${Date.now()}.csv`
        fs.writeFileSync(filePath, csv, 'utf-8')
        
        wx.shareFileMessage({
            filePath: filePath,
            fileName: `礼金记录_${Date.now()}.csv`,
            success: () => wx.showToast({ title: '导出成功', icon: 'success' }),
            fail: () => wx.showToast({ title: '导出失败', icon: 'none' })
        })
    } catch (err) {
        console.error('导出失败', err)
        wx.showToast({ title: '导出失败', icon: 'none' })
    }
}

六、总结

数据导出功能的关键点:

  1. BOM处理:解决中文乱码问题
  2. 字段转义:逗号和换行需要用双引号包裹
  3. 用户体验:导出完成后给用户明确反馈

数据导出让用户感觉"数据是自己的",而不是被工具锁住的。这是建立用户信任的重要一环。