diff --git a/electron/services/exportService.ts b/electron/services/exportService.ts index ca6c395..88b79f3 100644 --- a/electron/services/exportService.ts +++ b/electron/services/exportService.ts @@ -305,6 +305,121 @@ class ExportService { } } + private formatPlainExportContent( + content: string, + localType: number, + options: { exportVoiceAsText?: boolean }, + voiceTranscript?: string + ): string { + const safeContent = content || '' + + if (localType === 3) return '[图片]' + if (localType === 1) return this.stripSenderPrefix(safeContent) + if (localType === 34) { + if (options.exportVoiceAsText) { + return voiceTranscript || '[语音消息 - 转文字失败]' + } + return '[其他消息]' + } + if (localType === 42) { + const normalized = this.normalizeAppMessageContent(safeContent) + const nickname = + this.extractXmlValue(normalized, 'nickname') || + this.extractXmlValue(normalized, 'displayname') || + this.extractXmlValue(normalized, 'name') + return nickname ? `[名片]${nickname}` : '[名片]' + } + if (localType === 43) { + const normalized = this.normalizeAppMessageContent(safeContent) + const lengthValue = + this.extractXmlValue(normalized, 'playlength') || + this.extractXmlValue(normalized, 'playLength') || + this.extractXmlValue(normalized, 'length') || + this.extractXmlValue(normalized, 'duration') + const seconds = lengthValue ? this.parseDurationSeconds(lengthValue) : null + return seconds ? `[视频]${seconds}s` : '[视频]' + } + if (localType === 48) { + const normalized = this.normalizeAppMessageContent(safeContent) + const location = + this.extractXmlValue(normalized, 'label') || + this.extractXmlValue(normalized, 'poiname') || + this.extractXmlValue(normalized, 'poiName') || + this.extractXmlValue(normalized, 'name') + return location ? `[定位]${location}` : '[定位]' + } + if (localType === 50) { + return this.parseVoipMessage(safeContent) + } + if (localType === 10000 || localType === 266287972401) { + return this.cleanSystemMessage(safeContent) + } + + const normalized = this.normalizeAppMessageContent(safeContent) + const isAppMessage = normalized.includes('') + if (localType === 49 || isAppMessage) { + const typeMatch = /(\d+)<\/type>/i.exec(normalized) + const subType = typeMatch ? parseInt(typeMatch[1], 10) : 0 + const title = this.extractXmlValue(normalized, 'title') || this.extractXmlValue(normalized, 'appname') + if (subType === 3 || normalized.includes('= 1000) return Math.round(numeric / 1000) + return Math.round(numeric) + } + + private extractAmountFromText(text: string): string | null { + if (!text) return null + const match = /([¥¥]\s*\d+(?:\.\d+)?|\d+(?:\.\d+)?)/.exec(text) + return match ? match[1].replace(/\s+/g, '') : null + } + private stripSenderPrefix(content: string): string { return content.replace(/^[\s]*([a-zA-Z0-9_-]+):(?!\/\/)/, '') } @@ -555,31 +670,15 @@ class ExportService { private formatHtmlMessageText(content: string, localType: number): string { if (!content) return '' - const normalized = this.normalizeAppMessageContent(content) - const isAppMessage = normalized.includes('') - - if (localType === 49 || isAppMessage) { - const typeMatch = /(\d+)<\/type>/i.exec(normalized) - const subType = typeMatch ? parseInt(typeMatch[1], 10) : 0 - const title = this.extractXmlValue(normalized, 'title') || this.extractXmlValue(normalized, 'appname') - if (subType === 6) { - const fileName = this.extractXmlValue(normalized, 'filename') || title || '文件' - return `[文件] ${fileName}`.trim() - } - if (subType === 33 || subType === 36) { - const appName = this.extractXmlValue(normalized, 'appname') - const miniTitle = title || appName || '小程序' - return `[小程序] ${miniTitle}`.trim() - } - return title || '[链接]' + if (localType === 1) { + return this.stripSenderPrefix(content) } - if (localType === 42) { - const nickname = this.extractXmlValue(normalized, 'nickname') - return nickname ? `[名片] ${nickname}` : '[名片]' + if (localType === 34) { + return this.parseMessageContent(content, localType) || '' } - return this.parseMessageContent(content, localType) || '' + return this.formatPlainExportContent(content, localType, { exportVoiceAsText: false }) } /** @@ -1967,10 +2066,6 @@ class ExportService { for (let i = 0; i < sortedMessages.length; i++) { const msg = sortedMessages[i] - // 从缓存获取媒体信息 - const mediaKey = `${msg.localType}_${msg.localId}` - const mediaItem = mediaCache.get(mediaKey) || null - // 确定发送者信息 let senderRole: string let senderWxid: string @@ -2018,16 +2113,12 @@ class ExportService { const row = worksheet.getRow(currentRow) row.height = 24 - // 确定内容:优先使用预处理的缓存 - let contentValue: string - if (mediaItem) { - contentValue = mediaItem.relativePath - } else if (msg.localType === 34 && options.exportVoiceAsText) { - // 使用预处理的语音转文字结果 - contentValue = voiceTranscriptMap.get(msg.localId) || '[语音消息 - 转文字失败]' - } else { - contentValue = this.parseMessageContent(msg.content, msg.localType) || '' - } + const contentValue = this.formatPlainExportContent( + msg.content, + msg.localType, + options, + voiceTranscriptMap.get(msg.localId) + ) // 调试日志 if (msg.localType === 3 || msg.localType === 47) { @@ -2190,24 +2281,16 @@ class ExportService { phase: 'exporting' }) - const columnOrder = this.normalizeTxtColumns(options.txtColumns) - const columnLabelMap = new Map(TXT_COLUMN_DEFINITIONS.map((col) => [col.id, col.label])) const lines: string[] = [] - lines.push(columnOrder.map((id) => columnLabelMap.get(id) || id).join('\t')) for (let i = 0; i < sortedMessages.length; i++) { const msg = sortedMessages[i] - const mediaKey = `${msg.localType}_${msg.localId}` - const mediaItem = mediaCache.get(mediaKey) || null - - let contentValue: string - if (mediaItem) { - contentValue = mediaItem.relativePath - } else if (msg.localType === 34 && options.exportVoiceAsText) { - contentValue = voiceTranscriptMap.get(msg.localId) || '[语音消息 - 转文字失败]' - } else { - contentValue = this.parseMessageContent(msg.content, msg.localType) || '' - } + const contentValue = this.formatPlainExportContent( + msg.content, + msg.localType, + options, + voiceTranscriptMap.get(msg.localId) + ) let senderRole: string let senderWxid: string @@ -2242,21 +2325,9 @@ class ExportService { } } - const values: Record = { - index: String(i + 1), - time: this.formatTimestamp(msg.createTime), - senderRole, - senderNickname, - senderWxid, - senderRemark, - messageType: this.getMessageTypeName(msg.localType), - content: contentValue - } - - const line = columnOrder - .map((id) => this.sanitizeTxtValue(values[id] ?? '')) - .join('\t') - lines.push(line) + lines.push(`${this.formatTimestamp(msg.createTime)} '${senderRole}'`) + lines.push(contentValue) + lines.push('') if ((i + 1) % 200 === 0) { const progress = 60 + Math.floor((i + 1) / sortedMessages.length * 30) @@ -2417,7 +2488,7 @@ class ExportService { if (msg.localType === 34 && useVoiceTranscript) { textContent = voiceTranscriptMap.get(msg.localId) || '[语音消息 - 转文字失败]' } - if (mediaItem && (msg.localType === 3 || msg.localType === 43 || msg.localType === 47)) { + if (mediaItem && (msg.localType === 3 || msg.localType === 47)) { textContent = '' } diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 93c7484..18b2979 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -61,7 +61,6 @@ function SettingsPage() { const [exportDefaultMedia, setExportDefaultMedia] = useState(false) const [exportDefaultVoiceAsText, setExportDefaultVoiceAsText] = useState(true) const [exportDefaultExcelCompactColumns, setExportDefaultExcelCompactColumns] = useState(true) - const [exportDefaultTxtColumns, setExportDefaultTxtColumns] = useState(['index', 'time', 'senderRole', 'messageType', 'content']) const [isLoading, setIsLoadingState] = useState(false) const [isTesting, setIsTesting] = useState(false) @@ -142,8 +141,6 @@ function SettingsPage() { const savedExportDefaultMedia = await configService.getExportDefaultMedia() const savedExportDefaultVoiceAsText = await configService.getExportDefaultVoiceAsText() const savedExportDefaultExcelCompactColumns = await configService.getExportDefaultExcelCompactColumns() - const savedExportDefaultTxtColumns = await configService.getExportDefaultTxtColumns() - const defaultTxtColumns = ['index', 'time', 'senderRole', 'messageType', 'content'] if (savedKey) setDecryptKey(savedKey) if (savedPath) setDbPath(savedPath) @@ -161,11 +158,6 @@ function SettingsPage() { setExportDefaultMedia(savedExportDefaultMedia ?? false) setExportDefaultVoiceAsText(savedExportDefaultVoiceAsText ?? true) setExportDefaultExcelCompactColumns(savedExportDefaultExcelCompactColumns ?? true) - setExportDefaultTxtColumns( - savedExportDefaultTxtColumns && savedExportDefaultTxtColumns.length > 0 - ? savedExportDefaultTxtColumns - : defaultTxtColumns - ) // 如果语言列表为空,保存默认值 if (!savedTranscribeLanguages || savedTranscribeLanguages.length === 0) { @@ -174,9 +166,6 @@ function SettingsPage() { await configService.setTranscribeLanguages(defaultLanguages) } - if (!savedExportDefaultTxtColumns || savedExportDefaultTxtColumns.length === 0) { - await configService.setExportDefaultTxtColumns(defaultTxtColumns) - } if (savedWhisperModelDir) setWhisperModelDir(savedWhisperModelDir) } catch (e) { @@ -911,16 +900,6 @@ function SettingsPage() { { value: 'compact', label: '精简列', desc: '序号、时间、发送者身份、消息类型、内容' }, { value: 'full', label: '完整列', desc: '含发送者昵称/微信ID/备注' } ] - const exportTxtColumnOptions = [ - { value: 'index', label: '序号' }, - { value: 'time', label: '时间' }, - { value: 'senderRole', label: '发送者身份' }, - { value: 'messageType', label: '消息类型' }, - { value: 'content', label: '内容' }, - { value: 'senderNickname', label: '发送者昵称' }, - { value: 'senderWxid', label: '发送者微信ID' }, - { value: 'senderRemark', label: '发送者备注' } - ] const getOptionLabel = (options: { value: string; label: string }[], value: string) => { return options.find((option) => option.value === value)?.label ?? value @@ -1097,40 +1076,6 @@ function SettingsPage() { -
- - 默认与 Excel 精简列一致,可多选调整输出字段 -
- {exportTxtColumnOptions.map((column) => { - const checked = exportDefaultTxtColumns.includes(column.value) - return ( - - ) - })} -
-
) }