From ec9c1bbbba218d77235a9c61af785aad7786656a Mon Sep 17 00:00:00 2001 From: xuncha <1658671838@qq.com> Date: Tue, 7 Apr 2026 19:09:01 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E6=9C=8B=E5=8F=8B=E5=9C=88=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E9=A1=B5=E6=96=B0=E5=A2=9E=E5=A4=9A=E9=80=89=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Sns/SnsFilterPanel.tsx | 26 +++++++++++++++ src/pages/SnsPage.scss | 46 +++++++++++++++++++++++++++ src/pages/SnsPage.tsx | 19 +++++++++++ 3 files changed, 91 insertions(+) diff --git a/src/components/Sns/SnsFilterPanel.tsx b/src/components/Sns/SnsFilterPanel.tsx index 654d543..127a91d 100644 --- a/src/components/Sns/SnsFilterPanel.tsx +++ b/src/components/Sns/SnsFilterPanel.tsx @@ -29,6 +29,7 @@ interface SnsFilterPanelProps { activeContactUsername?: string onOpenContactTimeline: (contact: Contact) => void onToggleContactSelected: (contact: Contact) => void + onToggleFilteredContacts: (usernames: string[], shouldSelect: boolean) => void onClearSelectedContacts: () => void onExportSelectedContacts: () => void } @@ -46,6 +47,7 @@ export const SnsFilterPanel: React.FC = ({ activeContactUsername, onOpenContactTimeline, onToggleContactSelected, + onToggleFilteredContacts, onClearSelectedContacts, onExportSelectedContacts }) => { @@ -57,6 +59,16 @@ export const SnsFilterPanel: React.FC = ({ () => new Set(selectedContactUsernames), [selectedContactUsernames] ) + const filteredContactUsernames = React.useMemo( + () => filteredContacts.map((contact) => contact.username), + [filteredContacts] + ) + const selectedFilteredCount = React.useMemo( + () => filteredContactUsernames.filter((username) => selectedContactLookup.has(username)).length, + [filteredContactUsernames, selectedContactLookup] + ) + const hasFilteredContacts = filteredContactUsernames.length > 0 + const allFilteredSelected = hasFilteredContacts && selectedFilteredCount === filteredContactUsernames.length const clearFilters = () => { setSearchKeyword('') @@ -128,6 +140,20 @@ export const SnsFilterPanel: React.FC = ({ )} +
+ + 当前 {filteredContactUsernames.length} 人,已选 {selectedFilteredCount} 人 + + +
+ {contactsCountProgress && contactsCountProgress.total > 0 && (
{contactsCountProgress.running diff --git a/src/pages/SnsPage.scss b/src/pages/SnsPage.scss index 46584cd..6dea0d6 100644 --- a/src/pages/SnsPage.scss +++ b/src/pages/SnsPage.scss @@ -1265,6 +1265,52 @@ } } + .contact-selection-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 10px 16px; + border-bottom: 1px dashed color-mix(in srgb, var(--border-color) 72%, transparent); + } + + .contact-selection-summary { + min-width: 0; + font-size: 11px; + color: var(--text-tertiary); + font-variant-numeric: tabular-nums; + } + + .contact-selection-toggle { + flex-shrink: 0; + border: 1px solid color-mix(in srgb, var(--primary) 16%, var(--border-color)); + background: color-mix(in srgb, var(--bg-primary) 84%, rgba(var(--primary-rgb), 0.08)); + color: var(--text-secondary); + border-radius: 999px; + padding: 5px 10px; + font-size: 12px; + font-weight: 600; + line-height: 1.2; + cursor: pointer; + transition: border-color 0.2s ease, background 0.2s ease, color 0.2s ease; + + &:hover:not(:disabled) { + border-color: color-mix(in srgb, var(--primary) 40%, var(--border-color)); + color: var(--primary); + } + + &.active { + border-color: color-mix(in srgb, var(--primary) 40%, var(--border-color)); + background: rgba(var(--primary-rgb), 0.12); + color: var(--primary); + } + + &:disabled { + opacity: 0.45; + cursor: not-allowed; + } + } + .contact-count-progress { padding: 8px 16px 10px; font-size: 11px; diff --git a/src/pages/SnsPage.tsx b/src/pages/SnsPage.tsx index b2fd499..b9bf6b0 100644 --- a/src/pages/SnsPage.tsx +++ b/src/pages/SnsPage.tsx @@ -1398,6 +1398,24 @@ export default function SnsPage() { setSelectedContactUsernames([]) }, []) + const toggleSelectFilteredContacts = useCallback((usernames: string[], shouldSelect: boolean) => { + const normalizedTargets = Array.from(new Set( + usernames + .map((username) => String(username || '').trim()) + .filter(Boolean) + )) + if (normalizedTargets.length === 0) return + + setSelectedContactUsernames((prev) => { + if (shouldSelect) { + const next = new Set(prev) + normalizedTargets.forEach((username) => next.add(username)) + return Array.from(next) + } + return prev.filter((username) => !normalizedTargets.includes(username)) + }) + }, []) + const openSelectedContactsExport = useCallback(() => { if (selectedContactUsernames.length === 0) return openExportDialog({ kind: 'selected', usernames: [...selectedContactUsernames] }) @@ -1783,6 +1801,7 @@ export default function SnsPage() { activeContactUsername={authorTimelineTarget?.username} onOpenContactTimeline={openContactTimeline} onToggleContactSelected={toggleContactSelected} + onToggleFilteredContacts={toggleSelectFilteredContacts} onClearSelectedContacts={clearSelectedContacts} onExportSelectedContacts={openSelectedContactsExport} /> From 88d41f6857e6921fc5b4e0c2339774ac573f1f46 Mon Sep 17 00:00:00 2001 From: xuncha <1658671838@qq.com> Date: Tue, 7 Apr 2026 19:09:16 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E9=A1=B5=E6=84=8F=E5=A4=96=E7=9A=84=E6=A8=AA=E5=90=91=E6=BB=91?= =?UTF-8?q?=E5=8A=A8=E6=9D=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/ExportPage.tsx | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/pages/ExportPage.tsx b/src/pages/ExportPage.tsx index dbfcfc1..ff4c86f 100644 --- a/src/pages/ExportPage.tsx +++ b/src/pages/ExportPage.tsx @@ -6601,19 +6601,15 @@ function ExportPage() { const taskQueuedCount = tasks.filter(task => task.status === 'queued').length const taskCenterAlertCount = taskRunningCount + taskQueuedCount const hasFilteredContacts = filteredContacts.length > 0 - const CONTACTS_ACTION_STICKY_WIDTH = 184 - const contactsTableMinWidth = useMemo(() => { - const baseWidth = 24 + 34 + 44 + 280 + 120 + (4 * 72) + CONTACTS_ACTION_STICKY_WIDTH + (8 * 12) - const snsWidth = shouldShowSnsColumn ? 72 + 12 : 0 - const mutualFriendsWidth = shouldShowMutualFriendsColumn ? 72 + 12 : 0 - return baseWidth + snsWidth + mutualFriendsWidth - }, [shouldShowMutualFriendsColumn, shouldShowSnsColumn]) + const optionalMetricColumnCount = (shouldShowSnsColumn ? 1 : 0) + (shouldShowMutualFriendsColumn ? 1 : 0) + const contactsMetricColumnCount = 4 + optionalMetricColumnCount + const contactsColumnGapCount = 6 + optionalMetricColumnCount const contactsTableStyle = useMemo(() => ( { - ['--contacts-table-min-width' as const]: `${contactsTableMinWidth}px` + ['--contacts-table-min-width' as const]: `calc((2 * var(--contacts-inline-padding)) + var(--contacts-left-sticky-width) + var(--contacts-message-col-width) + (${contactsMetricColumnCount} * var(--contacts-media-col-width)) + var(--contacts-actions-sticky-width) + (${contactsColumnGapCount} * var(--contacts-column-gap)))` } as CSSProperties - ), [contactsTableMinWidth]) - const hasContactsHorizontalOverflow = contactsHorizontalScrollMetrics.contentWidth - contactsHorizontalScrollMetrics.viewportWidth > 1 + ), [contactsColumnGapCount, contactsMetricColumnCount]) + const hasContactsHorizontalOverflow = contactsHorizontalScrollMetrics.contentWidth - contactsHorizontalScrollMetrics.viewportWidth > 4 const contactsBottomScrollbarInnerStyle = useMemo(() => ({ width: `${Math.max(contactsHorizontalScrollMetrics.contentWidth, contactsHorizontalScrollMetrics.viewportWidth)}px` }), [contactsHorizontalScrollMetrics.contentWidth, contactsHorizontalScrollMetrics.viewportWidth])