diff --git a/electron/services/snsService.ts b/electron/services/snsService.ts index 5bf4d43..2bb2908 100644 --- a/electron/services/snsService.ts +++ b/electron/services/snsService.ts @@ -52,7 +52,6 @@ interface SnsContactIdentity { remark?: string nickName?: string displayName: string - avatarUrl?: string } interface ParsedLikeUser { @@ -80,7 +79,6 @@ interface ArkmeLikeDetail { remark?: string nickName?: string displayName: string - avatarUrl?: string source: 'xml' | 'legacy' } @@ -94,7 +92,6 @@ interface ArkmeCommentDetail { remark?: string nickName?: string displayName: string - avatarUrl?: string content: string refCommentId: string refNickname?: string @@ -105,7 +102,6 @@ interface ArkmeCommentDetail { refRemark?: string refNickName?: string refDisplayName?: string - refAvatarUrl?: string emojis?: { url: string; md5: string; width: number; height: number; encryptUrl?: string; aesKey?: string }[] source: 'xml' | 'legacy' } @@ -327,7 +323,6 @@ class SnsService { let alias: string | undefined let remark: string | undefined let nickName: string | undefined - let avatarUrl = this.toOptionalString(cached?.avatarUrl) try { const contactResult = await wcdbService.getContact(normalized) @@ -341,17 +336,6 @@ class SnsService { // 联系人补全失败不影响导出 } - if (!avatarUrl) { - try { - const avatarResult = await wcdbService.getAvatarUrls([normalized]) - if (avatarResult.success && avatarResult.map) { - avatarUrl = this.toOptionalString(avatarResult.map[normalized]) - } - } catch { - // 头像补全失败不影响导出 - } - } - const displayName = remark || nickName || alias || cached?.displayName || normalized return { username: normalized, @@ -360,8 +344,7 @@ class SnsService { wechatId: alias, remark, nickName, - displayName, - avatarUrl + displayName } })() identityCache.set(normalized, pending) @@ -429,7 +412,6 @@ class SnsService { remark: identity?.remark, nickName: identity?.nickName, displayName: identity?.displayName || nickname || username || '', - avatarUrl: identity?.avatarUrl, source: likeSource }) } @@ -501,7 +483,6 @@ class SnsService { remark: actor?.remark, nickName: actor?.nickName, displayName: actor?.displayName || nickname || username || '', - avatarUrl: actor?.avatarUrl, content: comment.content || '', refCommentId: comment.refCommentId || '', refNickname: comment.refNickname || refActor?.displayName, @@ -512,7 +493,6 @@ class SnsService { refRemark: refActor?.remark, refNickName: refActor?.nickName, refDisplayName: refActor?.displayName, - refAvatarUrl: refActor?.avatarUrl, emojis: comment.emojis, source: commentSource }) @@ -1041,8 +1021,7 @@ class SnsService { const result = await wcdbService.getSnsTimeline(limit, offset, usernames, keyword, startTime, endTime) if (!result.success || !result.timeline || result.timeline.length === 0) return result - const identityCache = new Map>() - const enrichedTimeline = await Promise.all(result.timeline.map(async (post: any) => { + const enrichedTimeline = result.timeline.map((post: any) => { const contact = this.contactCache.get(post.username) const isVideoPost = post.type === 15 const videoKey = extractVideoKey(post.rawXml || '') @@ -1082,22 +1061,14 @@ class SnsService { finalComments = this.fixCommentRefs(dllComments) } - const normalizedPost: SnsPost = { - ...post, - comments: finalComments - } - const { likesDetail, commentsDetail } = await this.buildArkmeInteractionDetails(normalizedPost, identityCache) - return { ...post, avatarUrl: contact?.avatarUrl, nickname: post.nickname || contact?.displayName || post.username, media: fixedMedia, - comments: finalComments, - likesDetail, - commentsDetail + comments: finalComments } - })) + }) return { ...result, timeline: enrichedTimeline } } diff --git a/src/pages/ExportPage.scss b/src/pages/ExportPage.scss index e6f895c..d1c8c0d 100644 --- a/src/pages/ExportPage.scss +++ b/src/pages/ExportPage.scss @@ -623,22 +623,18 @@ .session-mutual-friends-row { display: grid; - grid-template-columns: 36px minmax(0, 1fr) 72px 108px; - gap: 12px; + grid-template-columns: 36px minmax(120px, 0.82fr) max-content 56px 96px minmax(0, 1.28fr); + gap: 10px; align-items: center; - padding: 12px; + padding: 8px 12px; border-bottom: 1px solid color-mix(in srgb, var(--border-color) 68%, transparent); font-size: 12px; color: var(--text-secondary); - min-height: 72px; + min-height: 42px; &:last-child { border-bottom: none; } - - &.unconfirmed { - background: color-mix(in srgb, var(--bg-secondary) 60%, transparent); - } } .session-mutual-friends-rank, @@ -652,32 +648,6 @@ text-align: center; } -.session-mutual-friends-user { - min-width: 0; - display: flex; - align-items: center; - gap: 12px; -} - -.session-mutual-friends-user-avatar { - flex-shrink: 0; -} - -.session-mutual-friends-user-main { - min-width: 0; - display: flex; - flex-direction: column; - gap: 4px; -} - -.session-mutual-friends-user-head { - min-width: 0; - display: flex; - align-items: center; - gap: 8px; - flex-wrap: wrap; -} - .session-mutual-friends-name { min-width: 0; overflow: hidden; @@ -717,30 +687,6 @@ } } -.session-mutual-friends-identity-badge { - border-radius: 999px; - padding: 3px 8px; - font-size: 10px; - line-height: 1; - color: #92400e; - border: 1px solid color-mix(in srgb, #d97706 34%, var(--border-color)); - background: color-mix(in srgb, #f59e0b 11%, var(--bg-secondary)); - white-space: nowrap; -} - -.session-mutual-friends-identity { - min-width: 0; - color: var(--text-secondary); - line-height: 1.25; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - &.secondary { - color: var(--text-tertiary); - } -} - .session-mutual-friends-desc { min-width: 0; color: var(--text-tertiary); @@ -2034,7 +1980,6 @@ height: var(--contacts-default-list-height); overflow: hidden; padding: 0 0 12px; - position: relative; } .contacts-virtuoso { @@ -2052,43 +1997,6 @@ } } - .contacts-action-rail { - position: absolute; - top: 0; - right: 0; - bottom: 12px; - width: max(var(--contacts-action-col-width), 184px); - z-index: 18; - pointer-events: none; - - &::before { - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: -12px; - width: 12px; - pointer-events: none; - background: linear-gradient(to right, transparent, var(--bg-primary)); - } - } - - .contacts-action-rail-row { - position: absolute; - right: 0; - width: 100%; - height: var(--contacts-row-height); - padding: 0 0 4px; - box-sizing: border-box; - display: flex; - justify-content: flex-end; - pointer-events: none; - - &.selected .contacts-action-rail-card { - background: rgba(var(--primary-rgb), 0.08); - } - } - .table-bottom-scrollbar { flex: 0 0 auto; overflow-x: auto; @@ -2183,6 +2091,10 @@ &.selected .contact-item { background: rgba(var(--primary-rgb), 0.08); } + + &.selected .row-action-cell { + background: rgba(var(--primary-rgb), 0.08); + } } .contact-item { @@ -2562,30 +2474,26 @@ } } -.row-action-cell, -.contacts-action-rail-card { +.row-action-cell { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; - width: 100%; - min-width: 0; - height: calc(var(--contacts-row-height) - 4px); - padding: 0 6px 0 0; - box-sizing: border-box; - justify-content: center; + width: var(--contacts-action-col-width); + min-width: var(--contacts-action-col-width); + flex-shrink: 0; + position: sticky; + right: 0; + z-index: 10; background: var(--bg-primary); - pointer-events: auto; - position: relative; - z-index: 1; &::before { content: ''; position: absolute; top: -12px; bottom: -12px; - left: -12px; - width: 12px; + left: -8px; + width: 8px; pointer-events: none; background: linear-gradient(to right, transparent, var(--bg-primary)); } @@ -4253,19 +4161,13 @@ } .session-mutual-friends-row { - grid-template-columns: 28px minmax(0, 1fr) 56px 72px; + grid-template-columns: 30px minmax(88px, 0.9fr) max-content 44px 74px; gap: 8px; font-size: 12px; } - .session-mutual-friends-user { - align-items: flex-start; - gap: 10px; - } - - .session-mutual-friends-user-avatar { - width: 36px !important; - height: 36px !important; + .session-mutual-friends-desc { + display: none; } .session-load-detail-row { diff --git a/src/pages/ExportPage.tsx b/src/pages/ExportPage.tsx index b5741ce..38d01a5 100644 --- a/src/pages/ExportPage.tsx +++ b/src/pages/ExportPage.tsx @@ -44,12 +44,11 @@ import { subscribeBackgroundTasks } from '../services/backgroundTaskMonitor' import { useContactTypeCountsStore } from '../stores/contactTypeCountsStore' -import { Avatar } from '../components/Avatar' import { SnsPostItem } from '../components/Sns/SnsPostItem' import { ContactSnsTimelineDialog } from '../components/Sns/ContactSnsTimelineDialog' import { ExportDateRangeDialog } from '../components/Export/ExportDateRangeDialog' import { ExportDefaultsSettingsForm, type ExportDefaultsSettingsPatch } from '../components/Export/ExportDefaultsSettingsForm' -import type { SnsCommentDetail, SnsLikeDetail, SnsPost } from '../types/sns' +import type { SnsPost } from '../types/sns' import { cloneExportDateRange, createDefaultDateRange, @@ -73,7 +72,6 @@ type DisplayNamePreference = 'group-nickname' | 'remark' | 'nickname' type TextExportFormat = 'chatlab' | 'chatlab-jsonl' | 'json' | 'arkme-json' | 'html' | 'txt' | 'excel' | 'weclone' | 'sql' type SnsTimelineExportFormat = 'json' | 'html' | 'arkmejson' -const CONTACTS_ROW_HEIGHT = 76 interface ExportOptions { format: TextExportFormat @@ -515,12 +513,6 @@ const getAvatarLetter = (name: string): string => { return [...name][0] || '?' } -const toOptionalString = (value: unknown): string | undefined => { - if (typeof value !== 'string') return undefined - const trimmed = value.trim() - return trimmed ? trimmed : undefined -} - const toComparableNameSet = (values: Array): Set => { const set = new Set() for (const value of values) { @@ -610,15 +602,7 @@ type SessionMutualFriendDirection = 'incoming' | 'outgoing' | 'bidirectional' type SessionMutualFriendBehavior = 'likes' | 'comments' | 'both' interface SessionMutualFriendItem { - key: string - identityKey?: string name: string - username?: string - wxid?: string - wechatId?: string - remark?: string - avatarUrl?: string - isConfirmed: boolean incomingLikeCount: number incomingCommentCount: number outgoingLikeCount: number @@ -637,63 +621,6 @@ interface SessionMutualFriendsMetric { computedAt: number } -const getSessionMutualFriendIdentityKey = (username?: string, wechatId?: string): string | undefined => { - const normalizedUsername = toOptionalString(username) - if (normalizedUsername) return `u:${normalizedUsername}` - const normalizedWechatId = toOptionalString(wechatId) - if (normalizedWechatId) return `w:${normalizedWechatId}` - return undefined -} - -const getSessionMutualFriendFallbackKey = (name: string): string => `n:${name || '未知用户'}` - -const resolveSessionMutualFriendName = (params: { - displayName?: string - remark?: string - nickName?: string - wechatId?: string - nickname?: string - username?: string -}): string => { - return ( - toOptionalString(params.displayName) || - toOptionalString(params.remark) || - toOptionalString(params.nickName) || - toOptionalString(params.wechatId) || - toOptionalString(params.nickname) || - toOptionalString(params.username) || - '未知用户' - ) -} - -const applySessionMutualFriendDerivedState = (item: SessionMutualFriendItem): void => { - const incomingTotal = item.incomingLikeCount + item.incomingCommentCount - const outgoingTotal = item.outgoingLikeCount + item.outgoingCommentCount - item.direction = incomingTotal > 0 && outgoingTotal > 0 - ? 'bidirectional' - : incomingTotal > 0 - ? 'incoming' - : 'outgoing' - item.behavior = summarizeMutualFriendBehavior( - item.incomingLikeCount + item.outgoingLikeCount, - item.incomingCommentCount + item.outgoingCommentCount - ) -} - -const mergeSessionMutualFriendItemProfile = ( - target: SessionMutualFriendItem, - profile: Partial> -): void => { - if (profile.identityKey && !target.identityKey) target.identityKey = profile.identityKey - if (profile.username && !target.username) target.username = profile.username - if (profile.wxid && !target.wxid) target.wxid = profile.wxid - if (profile.wechatId && !target.wechatId) target.wechatId = profile.wechatId - if (profile.remark && !target.remark) target.remark = profile.remark - if (profile.avatarUrl && !target.avatarUrl) target.avatarUrl = profile.avatarUrl - if (profile.name && (!target.name || !target.isConfirmed)) target.name = profile.name - if (profile.isConfirmed) target.isConfirmed = true -} - interface SessionSnsRankCacheEntry { likes: SessionSnsRankItem[] comments: SessionSnsRankItem[] @@ -751,121 +678,55 @@ const buildSessionMutualFriendsMetric = ( ): SessionMutualFriendsMetric => { const friendMap = new Map() - const ensureItem = (seed: { - name: string - username?: string - wxid?: string - wechatId?: string - remark?: string - avatarUrl?: string - identityKey?: string - isConfirmed: boolean - }): SessionMutualFriendItem => { - const key = seed.identityKey || getSessionMutualFriendFallbackKey(seed.name) - const existing = friendMap.get(key) - if (existing) { - mergeSessionMutualFriendItemProfile(existing, seed) - return existing - } - - const created: SessionMutualFriendItem = { - key, - identityKey: seed.identityKey, - name: seed.name, - username: seed.username, - wxid: seed.wxid, - wechatId: seed.wechatId, - remark: seed.remark, - avatarUrl: seed.avatarUrl, - isConfirmed: seed.isConfirmed, - incomingLikeCount: 0, - incomingCommentCount: 0, - outgoingLikeCount: 0, - outgoingCommentCount: 0, - totalCount: 0, - latestTime: 0, - direction: 'incoming', - behavior: 'likes' - } - friendMap.set(key, created) - return created - } - for (const post of posts) { const createTime = Number(post?.createTime) || 0 - const likesDetail = Array.isArray(post?.likesDetail) && post.likesDetail.length > 0 - ? post.likesDetail - : (Array.isArray(post?.likes) ? post.likes : []).map((likeNameRaw): SnsLikeDetail => ({ - nickname: String(likeNameRaw || '').trim() || '未知用户', - displayName: String(likeNameRaw || '').trim() || '未知用户', - source: 'legacy' - })) - const commentsDetail = Array.isArray(post?.commentsDetail) && post.commentsDetail.length > 0 - ? post.commentsDetail - : (Array.isArray(post?.comments) ? post.comments : []).map((comment): SnsCommentDetail => ({ - id: String(comment?.id || ''), - nickname: String(comment?.nickname || '').trim() || '未知用户', - displayName: String(comment?.nickname || '').trim() || '未知用户', - content: String(comment?.content || ''), - refCommentId: String(comment?.refCommentId || ''), - refNickname: comment?.refNickname, - refUsername: comment?.refUsername, - emojis: comment?.emojis, - source: 'legacy' - })) + const likes = Array.isArray(post?.likes) ? post.likes : [] + const comments = Array.isArray(post?.comments) ? post.comments : [] - for (const like of likesDetail) { - const username = toOptionalString(like.username || like.wxid) - const wechatId = toOptionalString(like.wechatId || like.alias) - const identityKey = getSessionMutualFriendIdentityKey(username, wechatId) - const existing = ensureItem({ - name: resolveSessionMutualFriendName({ - displayName: like.displayName, - remark: like.remark, - nickName: like.nickName, - wechatId, - nickname: like.nickname, - username - }), - username, - wxid: username, - wechatId, - remark: toOptionalString(like.remark), - avatarUrl: toOptionalString(like.avatarUrl), - identityKey, - isConfirmed: Boolean(identityKey && username) + for (const likeNameRaw of likes) { + const name = String(likeNameRaw || '').trim() || '未知用户' + const existing = friendMap.get(name) + if (existing) { + existing.incomingLikeCount += 1 + existing.totalCount += 1 + existing.behavior = existing.incomingCommentCount > 0 ? 'both' : 'likes' + if (createTime > existing.latestTime) existing.latestTime = createTime + continue + } + friendMap.set(name, { + name, + incomingLikeCount: 1, + incomingCommentCount: 0, + outgoingLikeCount: 0, + outgoingCommentCount: 0, + totalCount: 1, + latestTime: createTime, + direction: 'incoming', + behavior: 'likes' }) - existing.incomingLikeCount += 1 - existing.totalCount += 1 - if (createTime > existing.latestTime) existing.latestTime = createTime - applySessionMutualFriendDerivedState(existing) } - for (const comment of commentsDetail) { - const username = toOptionalString(comment.username || comment.wxid) - const wechatId = toOptionalString(comment.wechatId || comment.alias) - const identityKey = getSessionMutualFriendIdentityKey(username, wechatId) - const existing = ensureItem({ - name: resolveSessionMutualFriendName({ - displayName: comment.displayName, - remark: comment.remark, - nickName: comment.nickName, - wechatId, - nickname: comment.nickname, - username - }), - username, - wxid: username, - wechatId, - remark: toOptionalString(comment.remark), - avatarUrl: toOptionalString(comment.avatarUrl), - identityKey, - isConfirmed: Boolean(identityKey && username) + for (const comment of comments) { + const name = String(comment?.nickname || '').trim() || '未知用户' + const existing = friendMap.get(name) + if (existing) { + existing.incomingCommentCount += 1 + existing.totalCount += 1 + existing.behavior = existing.incomingLikeCount > 0 ? 'both' : 'comments' + if (createTime > existing.latestTime) existing.latestTime = createTime + continue + } + friendMap.set(name, { + name, + incomingLikeCount: 0, + incomingCommentCount: 1, + outgoingLikeCount: 0, + outgoingCommentCount: 0, + totalCount: 1, + latestTime: createTime, + direction: 'incoming', + behavior: 'comments' }) - existing.incomingCommentCount += 1 - existing.totalCount += 1 - if (createTime > existing.latestTime) existing.latestTime = createTime - applySessionMutualFriendDerivedState(existing) } } @@ -1652,8 +1513,6 @@ function ExportPage() { const [nowTick, setNowTick] = useState(Date.now()) const [isContactsListAtTop, setIsContactsListAtTop] = useState(true) const [isContactsHeaderDragging, setIsContactsHeaderDragging] = useState(false) - const [contactsListScrollTop, setContactsListScrollTop] = useState(0) - const [contactsVisibleRange, setContactsVisibleRange] = useState({ startIndex: 0, endIndex: -1 }) const [contactsHorizontalScrollMetrics, setContactsHorizontalScrollMetrics] = useState({ viewportWidth: 0, contentWidth: 0 @@ -2929,27 +2788,21 @@ function ExportPage() { const getSessionMutualFriendProfile = useCallback((sessionId: string): { displayName: string - remark?: string - wechatId?: string - avatarUrl?: string - primaryIdentityKey: string - candidateIdentityKeys: Set + candidateNames: Set } => { const normalizedSessionId = String(sessionId || '').trim() const contact = contactsList.find(item => item.username === normalizedSessionId) const session = sessionsRef.current.find(item => item.username === normalizedSessionId) const displayName = contact?.displayName || contact?.remark || contact?.nickname || session?.displayName || normalizedSessionId - const wechatId = toOptionalString(contact?.alias) - const candidateIdentityKeys = new Set() - candidateIdentityKeys.add(`u:${normalizedSessionId}`) - if (wechatId) candidateIdentityKeys.add(`w:${wechatId}`) return { displayName, - remark: toOptionalString(contact?.remark), - wechatId, - avatarUrl: toOptionalString(contact?.avatarUrl), - primaryIdentityKey: `u:${normalizedSessionId}`, - candidateIdentityKeys + candidateNames: toComparableNameSet([ + displayName, + contact?.displayName, + contact?.remark, + contact?.nickname, + contact?.alias + ]) } }, [contactsList]) @@ -2961,54 +2814,41 @@ function ExportPage() { const directMetric = directMetrics[normalizedTargetSessionId] if (!directMetric) return null - const targetProfile = getSessionMutualFriendProfile(normalizedTargetSessionId) + const { candidateNames } = getSessionMutualFriendProfile(normalizedTargetSessionId) const mergedMap = new Map() for (const item of directMetric.items) { - mergedMap.set(item.key, { ...item }) + mergedMap.set(item.name, { ...item }) } for (const [sourceSessionId, sourceMetric] of Object.entries(directMetrics)) { if (!sourceMetric || sourceSessionId === normalizedTargetSessionId) continue const sourceProfile = getSessionMutualFriendProfile(sourceSessionId) if (!sourceProfile.displayName) continue - const reverseMatches = sourceMetric.items.filter(item => { - if (!item.identityKey) return false - return targetProfile.candidateIdentityKeys.has(item.identityKey) - }) + if (mergedMap.has(sourceProfile.displayName)) continue + + const reverseMatches = sourceMetric.items.filter(item => candidateNames.has(item.name)) if (reverseMatches.length === 0) continue const reverseCount = reverseMatches.reduce((sum, item) => sum + item.totalCount, 0) const reverseLikeCount = reverseMatches.reduce((sum, item) => sum + item.incomingLikeCount, 0) const reverseCommentCount = reverseMatches.reduce((sum, item) => sum + item.incomingCommentCount, 0) const reverseLatestTime = reverseMatches.reduce((latest, item) => Math.max(latest, item.latestTime), 0) - const existing = mergedMap.get(sourceProfile.primaryIdentityKey) + const existing = mergedMap.get(sourceProfile.displayName) if (existing) { - mergeSessionMutualFriendItemProfile(existing, { - identityKey: sourceProfile.primaryIdentityKey, - username: sourceSessionId, - wxid: sourceSessionId, - wechatId: sourceProfile.wechatId, - remark: sourceProfile.remark, - avatarUrl: sourceProfile.avatarUrl, - name: sourceProfile.displayName, - isConfirmed: true - }) existing.outgoingLikeCount += reverseLikeCount existing.outgoingCommentCount += reverseCommentCount existing.totalCount += reverseCount existing.latestTime = Math.max(existing.latestTime, reverseLatestTime) - applySessionMutualFriendDerivedState(existing) + existing.direction = (existing.incomingLikeCount + existing.incomingCommentCount) > 0 + ? 'bidirectional' + : 'outgoing' + existing.behavior = summarizeMutualFriendBehavior( + existing.incomingLikeCount + existing.outgoingLikeCount, + existing.incomingCommentCount + existing.outgoingCommentCount + ) } else { - const created: SessionMutualFriendItem = { - key: sourceProfile.primaryIdentityKey, - identityKey: sourceProfile.primaryIdentityKey, + mergedMap.set(sourceProfile.displayName, { name: sourceProfile.displayName, - username: sourceSessionId, - wxid: sourceSessionId, - wechatId: sourceProfile.wechatId, - remark: sourceProfile.remark, - avatarUrl: sourceProfile.avatarUrl, - isConfirmed: true, incomingLikeCount: 0, incomingCommentCount: 0, outgoingLikeCount: reverseLikeCount, @@ -3017,8 +2857,7 @@ function ExportPage() { latestTime: reverseLatestTime, direction: 'outgoing', behavior: summarizeMutualFriendBehavior(reverseLikeCount, reverseCommentCount) - } - mergedMap.set(created.key, created) + }) } } @@ -3048,7 +2887,7 @@ function ExportPage() { for (const targetSessionId of allSessionIds) { if (targetSessionId === normalizedSessionId) continue const targetProfile = getSessionMutualFriendProfile(targetSessionId) - if (directMetric.items.some(item => item.identityKey && targetProfile.candidateIdentityKeys.has(item.identityKey))) { + if (directMetric.items.some(item => targetProfile.candidateNames.has(item.name))) { impactedSessionIds.add(targetSessionId) } } @@ -4995,11 +4834,6 @@ function ExportPage() { const endIndex = Number.isFinite(range?.endIndex) ? Math.max(startIndex, Math.floor(range.endIndex)) : startIndex sessionMediaMetricVisibleRangeRef.current = { startIndex, endIndex } sessionMutualFriendsVisibleRangeRef.current = { startIndex, endIndex } - setContactsVisibleRange(prev => ( - prev.startIndex === startIndex && prev.endIndex === endIndex - ? prev - : { startIndex, endIndex } - )) if (isLoadingSessionCountsRef.current || !isSessionCountStageReady) return const visibleTargets = collectVisibleSessionMetricTargets(filteredContacts) if (visibleTargets.length === 0) return @@ -5203,18 +5037,7 @@ function ExportPage() { const items = sessionMutualFriendsDialogMetric?.items || [] const keyword = sessionMutualFriendsSearch.trim().toLowerCase() if (!keyword) return items - return items.filter((item) => { - const haystack = [ - item.name, - item.remark, - item.wechatId, - item.wxid, - item.username - ] - .map(value => String(value || '').toLowerCase()) - .join(' ') - return haystack.includes(keyword) - }) + return items.filter(item => item.name.toLowerCase().includes(keyword)) }, [sessionMutualFriendsDialogMetric, sessionMutualFriendsSearch]) const applySessionDetailStats = useCallback(( @@ -5795,7 +5618,7 @@ function ExportPage() { const taskCenterAlertCount = taskRunningCount + taskQueuedCount const hasFilteredContacts = filteredContacts.length > 0 const contactsTableMinWidth = useMemo(() => { - const baseWidth = 24 + 34 + 44 + 160 + 120 + (4 * 72) + (7 * 12) + const baseWidth = 24 + 34 + 44 + 280 + 120 + (4 * 72) + 140 + (8 * 12) const snsWidth = shouldShowSnsColumn ? 72 + 12 : 0 const mutualFriendsWidth = shouldShowMutualFriendsColumn ? 72 + 12 : 0 return baseWidth + snsWidth + mutualFriendsWidth @@ -5991,6 +5814,11 @@ function ExportPage() { const canExport = Boolean(matchedSession?.hasSession) const isSessionBindingPending = !matchedSession && (isLoading || isSessionEnriching) const checked = canExport && selectedSessions.has(contact.username) + const isRunning = canExport && runningSessionIds.has(contact.username) + const isQueued = canExport && queuedSessionIds.has(contact.username) + const recentExportTimestamp = lastExportBySession[contact.username] + const hasRecentExport = canExport && Boolean(recentExportTimestamp) + const recentExportTime = hasRecentExport ? formatRecentExportTime(recentExportTimestamp, nowTick) : '' const countedMessages = normalizeMessageCount(sessionMessageCounts[contact.username]) const hintedMessages = normalizeMessageCount(matchedSession?.messageCountHint) const displayedMessageCount = countedMessages ?? hintedMessages @@ -6167,14 +5995,47 @@ function ExportPage() { )} )} +
+
+
+ + {hasRecentExport && {recentExportTime}} +
+ +
+
) }, [ + lastExportBySession, + nowTick, openContactSnsTimeline, openSessionDetail, openSessionMutualFriendsDialog, + openSingleExport, + queuedSessionIds, + runningSessionIds, selectedSessions, + sessionDetail?.wxid, sessionContentMetrics, sessionMutualFriendsMetrics, sessionLoadTraceMap, @@ -6189,77 +6050,6 @@ function ExportPage() { snsUserPostCountsStatus, toggleSelectSession ]) - const visibleContactsForActionRail = useMemo(() => { - if (!hasFilteredContacts || contactsVisibleRange.endIndex < contactsVisibleRange.startIndex || contactsVisibleRange.endIndex < 0) return [] - const startIndex = Math.max(0, Math.min(filteredContacts.length - 1, contactsVisibleRange.startIndex)) - const endIndex = Math.max(startIndex, Math.min(filteredContacts.length - 1, contactsVisibleRange.endIndex)) - if (!Number.isFinite(startIndex) || !Number.isFinite(endIndex) || endIndex < startIndex) return [] - return filteredContacts.slice(startIndex, endIndex + 1).map((contact, offset) => { - const index = startIndex + offset - return { - contact, - index, - top: (index * CONTACTS_ROW_HEIGHT) - contactsListScrollTop - } - }) - }, [contactsListScrollTop, contactsVisibleRange.endIndex, contactsVisibleRange.startIndex, filteredContacts, hasFilteredContacts]) - const renderContactActionRailItem = useCallback((contact: ContactInfo, index: number, top: number) => { - const matchedSession = sessionRowByUsername.get(contact.username) - const canExport = Boolean(matchedSession?.hasSession) - const checked = canExport && selectedSessions.has(contact.username) - const isRunning = canExport && runningSessionIds.has(contact.username) - const isQueued = canExport && queuedSessionIds.has(contact.username) - const recentExportTimestamp = lastExportBySession[contact.username] - const hasRecentExport = canExport && Boolean(recentExportTimestamp) - const recentExportTime = hasRecentExport ? formatRecentExportTime(recentExportTimestamp, nowTick) : '' - - return ( -
-
-
-
- - {hasRecentExport && {recentExportTime}} -
- -
-
-
- ) - }, [ - lastExportBySession, - nowTick, - openSessionDetail, - openSingleExport, - queuedSessionIds, - runningSessionIds, - selectedSessions, - sessionDetail?.wxid, - sessionRowByUsername - ]) const handleContactsListWheelCapture = useCallback((event: WheelEvent) => { const deltaY = event.deltaY if (!deltaY) return @@ -6277,19 +6067,9 @@ function ExportPage() { window.scrollBy({ top: deltaY, behavior: 'auto' }) } }, [isContactsListAtTop]) - const handleContactsListScrollCapture = useCallback((event: UIEvent) => { - const target = event.target - if (!(target instanceof HTMLDivElement)) return - const nextScrollTop = Math.max(0, target.scrollTop) - setContactsListScrollTop(prev => ( - Math.abs(prev - nextScrollTop) > 1 ? nextScrollTop : prev - )) - }, []) useEffect(() => { if (hasFilteredContacts) return setIsContactsListAtTop(true) - setContactsListScrollTop(0) - setContactsVisibleRange({ startIndex: 0, endIndex: -1 }) }, [hasFilteredContacts]) const chooseExportFolder = useCallback(async () => { const result = await window.electronAPI.dialog.openFile({ @@ -6701,24 +6481,18 @@ function ExportPage() {
contact.username} - fixedItemHeight={CONTACTS_ROW_HEIGHT} + fixedItemHeight={76} itemContent={renderContactRow} rangeChanged={handleContactsRangeChanged} atTopStateChange={setIsContactsListAtTop} overscan={420} /> -
- {visibleContactsForActionRail.map(({ contact, index, top }) => ( - renderContactActionRailItem(contact, index, top) - ))} -
)} @@ -7016,15 +6790,15 @@ function ExportPage() {
- 打开桌面端微信进入对方朋友圈,刷得越多这里聚合得越全。已确认身份的共同好友会展示头像、备注、微信号;未确认的旧数据仅保留名字。 + 打开桌面端微信,进入到这个人的朋友圈中,刷ta 的朋友圈,刷的越多这里的数据聚合越多
setSessionMutualFriendsSearch(event.target.value)} - placeholder="搜索共同好友(备注 / 微信号 / wxid)" - aria-label="搜索共同好友(备注 / 微信号 / wxid)" + placeholder="搜索共同好友" + aria-label="搜索共同好友" />
@@ -7036,48 +6810,20 @@ function ExportPage() { ) : (
{filteredSessionMutualFriendsDialogItems.map((item, index) => ( -
+
{index + 1} -
- -
-
- {item.name} - - {getSessionMutualFriendDirectionLabel(item.direction)} - - {!item.isConfirmed && ( - 身份未确认 - )} -
-
- {item.isConfirmed ? ( - item.wechatId ? `微信号: ${item.wechatId}` : '微信号: 未设置' - ) : ( - '仅从旧数据中解析到名字' - )} -
-
- {item.isConfirmed - ? `wxid: ${item.wxid || item.username || ''}` - : '没有可用的 wxid,未做自动匹配'} -
- - {describeSessionMutualFriendRelation(item, sessionMutualFriendsDialogTarget.displayName)} - -
-
+ {item.name} + + {getSessionMutualFriendDirectionLabel(item.direction)} + {item.totalCount.toLocaleString('zh-CN')} {formatYmdDateFromSeconds(item.latestTime)} + + {describeSessionMutualFriendRelation(item, sessionMutualFriendsDialogTarget.displayName)} +
))}
diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index ef06b47..72aaa57 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -767,53 +767,7 @@ export interface ElectronAPI { } }> likes: Array - comments: Array<{ - id: string - nickname: string - username?: string - content: string - refCommentId: string - refNickname?: string - refUsername?: string - emojis?: Array<{ url: string; md5: string; width: number; height: number; encryptUrl?: string; aesKey?: string }> - }> - likesDetail?: Array<{ - nickname: string - username?: string - wxid?: string - alias?: string - wechatId?: string - remark?: string - nickName?: string - displayName: string - avatarUrl?: string - source: 'xml' | 'legacy' - }> - commentsDetail?: Array<{ - id: string - nickname: string - username?: string - wxid?: string - alias?: string - wechatId?: string - remark?: string - nickName?: string - displayName: string - avatarUrl?: string - content: string - refCommentId: string - refNickname?: string - refUsername?: string - refWxid?: string - refAlias?: string - refWechatId?: string - refRemark?: string - refNickName?: string - refDisplayName?: string - refAvatarUrl?: string - emojis?: Array<{ url: string; md5: string; width: number; height: number; encryptUrl?: string; aesKey?: string }> - source: 'xml' | 'legacy' - }> + comments: Array<{ id: string; nickname: string; content: string; refCommentId: string; refNickname?: string; emojis?: Array<{ url: string; md5: string; width: number; height: number; encryptUrl?: string; aesKey?: string }> }> rawXml?: string }> error?: string diff --git a/src/types/sns.ts b/src/types/sns.ts index ccfeba3..9193385 100644 --- a/src/types/sns.ts +++ b/src/types/sns.ts @@ -28,53 +28,12 @@ export interface SnsCommentEmoji { export interface SnsComment { id: string nickname: string - username?: string content: string refCommentId: string refNickname?: string - refUsername?: string emojis?: SnsCommentEmoji[] } -export interface SnsLikeDetail { - nickname: string - username?: string - wxid?: string - alias?: string - wechatId?: string - remark?: string - nickName?: string - displayName: string - avatarUrl?: string - source: 'xml' | 'legacy' -} - -export interface SnsCommentDetail { - id: string - nickname: string - username?: string - wxid?: string - alias?: string - wechatId?: string - remark?: string - nickName?: string - displayName: string - avatarUrl?: string - content: string - refCommentId: string - refNickname?: string - refUsername?: string - refWxid?: string - refAlias?: string - refWechatId?: string - refRemark?: string - refNickName?: string - refDisplayName?: string - refAvatarUrl?: string - emojis?: SnsCommentEmoji[] - source: 'xml' | 'legacy' -} - export interface SnsPost { id: string tid?: string // 数据库主键(雪花 ID),用于精确删除 @@ -87,8 +46,6 @@ export interface SnsPost { media: SnsMedia[] likes: string[] comments: SnsComment[] - likesDetail?: SnsLikeDetail[] - commentsDetail?: SnsCommentDetail[] rawXml?: string linkTitle?: string linkUrl?: string