Merge pull request #662 from chrocy/fix-export-excel-columns

feat: 新增 Excel 导出完整列开关
This commit is contained in:
xuncha
2026-04-07 19:33:14 +08:00
committed by GitHub
2 changed files with 121 additions and 51 deletions

View File

@@ -6491,9 +6491,12 @@ class ExportService {
currentRow++ currentRow++
// 表头行 // 表头行
const includeGroupNicknameColumn = !useCompactColumns && isGroup
const headers = useCompactColumns const headers = useCompactColumns
? ['序号', '时间', '发送者身份', '消息类型', '内容'] ? ['序号', '时间', '发送者身份', '消息类型', '内容']
: ['序号', '时间', '发送者昵称', '发送者微信ID', '发送者备注', '群昵称', '发送者身份', '消息类型', '内容'] : includeGroupNicknameColumn
? ['序号', '时间', '发送者昵称', '发送者微信ID', '发送者备注', '群昵称', '发送者身份', '消息类型', '内容']
: ['序号', '时间', '发送者昵称', '发送者微信ID', '发送者备注', '发送者身份', '消息类型', '内容']
const headerRow = worksheet.getRow(currentRow) const headerRow = worksheet.getRow(currentRow)
headerRow.height = 22 headerRow.height = 22
@@ -6521,10 +6524,16 @@ class ExportService {
worksheet.getColumn(3).width = 18 // 发送者昵称 worksheet.getColumn(3).width = 18 // 发送者昵称
worksheet.getColumn(4).width = 25 // 发送者微信ID worksheet.getColumn(4).width = 25 // 发送者微信ID
worksheet.getColumn(5).width = 18 // 发送者备注 worksheet.getColumn(5).width = 18 // 发送者备注
worksheet.getColumn(6).width = 18 // 群昵称 if (includeGroupNicknameColumn) {
worksheet.getColumn(7).width = 15 // 发送者身份 worksheet.getColumn(6).width = 18 // 群昵称
worksheet.getColumn(8).width = 12 // 消息类型 worksheet.getColumn(7).width = 15 // 发送者身份
worksheet.getColumn(9).width = 50 // 内容 worksheet.getColumn(8).width = 12 // 消息类型
worksheet.getColumn(9).width = 50 // 内容
} else {
worksheet.getColumn(6).width = 15 // 发送者身份
worksheet.getColumn(7).width = 12 // 消息类型
worksheet.getColumn(8).width = 50 // 内容
}
} }
// 预加载群昵称 (仅群聊且完整列模式) // 预加载群昵称 (仅群聊且完整列模式)
@@ -6805,7 +6814,7 @@ class ExportService {
enrichedContentValue = this.buildQuotedReplyText(quotedReplyDisplay) enrichedContentValue = this.buildQuotedReplyText(quotedReplyDisplay)
} }
const contentCellIndex = useCompactColumns ? 5 : 9 const contentCellIndex = useCompactColumns ? 5 : (includeGroupNicknameColumn ? 9 : 8)
const contentCell = worksheet.getCell(currentRow, contentCellIndex) const contentCell = worksheet.getCell(currentRow, contentCellIndex)
worksheet.getCell(currentRow, 1).value = i + 1 worksheet.getCell(currentRow, 1).value = i + 1
@@ -6813,13 +6822,19 @@ class ExportService {
if (useCompactColumns) { if (useCompactColumns) {
worksheet.getCell(currentRow, 3).value = senderRole worksheet.getCell(currentRow, 3).value = senderRole
worksheet.getCell(currentRow, 4).value = this.getMessageTypeName(msg.localType) worksheet.getCell(currentRow, 4).value = this.getMessageTypeName(msg.localType)
} else { } else if (includeGroupNicknameColumn) {
worksheet.getCell(currentRow, 3).value = senderNickname worksheet.getCell(currentRow, 3).value = senderNickname
worksheet.getCell(currentRow, 4).value = senderWxid worksheet.getCell(currentRow, 4).value = senderWxid
worksheet.getCell(currentRow, 5).value = senderRemark worksheet.getCell(currentRow, 5).value = senderRemark
worksheet.getCell(currentRow, 6).value = senderGroupNickname worksheet.getCell(currentRow, 6).value = senderGroupNickname
worksheet.getCell(currentRow, 7).value = senderRole worksheet.getCell(currentRow, 7).value = senderRole
worksheet.getCell(currentRow, 8).value = this.getMessageTypeName(msg.localType) worksheet.getCell(currentRow, 8).value = this.getMessageTypeName(msg.localType)
} else {
worksheet.getCell(currentRow, 3).value = senderNickname
worksheet.getCell(currentRow, 4).value = senderWxid
worksheet.getCell(currentRow, 5).value = senderRemark
worksheet.getCell(currentRow, 6).value = senderRole
worksheet.getCell(currentRow, 7).value = this.getMessageTypeName(msg.localType)
} }
contentCell.value = enrichedContentValue contentCell.value = enrichedContentValue
if (!quotedReplyDisplay) { if (!quotedReplyDisplay) {
@@ -6929,6 +6944,7 @@ class ExportService {
}) })
const worksheet = workbook.addWorksheet('聊天记录') const worksheet = workbook.addWorksheet('聊天记录')
const useCompactColumns = options.excelCompactColumns === true const useCompactColumns = options.excelCompactColumns === true
const includeGroupNicknameColumn = !useCompactColumns && isGroup
const senderProfileCache = new Map<string, ExportDisplayProfile>() const senderProfileCache = new Map<string, ExportDisplayProfile>()
worksheet.columns = useCompactColumns worksheet.columns = useCompactColumns
@@ -6939,17 +6955,28 @@ class ExportService {
{ width: 12 }, { width: 12 },
{ width: 50 } { width: 50 }
] ]
: [ : includeGroupNicknameColumn
{ width: 8 }, ? [
{ width: 20 }, { width: 8 },
{ width: 18 }, { width: 20 },
{ width: 25 }, { width: 18 },
{ width: 18 }, { width: 25 },
{ width: 18 }, { width: 18 },
{ width: 15 }, { width: 18 },
{ width: 12 }, { width: 15 },
{ width: 50 } { width: 12 },
] { width: 50 }
]
: [
{ width: 8 },
{ width: 20 },
{ width: 18 },
{ width: 25 },
{ width: 18 },
{ width: 15 },
{ width: 12 },
{ width: 50 }
]
const appendRow = (values: any[]) => { const appendRow = (values: any[]) => {
const row = worksheet.addRow(values) const row = worksheet.addRow(values)
@@ -6962,7 +6989,9 @@ class ExportService {
appendRow([]) appendRow([])
appendRow(useCompactColumns appendRow(useCompactColumns
? ['序号', '时间', '发送者身份', '消息类型', '内容'] ? ['序号', '时间', '发送者身份', '消息类型', '内容']
: ['序号', '时间', '发送者昵称', '发送者微信ID', '发送者备注', '群昵称', '发送者身份', '消息类型', '内容']) : includeGroupNicknameColumn
? ['序号', '时间', '发送者昵称', '发送者微信ID', '发送者备注', '群昵称', '发送者身份', '消息类型', '内容']
: ['序号', '时间', '发送者昵称', '发送者微信ID', '发送者备注', '发送者身份', '消息类型', '内容'])
for (let i = 0; i < totalMessages; i++) { for (let i = 0; i < totalMessages; i++) {
if ((i & 0x7f) === 0) this.throwIfStopRequested(control) if ((i & 0x7f) === 0) this.throwIfStopRequested(control)
@@ -7077,19 +7106,34 @@ class ExportService {
this.getMessageTypeName(msg.localType), this.getMessageTypeName(msg.localType),
enrichedContentValue enrichedContentValue
] ]
: [ : includeGroupNicknameColumn
i + 1, ? [
this.formatTimestamp(msg.createTime), i + 1,
senderNickname, this.formatTimestamp(msg.createTime),
senderWxid, senderNickname,
senderRemark, senderWxid,
senderGroupNickname, senderRemark,
senderRole, senderGroupNickname,
this.getMessageTypeName(msg.localType), senderRole,
enrichedContentValue this.getMessageTypeName(msg.localType),
]) enrichedContentValue
]
: [
i + 1,
this.formatTimestamp(msg.createTime),
senderNickname,
senderWxid,
senderRemark,
senderRole,
this.getMessageTypeName(msg.localType),
enrichedContentValue
])
if (!quotedReplyDisplay) { if (!quotedReplyDisplay) {
this.applyExcelLinkCardCell(row.getCell(useCompactColumns ? 5 : 9), msg.content, msg.localType) this.applyExcelLinkCardCell(
row.getCell(useCompactColumns ? 5 : (includeGroupNicknameColumn ? 9 : 8)),
msg.content,
msg.localType
)
} }
row.commit() row.commit()

View File

@@ -8268,6 +8268,8 @@ function ExportPage() {
</div> </div>
)} )}
<div className="dialog-section"> <div className="dialog-section">
<div className="section-header-action"> <div className="section-header-action">
<h4></h4> <h4></h4>
@@ -8407,27 +8409,51 @@ function ExportPage() {
</div> </div>
)} )}
{shouldShowDisplayNameSection && ( {(shouldShowDisplayNameSection || options.format === 'excel') && (
<div className="dialog-section"> <div className="dialog-section">
<h4></h4> {shouldShowDisplayNameSection && (
<div className="display-name-options" role="radiogroup" aria-label="发送者名称显示"> <>
{displayNameOptions.map(option => { <h4></h4>
const isActive = options.displayNamePreference === option.value <div className="display-name-options" role="radiogroup" aria-label="发送者名称显示">
return ( {displayNameOptions.map(option => {
<button const isActive = options.displayNamePreference === option.value
key={option.value} return (
type="button" <button
role="radio" key={option.value}
aria-checked={isActive} type="button"
className={`display-name-item ${isActive ? 'active' : ''}`} role="radio"
onClick={() => setOptions(prev => ({ ...prev, displayNamePreference: option.value }))} aria-checked={isActive}
> className={`display-name-item ${isActive ? 'active' : ''}`}
<span>{option.label}</span> onClick={() => setOptions(prev => ({ ...prev, displayNamePreference: option.value }))}
<small>{option.desc}</small> >
</button> <span>{option.label}</span>
) <small>{option.desc}</small>
})} </button>
</div> )
})}
</div>
</>
)}
{options.format === 'excel' && (
<div className={`dialog-switch-row ${shouldShowDisplayNameSection ? 'nested-row' : ''}`} style={shouldShowDisplayNameSection ? { marginTop: '12px', paddingTop: '12px', borderTop: '1px solid var(--border-light)' } : {}}>
<div className="dialog-switch-copy">
<h4></h4>
<div className="format-note">
Excel ID
</div>
</div>
<button
type="button"
className={`dialog-switch ${!options.excelCompactColumns ? 'on' : ''}`}
aria-pressed={!options.excelCompactColumns}
aria-label="切换导出完整列"
onClick={() => setOptions(prev => ({ ...prev, excelCompactColumns: !prev.excelCompactColumns }))}
>
<span className="dialog-switch-thumb" />
</button>
</div>
)}
</div> </div>
)} )}
</div> </div>