From 4e9c81a93deaad1c604de321c964c7f9038c8823 Mon Sep 17 00:00:00 2001 From: xuncha <1658671838@qq.com> Date: Sun, 25 Jan 2026 10:45:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AF=BC=E5=87=BA=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=BE=A4=E6=98=B5=E7=A7=B0=20=E5=A4=87?= =?UTF-8?q?=E6=B3=A8=E7=AD=89=E9=80=89=E6=8B=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/services/exportService.ts | 77 ++++++++++++++++++++++-- src/pages/ExportPage.scss | 97 +++++++++++++++++++++++++++++- src/pages/ExportPage.tsx | 76 ++++++++++++++++++++++- src/types/electron.d.ts | 1 + 4 files changed, 241 insertions(+), 10 deletions(-) diff --git a/electron/services/exportService.ts b/electron/services/exportService.ts index b25d257..7e57c05 100644 --- a/electron/services/exportService.ts +++ b/electron/services/exportService.ts @@ -77,6 +77,7 @@ export interface ExportOptions { excelCompactColumns?: boolean txtColumns?: string[] sessionLayout?: 'shared' | 'per-session' + displayNamePreference?: 'group-nickname' | 'remark' | 'nickname' } const TXT_COLUMN_DEFINITIONS: Array<{ id: string; label: string }> = [ @@ -408,6 +409,28 @@ class ExportService { return /^[0-9a-fA-F]+$/.test(s) } + /** + * 根据用户偏好获取显示名称 + */ + private getPreferredDisplayName( + wxid: string, + nickname: string, + remark: string, + groupNickname: string, + preference: 'group-nickname' | 'remark' | 'nickname' = 'remark' + ): string { + switch (preference) { + case 'group-nickname': + return groupNickname || remark || nickname || wxid + case 'remark': + return remark || nickname || wxid + case 'nickname': + return nickname || wxid + default: + return nickname || wxid + } + } + private looksLikeBase64(s: string): boolean { if (s.length % 4 !== 0) return false return /^[A-Za-z0-9+/=]+$/.test(s) @@ -1886,6 +1909,11 @@ class ExportService { }) } + // ========== 预加载群昵称(用于名称显示偏好) ========== + const groupNicknamesMap = isGroup + ? await this.getGroupNicknamesForRoom(sessionId) + : new Map() + // ========== 阶段3:构建消息列表 ========== onProgress?.({ current: 55, @@ -1912,6 +1940,24 @@ class ExportService { content = this.parseMessageContent(msg.content, msg.localType) } + // 获取发送者信息用于名称显示 + const senderWxid = msg.senderUsername + const contact = await wcdbService.getContact(senderWxid) + const senderNickname = contact.success && contact.contact?.nickName + ? contact.contact.nickName + : (senderInfo.displayName || senderWxid) + const senderRemark = contact.success && contact.contact?.remark ? contact.contact.remark : '' + const senderGroupNickname = groupNicknamesMap.get(senderWxid?.toLowerCase() || '') || '' + + // 使用用户偏好的显示名称 + const senderDisplayName = this.getPreferredDisplayName( + senderWxid, + senderNickname, + senderRemark, + senderGroupNickname, + options.displayNamePreference || 'remark' + ) + allMessages.push({ localId: allMessages.length + 1, createTime: msg.createTime, @@ -1921,7 +1967,7 @@ class ExportService { content, isSend: msg.isSend ? 1 : 0, senderUsername: msg.senderUsername, - senderDisplayName: senderInfo.displayName, + senderDisplayName, source, senderAvatarKey: msg.senderUsername }) @@ -1938,14 +1984,33 @@ class ExportService { const { chatlab, meta } = this.getExportMeta(sessionId, sessionInfo, isGroup) + // 获取会话的昵称和备注信息 + const sessionContact = await wcdbService.getContact(sessionId) + const sessionNickname = sessionContact.success && sessionContact.contact?.nickName + ? sessionContact.contact.nickName + : sessionInfo.displayName + const sessionRemark = sessionContact.success && sessionContact.contact?.remark + ? sessionContact.contact.remark + : '' + const sessionGroupNickname = isGroup + ? (groupNicknamesMap.get(sessionId.toLowerCase()) || '') + : '' + + // 使用用户偏好的显示名称 + const sessionDisplayName = this.getPreferredDisplayName( + sessionId, + sessionNickname, + sessionRemark, + sessionGroupNickname, + options.displayNamePreference || 'remark' + ) + const detailedExport: any = { - chatlab, - meta, session: { wxid: sessionId, - nickname: sessionInfo.displayName, - remark: sessionInfo.displayName, - displayName: sessionInfo.displayName, + nickname: sessionNickname, + remark: sessionRemark, + displayName: sessionDisplayName, type: isGroup ? '群聊' : '私聊', lastTimestamp: collected.lastTime, messageCount: allMessages.length, diff --git a/src/pages/ExportPage.scss b/src/pages/ExportPage.scss index ddedbd9..cdbe7f6 100644 --- a/src/pages/ExportPage.scss +++ b/src/pages/ExportPage.scss @@ -396,6 +396,99 @@ } } + .select-field { + position: relative; + } + + .select-trigger { + width: 100%; + padding: 10px 16px; + border: 1px solid var(--border-color); + border-radius: 9999px; + font-size: 14px; + background: var(--bg-primary); + color: var(--text-primary); + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + cursor: pointer; + transition: all 0.2s; + + &:hover { + border-color: var(--text-tertiary); + } + + &.open { + border-color: var(--primary); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 15%, transparent); + } + } + + .select-value { + flex: 1; + min-width: 0; + text-align: left; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .select-dropdown { + position: absolute; + top: calc(100% + 6px); + left: 0; + right: 0; + background: color-mix(in srgb, var(--bg-primary) 85%, var(--bg-secondary)); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 6px; + box-shadow: var(--shadow-md); + z-index: 20; + max-height: 260px; + overflow-y: auto; + backdrop-filter: blur(14px); + -webkit-backdrop-filter: blur(14px); + } + + .select-option { + width: 100%; + text-align: left; + display: flex; + flex-direction: column; + gap: 4px; + padding: 10px 12px; + border: none; + border-radius: 10px; + background: transparent; + cursor: pointer; + transition: all 0.15s; + color: var(--text-primary); + font-size: 14px; + + &:hover { + background: var(--bg-tertiary); + } + + &.active { + background: color-mix(in srgb, var(--primary) 12%, transparent); + color: var(--primary); + } + } + + .option-label { + font-weight: 500; + } + + .option-desc { + font-size: 12px; + color: var(--text-tertiary); + } + + .select-option.active .option-desc { + color: var(--primary); + } + .media-options { display: flex; flex-wrap: wrap; @@ -1130,11 +1223,11 @@ } } - input:checked + .slider { + input:checked+.slider { background-color: var(--primary); } - input:checked + .slider::before { + input:checked+.slider::before { transform: translateX(20px); } } diff --git a/src/pages/ExportPage.tsx b/src/pages/ExportPage.tsx index bd9365b..d1f5bad 100644 --- a/src/pages/ExportPage.tsx +++ b/src/pages/ExportPage.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react' +import { useState, useEffect, useCallback, useRef } from 'react' import { Search, Download, FolderOpen, RefreshCw, Check, Calendar, FileJson, FileText, Table, Loader2, X, ChevronDown, ChevronLeft, ChevronRight, FileSpreadsheet, Database, FileCode, CheckCircle, XCircle, ExternalLink } from 'lucide-react' import * as configService from '../services/config' import './ExportPage.scss' @@ -23,6 +23,7 @@ interface ExportOptions { exportVoiceAsText: boolean excelCompactColumns: boolean txtColumns: string[] + displayNamePreference: 'group-nickname' | 'remark' | 'nickname' } interface ExportResult { @@ -49,6 +50,8 @@ function ExportPage() { const [calendarDate, setCalendarDate] = useState(new Date()) const [selectingStart, setSelectingStart] = useState(true) const [showMediaLayoutPrompt, setShowMediaLayoutPrompt] = useState(false) + const [showDisplayNameSelect, setShowDisplayNameSelect] = useState(false) + const displayNameDropdownRef = useRef(null) const [options, setOptions] = useState({ format: 'excel', @@ -64,7 +67,8 @@ function ExportPage() { exportEmojis: true, exportVoiceAsText: true, excelCompactColumns: true, - txtColumns: defaultTxtColumns + txtColumns: defaultTxtColumns, + displayNamePreference: 'remark' }) const buildDateRangeFromPreset = (preset: string) => { @@ -189,6 +193,16 @@ function ExportPage() { removeListener?.() } }, []) + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Node + if (showDisplayNameSelect && displayNameDropdownRef.current && !displayNameDropdownRef.current.contains(target)) { + setShowDisplayNameSelect(false) + } + } + document.addEventListener('mousedown', handleClickOutside) + return () => document.removeEventListener('mousedown', handleClickOutside) + }, [showDisplayNameSelect]) useEffect(() => { if (!searchKeyword.trim()) { @@ -271,6 +285,7 @@ function ExportPage() { exportVoiceAsText: options.exportVoiceAsText, // 即使不导出媒体,也可以导出语音转文字内容 excelCompactColumns: options.excelCompactColumns, txtColumns: options.txtColumns, + displayNamePreference: options.displayNamePreference, sessionLayout, dateRange: options.useAllTime ? null : options.dateRange ? { start: Math.floor(options.dateRange.start.getTime() / 1000), @@ -402,6 +417,25 @@ function ExportPage() { { value: 'excel', label: 'Excel', icon: FileSpreadsheet, desc: '电子表格,适合统计分析' }, { value: 'sql', label: 'PostgreSQL', icon: Database, desc: '数据库脚本,便于导入到数据库' } ] + const displayNameOptions = [ + { + value: 'group-nickname', + label: '群昵称优先', + desc: '仅群聊有效,私聊显示备注/昵称' + }, + { + value: 'remark', + label: '备注优先', + desc: '有备注显示备注,否则显示昵称' + }, + { + value: 'nickname', + label: '微信昵称', + desc: '始终显示微信昵称' + } + ] + const displayNameOption = displayNameOptions.find(option => option.value === options.displayNamePreference) + const displayNameLabel = displayNameOption?.label || '备注优先' return (
@@ -516,6 +550,44 @@ function ExportPage() {
+ {/* 发送者名称显示偏好 */} + {(options.format === 'html' || options.format === 'json' || options.format === 'txt') && ( +
+

发送者名称显示

+

选择导出时优先显示的名称

+
+ + {showDisplayNameSelect && ( +
+ {displayNameOptions.map(option => ( + + ))} +
+ )} +
+
+ )}

媒体文件

导出图片/语音/表情并在记录内写入相对路径

diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index cce6785..abd1d6f 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -354,6 +354,7 @@ export interface ExportOptions { excelCompactColumns?: boolean txtColumns?: string[] sessionLayout?: 'shared' | 'per-session' + displayNamePreference?: 'group-nickname' | 'remark' | 'nickname' } export interface ExportProgress {