diff --git a/electron/services/exportService.ts b/electron/services/exportService.ts index a3bfa4e..0a3f1cb 100644 --- a/electron/services/exportService.ts +++ b/electron/services/exportService.ts @@ -71,6 +71,7 @@ export interface ExportOptions { exportVoices?: boolean exportEmojis?: boolean exportVoiceAsText?: boolean + excelCompactColumns?: boolean } interface MediaExportItem { @@ -1250,13 +1251,18 @@ class ExportService { const sourceMatch = /[\s\S]*?<\/msgsource>/i.exec(msg.content || '') const source = sourceMatch ? sourceMatch[0] : '' + let content = this.parseMessageContent(msg.content, msg.localType) + if (msg.localType === 34 && options.exportVoiceAsText) { + content = await this.transcribeVoice(sessionId, String(msg.localId)) + } + allMessages.push({ localId: allMessages.length + 1, createTime: msg.createTime, formattedTime: this.formatTimestamp(msg.createTime), type: this.getMessageTypeName(msg.localType), localType: msg.localType, - content: this.parseMessageContent(msg.content, msg.localType), + content, isSend: msg.isSend ? 1 : 0, senderUsername: msg.senderUsername, senderDisplayName: senderInfo.displayName, @@ -1379,8 +1385,9 @@ class ExportService { let currentRow = 1 + const useCompactColumns = options.excelCompactColumns === true + // 第一行:会话信息标题 - worksheet.mergeCells(currentRow, 1, currentRow, 8) const titleCell = worksheet.getCell(currentRow, 1) titleCell.value = '会话信息' titleCell.font = { name: 'Calibri', bold: true, size: 11 } @@ -1436,7 +1443,9 @@ class ExportService { currentRow++ // 表头行 - const headers = ['序号', '时间', '发送者昵称', '发送者微信ID', '发送者备注', '发送者身份', '消息类型', '内容'] + const headers = useCompactColumns + ? ['序号', '时间', '发送者身份', '消息类型', '内容'] + : ['序号', '时间', '发送者昵称', '发送者微信ID', '发送者备注', '发送者身份', '消息类型', '内容'] const headerRow = worksheet.getRow(currentRow) headerRow.height = 22 @@ -1456,12 +1465,18 @@ class ExportService { // 设置列宽 worksheet.getColumn(1).width = 8 // 序号 worksheet.getColumn(2).width = 20 // 时间 - worksheet.getColumn(3).width = 18 // 发送者昵称 - worksheet.getColumn(4).width = 25 // 发送者微信ID - worksheet.getColumn(5).width = 18 // 发送者备注 - worksheet.getColumn(6).width = 15 // 发送者身份 - worksheet.getColumn(7).width = 12 // 消息类型 - worksheet.getColumn(8).width = 50 // 内容 + if (useCompactColumns) { + worksheet.getColumn(3).width = 18 // 发送者身份 + worksheet.getColumn(4).width = 12 // 消息类型 + worksheet.getColumn(5).width = 50 // 内容 + } else { + worksheet.getColumn(3).width = 18 // 发送者昵称 + worksheet.getColumn(4).width = 25 // 发送者微信ID + worksheet.getColumn(5).width = 18 // 发送者备注 + worksheet.getColumn(6).width = 15 // 发送者身份 + worksheet.getColumn(7).width = 12 // 消息类型 + worksheet.getColumn(8).width = 50 // 内容 + } // 填充数据 const sortedMessages = collected.rows.sort((a, b) => a.createTime - b.createTime) @@ -1541,9 +1556,12 @@ class ExportService { row.height = 24 // 确定内容:如果有媒体文件导出成功则显示相对路径,否则显示解析后的内容 - const contentValue = mediaItem + let contentValue = mediaItem ? mediaItem.relativePath : (this.parseMessageContent(msg.content, msg.localType) || '') + if (!mediaItem && msg.localType === 34 && options.exportVoiceAsText) { + contentValue = await this.transcribeVoice(sessionId, String(msg.localId)) + } // 调试日志 if (msg.localType === 3 || msg.localType === 47) { @@ -1551,15 +1569,22 @@ class ExportService { worksheet.getCell(currentRow, 1).value = i + 1 worksheet.getCell(currentRow, 2).value = this.formatTimestamp(msg.createTime) - 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) - worksheet.getCell(currentRow, 8).value = contentValue + if (useCompactColumns) { + worksheet.getCell(currentRow, 3).value = senderRole + worksheet.getCell(currentRow, 4).value = this.getMessageTypeName(msg.localType) + worksheet.getCell(currentRow, 5).value = contentValue + } 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) + worksheet.getCell(currentRow, 8).value = contentValue + } // 设置每个单元格的样式 - for (let col = 1; col <= 8; col++) { + const maxColumns = useCompactColumns ? 5 : 8 + for (let col = 1; col <= maxColumns; col++) { const cell = worksheet.getCell(currentRow, col) cell.font = { name: 'Calibri', size: 11 } cell.alignment = { vertical: 'middle', wrapText: false } @@ -1689,4 +1714,3 @@ class ExportService { } export const exportService = new ExportService() - diff --git a/electron/services/wcdbCore.ts b/electron/services/wcdbCore.ts index 7f8f32c..6144726 100644 --- a/electron/services/wcdbCore.ts +++ b/electron/services/wcdbCore.ts @@ -382,6 +382,12 @@ export class WcdbCore { return { success: true, sessionCount: 0 } } + // 记录当前活动连接,用于在测试结束后恢复(避免影响聊天页等正在使用的连接) + const hadActiveConnection = this.handle !== null + const prevPath = this.currentPath + const prevKey = this.currentKey + const prevWxid = this.currentWxid + if (!this.initialized) { const initOk = await this.initialize() if (!initOk) { @@ -424,8 +430,8 @@ export class WcdbCore { return { success: false, error: '无效的数据库句柄' } } - // 测试成功,使用 shutdown 清理所有资源(包括测试句柄) - // 这会中断当前活动连接,但 testConnection 本应该是独立测试 + // 测试成功:使用 shutdown 清理资源(包括测试句柄) + // 注意:shutdown 会断开当前活动连接,因此需要在测试后尝试恢复之前的连接 try { this.wcdbShutdown() this.handle = null @@ -437,6 +443,15 @@ export class WcdbCore { console.error('关闭测试数据库时出错:', closeErr) } + // 恢复测试前的连接(如果之前有活动连接) + if (hadActiveConnection && prevPath && prevKey && prevWxid) { + try { + await this.open(prevPath, prevKey, prevWxid) + } catch { + // 恢复失败则保持断开,由调用方处理 + } + } + return { success: true, sessionCount: 0 } } catch (e) { console.error('测试连接异常:', e) diff --git a/src/pages/ExportPage.tsx b/src/pages/ExportPage.tsx index f60a588..19d8605 100644 --- a/src/pages/ExportPage.tsx +++ b/src/pages/ExportPage.tsx @@ -21,6 +21,7 @@ interface ExportOptions { exportVoices: boolean exportEmojis: boolean exportVoiceAsText: boolean + excelCompactColumns: boolean } interface ExportResult { @@ -45,20 +46,40 @@ function ExportPage() { const [selectingStart, setSelectingStart] = useState(true) const [options, setOptions] = useState({ - format: 'chatlab', + format: 'excel', dateRange: { - start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), + start: new Date(new Date().setHours(0, 0, 0, 0)), end: new Date() }, - useAllTime: true, + useAllTime: false, exportAvatars: true, exportMedia: false, exportImages: true, exportVoices: true, exportEmojis: true, - exportVoiceAsText: false + exportVoiceAsText: true, + excelCompactColumns: true }) + const buildDateRangeFromPreset = (preset: string) => { + const now = new Date() + if (preset === 'all') { + return { useAllTime: true, dateRange: { start: now, end: now } } + } + let rangeMs = 0 + if (preset === '7d') rangeMs = 7 * 24 * 60 * 60 * 1000 + if (preset === '30d') rangeMs = 30 * 24 * 60 * 60 * 1000 + if (preset === '90d') rangeMs = 90 * 24 * 60 * 60 * 1000 + if (preset === 'today' || rangeMs === 0) { + const start = new Date(now) + start.setHours(0, 0, 0, 0) + return { useAllTime: false, dateRange: { start, end: now } } + } + const start = new Date(now.getTime() - rangeMs) + start.setHours(0, 0, 0, 0) + return { useAllTime: false, dateRange: { start, end: now } } + } + const loadSessions = useCallback(async () => { setIsLoading(true) try { @@ -94,10 +115,44 @@ function ExportPage() { } }, []) + const loadExportDefaults = useCallback(async () => { + try { + const [ + savedFormat, + savedRange, + savedMedia, + savedVoiceAsText, + savedExcelCompactColumns + ] = await Promise.all([ + configService.getExportDefaultFormat(), + configService.getExportDefaultDateRange(), + configService.getExportDefaultMedia(), + configService.getExportDefaultVoiceAsText(), + configService.getExportDefaultExcelCompactColumns() + ]) + + const preset = savedRange || 'today' + const rangeDefaults = buildDateRangeFromPreset(preset) + + setOptions((prev) => ({ + ...prev, + format: (savedFormat as ExportOptions['format']) || 'excel', + useAllTime: rangeDefaults.useAllTime, + dateRange: rangeDefaults.dateRange, + exportMedia: savedMedia ?? false, + exportVoiceAsText: savedVoiceAsText ?? true, + excelCompactColumns: savedExcelCompactColumns ?? true + })) + } catch (e) { + console.error('加载导出默认设置失败:', e) + } + }, []) + useEffect(() => { loadSessions() loadExportPath() - }, [loadSessions, loadExportPath]) + loadExportDefaults() + }, [loadSessions, loadExportPath, loadExportDefaults]) useEffect(() => { if (!searchKeyword.trim()) { @@ -161,6 +216,7 @@ function ExportPage() { exportVoices: options.exportMedia && options.exportVoices, exportEmojis: options.exportMedia && options.exportEmojis, exportVoiceAsText: options.exportVoiceAsText, // 独立于 exportMedia + excelCompactColumns: options.excelCompactColumns, dateRange: options.useAllTime ? null : options.dateRange ? { start: Math.floor(options.dateRange.start.getTime() / 1000), // 将结束日期设置为当天的 23:59:59,以包含当天的所有消息 diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index bc259d6..6fbe196 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from 'react' +import { useState, useEffect, useRef } from 'react' import { useAppStore } from '../stores/appStore' import { useThemeStore, themes } from '../stores/themeStore' import { useAnalyticsStore } from '../stores/analyticsStore' @@ -11,12 +11,13 @@ import { } from 'lucide-react' import './SettingsPage.scss' -type SettingsTab = 'appearance' | 'database' | 'whisper' | 'cache' | 'about' +type SettingsTab = 'appearance' | 'database' | 'whisper' | 'export' | 'cache' | 'about' const tabs: { id: SettingsTab; label: string; icon: React.ElementType }[] = [ { id: 'appearance', label: '外观', icon: Palette }, { id: 'database', label: '数据库连接', icon: Database }, { id: 'whisper', label: '语音识别模型', icon: Mic }, + { id: 'export', label: '导出', icon: Download }, { id: 'cache', label: '缓存', icon: HardDrive }, { id: 'about', label: '关于', icon: Info } ] @@ -49,6 +50,11 @@ function SettingsPage() { const [whisperModelStatus, setWhisperModelStatus] = useState<{ exists: boolean; modelPath?: string; tokensPath?: string } | null>(null) const [autoTranscribeVoice, setAutoTranscribeVoice] = useState(false) const [transcribeLanguages, setTranscribeLanguages] = useState(['zh']) + const [exportDefaultFormat, setExportDefaultFormat] = useState('excel') + const [exportDefaultDateRange, setExportDefaultDateRange] = useState('today') + const [exportDefaultMedia, setExportDefaultMedia] = useState(false) + const [exportDefaultVoiceAsText, setExportDefaultVoiceAsText] = useState(true) + const [exportDefaultExcelCompactColumns, setExportDefaultExcelCompactColumns] = useState(true) const [isLoading, setIsLoadingState] = useState(false) const [isTesting, setIsTesting] = useState(false) @@ -114,6 +120,11 @@ function SettingsPage() { const savedWhisperModelDir = await configService.getWhisperModelDir() const savedAutoTranscribe = await configService.getAutoTranscribeVoice() const savedTranscribeLanguages = await configService.getTranscribeLanguages() + const savedExportDefaultFormat = await configService.getExportDefaultFormat() + const savedExportDefaultDateRange = await configService.getExportDefaultDateRange() + const savedExportDefaultMedia = await configService.getExportDefaultMedia() + const savedExportDefaultVoiceAsText = await configService.getExportDefaultVoiceAsText() + const savedExportDefaultExcelCompactColumns = await configService.getExportDefaultExcelCompactColumns() if (savedKey) setDecryptKey(savedKey) if (savedPath) setDbPath(savedPath) @@ -126,6 +137,11 @@ function SettingsPage() { setLogEnabled(savedLogEnabled) setAutoTranscribeVoice(savedAutoTranscribe) setTranscribeLanguages(savedTranscribeLanguages) + setExportDefaultFormat(savedExportDefaultFormat || 'excel') + setExportDefaultDateRange(savedExportDefaultDateRange || 'today') + setExportDefaultMedia(savedExportDefaultMedia ?? false) + setExportDefaultVoiceAsText(savedExportDefaultVoiceAsText ?? true) + setExportDefaultExcelCompactColumns(savedExportDefaultExcelCompactColumns ?? true) // 如果语言列表为空,保存默认值 if (!savedTranscribeLanguages || savedTranscribeLanguages.length === 0) { @@ -468,15 +484,8 @@ function SettingsPage() { await configService.setTranscribeLanguages(transcribeLanguages) await configService.setOnboardingDone(true) - showMessage('配置保存成功,正在测试连接...', true) - const result = await window.electronAPI.wcdb.testConnection(dbPath, decryptKey, wxid) - - if (result.success) { - setDbConnected(true, dbPath) - showMessage('配置保存成功!数据库连接正常', true) - } else { - showMessage(result.error || '数据库连接失败,请检查配置', false) - } + // 保存按钮只负责持久化配置,不做连接测试/重连,避免影响聊天页的活动连接 + showMessage('配置保存成功', true) } catch (e) { showMessage(`保存配置失败: ${e}`, false) } finally { @@ -853,6 +862,115 @@ function SettingsPage() { ) + + const renderExportTab = () => ( +
+
+ + 导出页面默认选中的格式 + +
+ +
+ + 控制导出页面的默认时间选择 + +
+ +
+ + 控制图片/语音/表情的默认导出开关 +
+ {exportDefaultMedia ? '已开启' : '已关闭'} + +
+
+ +
+ + 导出时默认将语音转写为文字 +
+ {exportDefaultVoiceAsText ? '已开启' : '已关闭'} + +
+
+ +
+ + 控制 Excel 导出的列字段 + +
+
+ ) const renderCacheTab = () => (

管理应用缓存数据

@@ -992,6 +1110,7 @@ function SettingsPage() { {activeTab === 'appearance' && renderAppearanceTab()} {activeTab === 'database' && renderDatabaseTab()} {activeTab === 'whisper' && renderWhisperTab()} + {activeTab === 'export' && renderExportTab()} {activeTab === 'cache' && renderCacheTab()} {activeTab === 'about' && renderAboutTab()}
@@ -1001,4 +1120,3 @@ function SettingsPage() { export default SettingsPage - diff --git a/src/services/config.ts b/src/services/config.ts index 4704d36..9bc8a5e 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -22,7 +22,12 @@ export const CONFIG_KEYS = { WHISPER_MODEL_DIR: 'whisperModelDir', WHISPER_DOWNLOAD_SOURCE: 'whisperDownloadSource', AUTO_TRANSCRIBE_VOICE: 'autoTranscribeVoice', - TRANSCRIBE_LANGUAGES: 'transcribeLanguages' + TRANSCRIBE_LANGUAGES: 'transcribeLanguages', + EXPORT_DEFAULT_FORMAT: 'exportDefaultFormat', + EXPORT_DEFAULT_DATE_RANGE: 'exportDefaultDateRange', + EXPORT_DEFAULT_MEDIA: 'exportDefaultMedia', + EXPORT_DEFAULT_VOICE_AS_TEXT: 'exportDefaultVoiceAsText', + EXPORT_DEFAULT_EXCEL_COMPACT_COLUMNS: 'exportDefaultExcelCompactColumns' } as const // 获取解密密钥 @@ -243,3 +248,61 @@ export async function getTranscribeLanguages(): Promise { export async function setTranscribeLanguages(languages: string[]): Promise { await config.set(CONFIG_KEYS.TRANSCRIBE_LANGUAGES, languages) } + +// 获取导出默认格式 +export async function getExportDefaultFormat(): Promise { + const value = await config.get(CONFIG_KEYS.EXPORT_DEFAULT_FORMAT) + return (value as string) || null +} + +// 设置导出默认格式 +export async function setExportDefaultFormat(format: string): Promise { + await config.set(CONFIG_KEYS.EXPORT_DEFAULT_FORMAT, format) +} + +// 获取导出默认时间范围 +export async function getExportDefaultDateRange(): Promise { + const value = await config.get(CONFIG_KEYS.EXPORT_DEFAULT_DATE_RANGE) + return (value as string) || null +} + +// 设置导出默认时间范围 +export async function setExportDefaultDateRange(range: string): Promise { + await config.set(CONFIG_KEYS.EXPORT_DEFAULT_DATE_RANGE, range) +} + +// 获取导出默认媒体设置 +export async function getExportDefaultMedia(): Promise { + const value = await config.get(CONFIG_KEYS.EXPORT_DEFAULT_MEDIA) + if (typeof value === 'boolean') return value + return null +} + +// 设置导出默认媒体设置 +export async function setExportDefaultMedia(enabled: boolean): Promise { + await config.set(CONFIG_KEYS.EXPORT_DEFAULT_MEDIA, enabled) +} + +// 获取导出默认语音转文字 +export async function getExportDefaultVoiceAsText(): Promise { + const value = await config.get(CONFIG_KEYS.EXPORT_DEFAULT_VOICE_AS_TEXT) + if (typeof value === 'boolean') return value + return null +} + +// 设置导出默认语音转文字 +export async function setExportDefaultVoiceAsText(enabled: boolean): Promise { + await config.set(CONFIG_KEYS.EXPORT_DEFAULT_VOICE_AS_TEXT, enabled) +} + +// 获取导出默认 Excel 列模式 +export async function getExportDefaultExcelCompactColumns(): Promise { + const value = await config.get(CONFIG_KEYS.EXPORT_DEFAULT_EXCEL_COMPACT_COLUMNS) + if (typeof value === 'boolean') return value + return null +} + +// 设置导出默认 Excel 列模式 +export async function setExportDefaultExcelCompactColumns(enabled: boolean): Promise { + await config.set(CONFIG_KEYS.EXPORT_DEFAULT_EXCEL_COMPACT_COLUMNS, enabled) +} diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index e3528cf..bacefb3 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -327,6 +327,11 @@ export interface ExportOptions { dateRange?: { start: number; end: number } | null exportMedia?: boolean exportAvatars?: boolean + exportImages?: boolean + exportVoices?: boolean + exportEmojis?: boolean + exportVoiceAsText?: boolean + excelCompactColumns?: boolean } export interface WxidInfo {