332 lines
11 KiB
JavaScript
332 lines
11 KiB
JavaScript
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
|
||
}
|
||
}
|
||
}
|
||
|