diff --git a/electron/services/chatService.ts b/electron/services/chatService.ts index e55dae7..81ea83a 100644 --- a/electron/services/chatService.ts +++ b/electron/services/chatService.ts @@ -103,7 +103,7 @@ export interface ContactInfo { remark?: string nickname?: string avatarUrl?: string - type: 'friend' | 'group' | 'official' | 'other' + type: 'friend' | 'group' | 'official' | 'deleted_friend' | 'other' } // 表情包缓存 @@ -603,7 +603,7 @@ class ChatService { // 使用execQuery直接查询加密的contact.db // kind='contact', path=null表示使用已打开的contact.db const contactQuery = ` - SELECT username, remark, nick_name, alias, local_type + SELECT username, remark, nick_name, alias, local_type, flag FROM contact ` @@ -663,28 +663,31 @@ class ChatService { } // 判断类型 - 正确规则:wxid开头且有alias的是好友 - let type: 'friend' | 'group' | 'official' | 'other' = 'other' + let type: 'friend' | 'group' | 'official' | 'deleted_friend' | 'other' = 'other' const localType = row.local_type || 0 + const flag = Number(row.flag ?? 0) + if (username.includes('@chatroom')) { type = 'group' } else if (username.startsWith('gh_')) { + if (flag === 0) continue type = 'official' } else if (localType === 3 || localType === 4) { + if (flag === 0) continue + if (flag === 4) continue type = 'official' } else if (username.startsWith('wxid_') && row.alias) { - // wxid开头且有alias的是好友 - type = 'friend' + type = flag === 0 ? 'deleted_friend' : 'friend' } else if (localType === 1) { - // local_type=1 也是好友 - type = 'friend' + type = flag === 0 ? 'deleted_friend' : 'friend' } else if (localType === 2) { // local_type=2 是群成员但非好友,跳过 continue } else if (localType === 0) { // local_type=0 可能是好友或其他,检查是否有备注或昵称 if (row.remark || row.nick_name) { - type = 'friend' + type = flag === 0 ? 'deleted_friend' : 'friend' } else { continue } diff --git a/electron/services/exportService.ts b/electron/services/exportService.ts index be1a653..8d4fbf5 100644 --- a/electron/services/exportService.ts +++ b/electron/services/exportService.ts @@ -1479,13 +1479,17 @@ class ExportService { result.localPath = thumbResult.localPath } + // 为每条消息生成稳定且唯一的文件名前缀,避免跨日期/消息发生同名覆盖 + const messageId = String(msg.localId || Date.now()) + const imageKey = (imageMd5 || imageDatName || 'image').replace(/[^a-zA-Z0-9_-]/g, '') + // 从 data URL 或 file URL 获取实际路径 let sourcePath = result.localPath if (sourcePath.startsWith('data:')) { // 是 data URL,需要保存为文件 const base64Data = sourcePath.split(',')[1] const ext = this.getExtFromDataUrl(sourcePath) - const fileName = `${imageMd5 || imageDatName || msg.localId}${ext}` + const fileName = `${messageId}_${imageKey}${ext}` const destPath = path.join(imagesDir, fileName) fs.writeFileSync(destPath, Buffer.from(base64Data, 'base64')) @@ -1501,7 +1505,7 @@ class ExportService { // 复制文件 if (!fs.existsSync(sourcePath)) return null const ext = path.extname(sourcePath) || '.jpg' - const fileName = `${imageMd5 || imageDatName || msg.localId}${ext}` + const fileName = `${messageId}_${imageKey}${ext}` const destPath = path.join(imagesDir, fileName) if (!fs.existsSync(destPath)) { @@ -4769,4 +4773,3 @@ class ExportService { } export const exportService = new ExportService() - diff --git a/src/components/NotificationToast.scss b/src/components/NotificationToast.scss index cb405a3..b48013c 100644 --- a/src/components/NotificationToast.scss +++ b/src/components/NotificationToast.scss @@ -6,6 +6,13 @@ backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border: 1px solid var(--border-light); + + // 浅色模式下使用不透明背景,避免透明窗口中通知过于透明 + [data-mode="light"] &, + :not([data-mode]) & { + background: rgba(255, 255, 255, 1); + } + border-radius: 12px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); padding: 12px; @@ -39,7 +46,7 @@ backdrop-filter: none !important; -webkit-backdrop-filter: none !important; - // Ensure background is solid + // 确保背景不透明 background: var(--bg-secondary, #2c2c2c); color: var(--text-primary, #ffffff); diff --git a/src/pages/ChatPage.scss b/src/pages/ChatPage.scss index 05fb05a..40ad9ca 100644 --- a/src/pages/ChatPage.scss +++ b/src/pages/ChatPage.scss @@ -1288,6 +1288,21 @@ z-index: 2; } +.empty-chat-inline { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 10px; + padding: 60px 0; + color: var(--text-tertiary); + font-size: 14px; + + svg { + opacity: 0.4; + } +} + .message-list * { -webkit-app-region: no-drag !important; } diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index f0f5641..26b019a 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -1261,6 +1261,7 @@ function ChatPage(_props: ChatPageProps) { useEffect(() => { if (currentSessionId && messages.length === 0 && !isLoadingMessages && !isLoadingMore) { + setHasInitialMessages(false) loadMessages(currentSessionId, 0) } }, [currentSessionId, messages.length, isLoadingMessages, isLoadingMore]) @@ -1327,8 +1328,21 @@ function ChatPage(_props: ChatPageProps) { return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}` }, []) - // 获取当前会话信息 - const currentSession = Array.isArray(sessions) ? sessions.find(s => s.username === currentSessionId) : undefined + // 获取当前会话信息(从通讯录跳转时可能不在 sessions 列表中,构造 fallback) + const currentSession = (() => { + const found = Array.isArray(sessions) ? sessions.find(s => s.username === currentSessionId) : undefined + if (found || !currentSessionId) return found + return { + username: currentSessionId, + type: 0, + unreadCount: 0, + summary: '', + sortTimestamp: 0, + lastTimestamp: 0, + lastMsgType: 0, + displayName: currentSessionId, + } as ChatSession + })() // 判断是否为群聊 const isGroupChat = (username: string) => username.includes('@chatroom') @@ -2048,6 +2062,13 @@ function ChatPage(_props: ChatPageProps) { )} + {!isLoadingMessages && messages.length === 0 && !hasMoreMessages && ( +
+ + 该联系人没有聊天记录 +
+ )} + {messages.map((msg, index) => { const prevMsg = index > 0 ? messages[index - 1] : undefined const showDateDivider = shouldShowDateDivider(msg, prevMsg) diff --git a/src/pages/ContactsPage.scss b/src/pages/ContactsPage.scss index 2609639..7f5a1b3 100644 --- a/src/pages/ContactsPage.scss +++ b/src/pages/ContactsPage.scss @@ -7,8 +7,8 @@ // 左侧联系人面板 .contacts-panel { - width: 380px; - min-width: 380px; + width: 400px; + min-width: 400px; display: flex; flex-direction: column; border-right: 1px solid var(--border-color); @@ -55,6 +55,11 @@ .spin { animation: contactsSpin 1s linear infinite; } + + &.export-mode-btn.active { + background: var(--primary); + color: #fff; + } } } @@ -231,8 +236,8 @@ padding: 12px; border-radius: 10px; transition: all 0.2s; - margin-bottom: 4px; cursor: pointer; + margin-bottom: 4px; &:hover { background: var(--bg-hover); @@ -242,6 +247,10 @@ background: color-mix(in srgb, var(--primary) 12%, transparent); } + &.active { + background: var(--bg-tertiary); + } + .contact-select { display: flex; align-items: center; @@ -334,6 +343,93 @@ } } + // 右侧详情面板内的联系人资料 + .detail-profile { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + padding-bottom: 24px; + border-bottom: 1px solid var(--border-color); + margin-bottom: 20px; + + .detail-avatar { + width: 80px; + height: 80px; + border-radius: 16px; + background: linear-gradient(135deg, var(--primary), var(--primary-hover)); + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + + img { width: 100%; height: 100%; object-fit: cover; } + span { color: #fff; font-size: 28px; font-weight: 600; } + } + + .detail-name { + font-size: 18px; + font-weight: 600; + color: var(--text-primary); + } + } + + .detail-info-list { + margin-bottom: 24px; + + .detail-row { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 0; + font-size: 13px; + border-bottom: 1px solid var(--border-color); + + &:last-child { border-bottom: none; } + } + + .detail-label { + color: var(--text-tertiary); + min-width: 48px; + flex-shrink: 0; + } + + .detail-value { + color: var(--text-primary); + word-break: break-all; + } + } + + .goto-chat-btn { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 12px; + background: var(--primary); + color: #fff; + border: none; + border-radius: 10px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + + &:hover { background: var(--primary-hover); } + } + + .empty-detail { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12px; + color: var(--text-tertiary); + font-size: 14px; + } + // 右侧设置面板 .settings-panel { flex: 1; diff --git a/src/pages/ContactsPage.tsx b/src/pages/ContactsPage.tsx index 43968ae..198c809 100644 --- a/src/pages/ContactsPage.tsx +++ b/src/pages/ContactsPage.tsx @@ -1,5 +1,7 @@ import { useState, useEffect, useCallback, useRef } from 'react' -import { Search, RefreshCw, X, User, Users, MessageSquare, Loader2, FolderOpen, Download, ChevronDown } from 'lucide-react' +import { useNavigate } from 'react-router-dom' +import { Search, RefreshCw, X, User, Users, MessageSquare, Loader2, FolderOpen, Download, ChevronDown, MessageCircle, UserX } from 'lucide-react' +import { useChatStore } from '../stores/chatStore' import './ContactsPage.scss' interface ContactInfo { @@ -8,7 +10,7 @@ interface ContactInfo { remark?: string nickname?: string avatarUrl?: string - type: 'friend' | 'group' | 'official' | 'other' + type: 'friend' | 'group' | 'official' | 'deleted_friend' | 'other' } function ContactsPage() { @@ -20,9 +22,16 @@ function ContactsPage() { const [contactTypes, setContactTypes] = useState({ friends: true, groups: true, - officials: true + officials: true, + deletedFriends: false }) + // 导出模式与查看详情 + const [exportMode, setExportMode] = useState(false) + const [selectedContact, setSelectedContact] = useState(null) + const navigate = useNavigate() + const { setCurrentSession } = useChatStore() + // 导出相关状态 const [exportFormat, setExportFormat] = useState<'json' | 'csv' | 'vcf'>('json') const [exportAvatars, setExportAvatars] = useState(true) @@ -85,6 +94,7 @@ function ContactsPage() { if (c.type === 'friend' && !contactTypes.friends) return false if (c.type === 'group' && !contactTypes.groups) return false if (c.type === 'official' && !contactTypes.officials) return false + if (c.type === 'deleted_friend' && !contactTypes.deletedFriends) return false return true }) @@ -154,6 +164,7 @@ function ContactsPage() { case 'friend': return case 'group': return case 'official': return + case 'deleted_friend': return default: return } } @@ -163,6 +174,7 @@ function ContactsPage() { case 'friend': return '好友' case 'group': return '群聊' case 'official': return '公众号' + case 'deleted_friend': return '已删除' default: return '其他' } } @@ -236,9 +248,18 @@ function ContactsPage() {

通讯录

- +
+ + +
@@ -258,49 +279,41 @@ function ContactsPage() {
+
共 {filteredContacts.length} 个联系人
-
- - 已选 {selectedUsernames.size}(当前筛选 {selectedInFilteredCount} / {filteredContacts.length}) -
+ + {exportMode && ( +
+ + 已选 {selectedUsernames.size}(当前筛选 {selectedInFilteredCount} / {filteredContacts.length}) +
+ )} {isLoading ? (
@@ -314,20 +327,29 @@ function ContactsPage() { ) : (
{filteredContacts.map(contact => { - const isSelected = selectedUsernames.has(contact.username) + const isChecked = selectedUsernames.has(contact.username) + const isActive = !exportMode && selectedContact?.username === contact.username return (
toggleContactSelected(contact.username, !isSelected)} + className={`contact-item ${exportMode && isChecked ? 'selected' : ''} ${isActive ? 'active' : ''}`} + onClick={() => { + if (exportMode) { + toggleContactSelected(contact.username, !isChecked) + } else { + setSelectedContact(isActive ? null : contact) + } + }} > - + {exportMode && ( + + )}
{contact.avatarUrl ? ( @@ -352,90 +374,129 @@ function ContactsPage() { )}
- {/* 右侧:导出设置 */} -
-
-

导出设置

-
+ {/* 右侧面板 */} + {exportMode ? ( +
+
+

导出设置

+
-
-
-

导出格式

-
- + {showFormatSelect && ( +
+ {exportFormatOptions.map(option => ( + + ))} +
+ )} +
+
+ +
+

导出选项

+ +
+ +
+

导出位置

+
+ + {exportFolder || '未设置'} +
+ - {showFormatSelect && ( -
- {exportFormatOptions.map(option => ( - - ))} -
+
+
+ +
+
-
- -
-

导出选项

- -
- -
-

导出位置

-
- - {exportFolder || '未设置'} -
-
+ ) : selectedContact ? ( +
+
+

联系人详情

+
+
+
+
+ {selectedContact.avatarUrl ? ( + + ) : ( + {getAvatarLetter(selectedContact.displayName)} + )} +
+
{selectedContact.displayName}
+
+ {getContactTypeIcon(selectedContact.type)} + {getContactTypeName(selectedContact.type)} +
+
-
- +
+
用户名{selectedContact.username}
+
昵称{selectedContact.nickname || selectedContact.displayName}
+ {selectedContact.remark &&
备注{selectedContact.remark}
} +
类型{getContactTypeName(selectedContact.type)}
+
+ + +
-
+ ) : ( +
+
+ + 点击左侧联系人查看详情 +
+
+ )}
) } diff --git a/src/pages/NotificationWindow.tsx b/src/pages/NotificationWindow.tsx index deb6616..2e9acd0 100644 --- a/src/pages/NotificationWindow.tsx +++ b/src/pages/NotificationWindow.tsx @@ -1,9 +1,11 @@ import { useEffect, useState, useRef } from 'react' import { NotificationToast, type NotificationData } from '../components/NotificationToast' +import { useThemeStore } from '../stores/themeStore' import '../components/NotificationToast.scss' import './NotificationWindow.scss' export default function NotificationWindow() { + const { currentTheme, themeMode } = useThemeStore() const [notification, setNotification] = useState(null) const [prevNotification, setPrevNotification] = useState(null) @@ -17,6 +19,12 @@ export default function NotificationWindow() { const notificationRef = useRef(null) + // 应用主题到通知窗口 + useEffect(() => { + document.documentElement.setAttribute('data-theme', currentTheme) + document.documentElement.setAttribute('data-mode', themeMode) + }, [currentTheme, themeMode]) + useEffect(() => { notificationRef.current = notification }, [notification])