diff --git a/src/pages/ExportPage.scss b/src/pages/ExportPage.scss index 132b957..978483d 100644 --- a/src/pages/ExportPage.scss +++ b/src/pages/ExportPage.scss @@ -858,6 +858,7 @@ } .table-wrap { + --contacts-select-col-width: 34px; --contacts-message-col-width: 120px; --contacts-action-col-width: 280px; overflow: visible; @@ -993,6 +994,13 @@ flex-shrink: 0; } + .contacts-list-header-select { + width: var(--contacts-select-col-width); + min-width: var(--contacts-select-col-width); + text-align: center; + flex-shrink: 0; + } + .contacts-list-header-main { flex: 1; min-width: 0; @@ -1020,10 +1028,76 @@ padding: 0 12px 12px; } + .contacts-selection-toolbar { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 12px 8px; + border-bottom: 1px solid color-mix(in srgb, var(--border-color) 85%, transparent); + background: color-mix(in srgb, var(--bg-primary) 88%, var(--bg-secondary)); + flex-wrap: wrap; + + .selection-toggle-btn, + .selection-clear-btn { + border: 1px solid var(--border-color); + border-radius: 8px; + background: var(--bg-secondary); + color: var(--text-secondary); + font-size: 12px; + padding: 6px 10px; + cursor: pointer; + + &:hover:not(:disabled) { + border-color: var(--text-tertiary); + color: var(--text-primary); + } + + &:disabled { + opacity: 0.65; + cursor: not-allowed; + } + } + + .selection-summary { + font-size: 12px; + color: var(--text-secondary); + font-weight: 600; + + &.muted { + color: var(--text-tertiary); + font-weight: 500; + } + } + + .selection-export-btn { + border: none; + border-radius: 8px; + padding: 6px 10px; + background: var(--primary); + color: #fff; + font-size: 12px; + cursor: pointer; + margin-left: auto; + + &:hover:not(:disabled) { + background: var(--primary-hover); + } + + &:disabled { + opacity: 0.65; + cursor: not-allowed; + } + } + } + .contact-row { position: static; height: auto; padding-bottom: 4px; + + &.selected .contact-item { + background: rgba(var(--primary-rgb), 0.08); + } } .contact-item { @@ -1042,6 +1116,14 @@ } } + .row-select-cell { + width: var(--contacts-select-col-width); + min-width: var(--contacts-select-col-width); + display: flex; + justify-content: center; + flex-shrink: 0; + } + .contact-avatar { width: 44px; height: 44px; @@ -2160,6 +2242,14 @@ padding: 8px 10px 6px; } + .table-wrap .contacts-selection-toolbar { + padding: 8px 10px 6px; + + .selection-export-btn { + margin-left: 0; + } + } + .table-wrap .contacts-list { padding: 0 10px 10px; } diff --git a/src/pages/ExportPage.tsx b/src/pages/ExportPage.tsx index 0b15071..f98b569 100644 --- a/src/pages/ExportPage.tsx +++ b/src/pages/ExportPage.tsx @@ -2053,24 +2053,6 @@ function ExportPage() { } }, [sessions, preselectSessionIds]) - const visibleSessions = useMemo(() => { - const keyword = searchKeyword.trim().toLowerCase() - return sessions - .filter((session) => { - if (session.kind !== activeTab) return false - if (!keyword) return true - return ( - (session.displayName || '').toLowerCase().includes(keyword) || - session.username.toLowerCase().includes(keyword) - ) - }) - .sort((a, b) => { - const latestA = a.sortTimestamp || a.lastTimestamp || 0 - const latestB = b.sortTimestamp || b.lastTimestamp || 0 - return latestB - latestA - }) - }, [sessions, activeTab, searchKeyword]) - const selectedCount = selectedSessions.size const toggleSelectSession = (sessionId: string) => { @@ -2088,7 +2070,9 @@ function ExportPage() { } const toggleSelectAllVisible = () => { - const visibleIds = visibleSessions.filter(session => session.hasSession).map(session => session.username) + const visibleIds = filteredContacts + .filter(contact => sessionRowByUsername.get(contact.username)?.hasSession) + .map(contact => contact.username) if (visibleIds.length === 0) return setSelectedSessions(prev => { @@ -3461,13 +3445,23 @@ function ExportPage() { } const visibleSelectedCount = useMemo(() => { - const visibleSet = new Set(visibleSessions.map(session => session.username)) + const visibleSet = new Set( + filteredContacts + .filter(contact => sessionRowByUsername.get(contact.username)?.hasSession) + .map(contact => contact.username) + ) let count = 0 for (const id of selectedSessions) { if (visibleSet.has(id)) count += 1 } return count - }, [visibleSessions, selectedSessions]) + }, [filteredContacts, selectedSessions, sessionRowByUsername]) + const visibleSelectableCount = useMemo(() => ( + filteredContacts.reduce((count, contact) => ( + sessionRowByUsername.get(contact.username)?.hasSession ? count + 1 : count + ), 0) + ), [filteredContacts, sessionRowByUsername]) + const isAllVisibleSelected = visibleSelectableCount > 0 && visibleSelectedCount === visibleSelectableCount const canCreateTask = exportDialog.scope === 'sns' ? Boolean(exportFolder) @@ -3522,6 +3516,7 @@ function ExportPage() { filteredContacts.map((contact) => { 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 recent = canExport ? formatRecentExportTime(lastExportBySession[contact.username], nowTick) : '' @@ -3536,9 +3531,20 @@ function ExportPage() { return (
+
+ +
{contact.avatarUrl ? ( @@ -3611,10 +3617,12 @@ function ExportPage() { openSingleExport, queuedSessionIds, runningSessionIds, + selectedSessions, sessionDetail?.wxid, sessionMessageCounts, sessionRowByUsername, - showSessionDetailPanel + showSessionDetailPanel, + toggleSelectSession ]) const chooseExportFolder = useCallback(async () => { const result = await window.electronAPI.dialog.openFile({ @@ -3868,7 +3876,47 @@ function ExportPage() {
) : ( <> +
+ + + 已选 {selectedCount} 项 + + 当前筛选 {visibleSelectedCount}/{visibleSelectableCount} + + + +
+ 选择 联系人(头像/名称/微信号) 总消息数 操作