import * as XLSX from 'xlsx-js-style' import dayjs from 'dayjs' /** * 格式化数字,添加千分位分隔符 */ function formatNumber(num) { if (typeof num !== 'number' || isNaN(num)) return num return num.toLocaleString('zh-CN') } /** * 计算列宽(根据内容自动调整) */ function calculateColumnWidth(data, columnKey) { if (!data || data.length === 0) return 15 const maxLength = Math.max( columnKey.length, // 表头长度 ...data.map(row => { const value = row[columnKey] if (value === null || value === undefined) return 0 return String(value).length }) ) // 最小宽度 8,最大宽度根据内容,但不超过 100 return Math.min(Math.max(maxLength + 2, 8), 100) } /** * 导出博主主页数据到 Excel(优化版) * @param {Array} data - 要导出的数据数组 * @param {Object} options - 导出配置选项 * @param {string} options.platform - 平台类型(抖音/小红书) * @param {Function} options.formatTime - 时间格式化函数 * @param {string} options.filename - 自定义文件名(可选) * @returns {Object} 导出结果 */ export function exportBenchmarkDataToExcel(data, options = {}) { const { platform = '抖音', formatTime, filename } = options if (!data || data.length === 0) { return { success: false, message: '暂无数据可导出' } } try { // 定义列配置 const columnConfig = [ { key: '序号', width: 8, align: 'center' }, { key: 'ID', width: 18, align: 'left' }, { key: '描述', width: 60, align: 'left', wrap: true }, { key: '点赞数', width: 12, align: 'right', format: 'number' }, { key: '评论数', width: 12, align: 'right', format: 'number' }, { key: '分享数', width: 12, align: 'right', format: 'number' }, { key: '收藏数', width: 12, align: 'right', format: 'number' }, { key: '播放量', width: 12, align: 'right', format: 'number' }, { key: '时长(秒)', width: 12, align: 'right', format: 'number' }, { key: '置顶', width: 8, align: 'center' }, { key: '创建时间', width: 20, align: 'left' }, { key: '链接', width: 60, align: 'left' }, { key: '封面链接', width: 60, align: 'left' }, { key: '音频链接', width: 60, align: 'left' }, { key: '原配音', width: 80, align: 'left', wrap: true }, { key: '风格提示词', width: 80, align: 'left', wrap: true } ] // 准备导出数据 const exportData = data.map((item, index) => { const row = { '序号': index + 1, 'ID': item.id || '', '描述': item.desc || '', '点赞数': item.digg_count || 0, '评论数': item.comment_count || 0, '分享数': item.share_count || 0, '收藏数': item.collect_count || 0, '播放量': item.play_count || 0, '时长(秒)': item.duration_s || 0, '置顶': item.is_top ? '是' : '否', '创建时间': formatTime ? formatTime(item.create_time) : (item.create_time || ''), '链接': item.share_url || '', '封面链接': item.cover || '', '音频链接': item.audio_url || '', '原配音': item.transcriptions || '', '风格提示词': item.prompt || '' } // 小红书平台特殊处理 if (platform === '小红书' && !row['播放量']) { row['浏览量'] = item.view_count || 0 // 调整列配置,将播放量改为浏览量 const playIndex = columnConfig.findIndex(col => col.key === '播放量') if (playIndex !== -1) { columnConfig[playIndex].key = '浏览量' } } // 确保数字字段为数字类型(不格式化,显示完整数字) columnConfig.forEach(col => { if (col.format === 'number' && row[col.key] !== undefined) { const num = Number(row[col.key]) if (!isNaN(num)) { row[col.key] = num // 直接使用数字,不格式化 } else { row[col.key] = 0 } } }) return row }) // 创建工作簿和工作表 const wb = XLSX.utils.book_new() const ws = XLSX.utils.json_to_sheet(exportData) // 优化列宽设置(根据实际内容动态计算) ws['!cols'] = columnConfig.map(col => { // 如果数据中有该列,根据实际内容计算宽度 const actualWidth = calculateColumnWidth(exportData, col.key) return { wch: Math.max(col.width, actualWidth) } }) // 设置冻结首行(冻结窗格) ws['!freeze'] = { xSplit: 0, ySplit: 1, topLeftCell: 'A2', activePane: 'bottomLeft', state: 'frozen' } // 设置行高(首行稍高,便于阅读) if (!ws['!rows']) ws['!rows'] = [] ws['!rows'][0] = { hpt: 25 } // 首行高度 25pt // 数据行高度 for (let i = 1; i <= exportData.length; i++) { ws['!rows'][i] = { hpt: 18 } } // 获取表头范围 const headerRange = XLSX.utils.decode_range(ws['!ref']) const lastCol = headerRange.e.c // 设置表头样式(加粗、背景色、居中对齐) const headerStyle = { font: { bold: true, color: { rgb: 'FFFFFF' }, sz: 11 }, fill: { fgColor: { rgb: '4472C4' } }, // 蓝色背景 alignment: { horizontal: 'center', vertical: 'center', wrapText: true }, border: { top: { style: 'thin', color: { rgb: '000000' } }, bottom: { style: 'thin', color: { rgb: '000000' } }, left: { style: 'thin', color: { rgb: '000000' } }, right: { style: 'thin', color: { rgb: '000000' } } } } // 应用表头样式 for (let col = 0; col <= lastCol; col++) { const cellAddress = XLSX.utils.encode_cell({ r: 0, c: col }) if (!ws[cellAddress]) ws[cellAddress] = { t: 's', v: '' } ws[cellAddress].s = headerStyle } // 设置数据行样式(边框、对齐方式) const dataStyle = { border: { top: { style: 'thin', color: { rgb: 'D9D9D9' } }, bottom: { style: 'thin', color: { rgb: 'D9D9D9' } }, left: { style: 'thin', color: { rgb: 'D9D9D9' } }, right: { style: 'thin', color: { rgb: 'D9D9D9' } } }, alignment: { vertical: 'top', wrapText: true } } // 应用数据行样式 for (let row = 1; row <= exportData.length; row++) { for (let col = 0; col <= lastCol; col++) { const cellAddress = XLSX.utils.encode_cell({ r: row, c: col }) if (ws[cellAddress]) { if (!ws[cellAddress].s) ws[cellAddress].s = {} // 根据列配置设置对齐方式 const colConfig = columnConfig[col] if (colConfig) { ws[cellAddress].s.alignment = { ...dataStyle.alignment, horizontal: colConfig.align || 'left' } } else { ws[cellAddress].s.alignment = dataStyle.alignment } // 设置边框 ws[cellAddress].s.border = dataStyle.border // 数字列右对齐,并确保为数字格式 if (colConfig && colConfig.format === 'number') { ws[cellAddress].s.alignment.horizontal = 'right' // 确保单元格类型为数字 if (typeof ws[cellAddress].v === 'number') { ws[cellAddress].t = 'n' // 数字类型 // 设置数字格式(不显示千分位,显示完整数字) ws[cellAddress].z = '0' } } } } // 斑马纹效果(偶数行浅灰色背景) if (row % 2 === 0) { for (let col = 0; col <= lastCol; col++) { const cellAddress = XLSX.utils.encode_cell({ r: row, c: col }) if (ws[cellAddress]) { if (!ws[cellAddress].s) ws[cellAddress].s = {} ws[cellAddress].s.fill = { fgColor: { rgb: 'F2F2F2' } } } } } } // 设置打印区域 const lastRow = exportData.length + 1 const lastColLetter = XLSX.utils.encode_col(lastCol) ws['!print'] = { area: `A1:${lastColLetter}${lastRow}`, margins: { left: 0.7, right: 0.7, top: 0.75, bottom: 0.75, header: 0.3, footer: 0.3 }, orientation: 'landscape', // 横向打印 paperSize: 9, // A4 fitToPage: true, fitToWidth: 1, fitToHeight: 0 } XLSX.utils.book_append_sheet(wb, ws, '博主主页数据') // 生成文件名 const timestamp = dayjs().format('YYYY-MM-DD_HH-mm-ss') const finalFilename = filename || `${platform}_博主主页_${timestamp}.xlsx` // 导出文件 XLSX.writeFile(wb, finalFilename) return { success: true, message: `已导出 ${exportData.length} 条数据`, count: exportData.length } } catch (error) { console.error('导出Excel失败:', error) return { success: false, message: '导出失败,请稍后重试', error: error.message } } } /** * 通用 Excel 导出工具 * @param {Array} data - 要导出的数据数组 * @param {Object} options - 导出配置 * @param {string} options.sheetName - 工作表名称 * @param {string} options.filename - 文件名 * @param {Array} options.columns - 列配置 [{key, title, width}] * @param {Function} options.formatter - 数据格式化函数 * @returns {boolean} 是否导出成功 */ export function exportToExcel(data, options = {}) { const { sheetName = '数据', filename = `导出数据_${dayjs().format('YYYY-MM-DD_HH-mm-ss')}.xlsx`, columns, formatter } = options if (!data || data.length === 0) { return { success: false, message: '暂无数据可导出' } } try { let exportData = data // 如果有格式化函数,先格式化数据 if (formatter && typeof formatter === 'function') { exportData = data.map(formatter) } else if (columns && Array.isArray(columns)) { // 如果有列配置,按配置转换数据 exportData = data.map((item, index) => { const row = { '序号': index + 1 } columns.forEach(col => { const value = item[col.key] row[col.title || col.key] = value !== undefined ? value : '' }) return row }) } // 创建工作簿和工作表 const wb = XLSX.utils.book_new() const ws = XLSX.utils.json_to_sheet(exportData) // 设置列宽 if (columns && Array.isArray(columns)) { ws['!cols'] = columns.map(col => ({ wch: col.width || 15 })) } XLSX.utils.book_append_sheet(wb, ws, sheetName) // 导出文件 XLSX.writeFile(wb, filename) return { success: true, message: `已导出 ${exportData.length} 条数据`, count: exportData.length } } catch (error) { console.error('导出Excel失败:', error) return { success: false, message: '导出失败,请稍后重试', error: error.message } } }