feat(export): add clickable sns count column in session list

This commit is contained in:
aits2026
2026-03-05 19:43:11 +08:00
parent c625756ab4
commit 2eff82891e
2 changed files with 89 additions and 12 deletions

View File

@@ -1559,6 +1559,38 @@
color: var(--text-tertiary); color: var(--text-tertiary);
} }
.row-sns-metric-btn {
border: none;
background: transparent;
margin: 0;
padding: 0;
min-height: 14px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 12px;
line-height: 1.2;
color: var(--primary);
font-variant-numeric: tabular-nums;
cursor: pointer;
&:hover {
color: var(--primary-hover);
text-decoration: underline;
text-underline-offset: 2px;
}
&:focus-visible {
outline: 2px solid color-mix(in srgb, var(--primary) 48%, transparent);
outline-offset: 2px;
border-radius: 4px;
}
&.loading {
color: var(--text-tertiary);
}
}
.row-message-stats { .row-message-stats {
width: 100%; width: 100%;
display: flex; display: flex;

View File

@@ -2158,16 +2158,7 @@ function ExportPage() {
setSessionSnsTimelineStatsLoading(false) setSessionSnsTimelineStatsLoading(false)
}, []) }, [])
const openSessionSnsTimeline = useCallback(() => { const openSessionSnsTimelineByTarget = useCallback((target: SessionSnsTimelineTarget) => {
const normalizedSessionId = String(sessionDetail?.wxid || '').trim()
if (!isSingleContactSession(normalizedSessionId) || !sessionDetail) return
const target: SessionSnsTimelineTarget = {
username: normalizedSessionId,
displayName: sessionDetail.displayName || sessionDetail.remark || sessionDetail.nickName || normalizedSessionId,
avatarUrl: sessionDetail.avatarUrl
}
setSessionSnsTimelineTarget(target) setSessionSnsTimelineTarget(target)
setSessionSnsTimelinePosts([]) setSessionSnsTimelinePosts([])
setSessionSnsTimelineHasMore(false) setSessionSnsTimelineHasMore(false)
@@ -2175,7 +2166,7 @@ function ExportPage() {
setSessionSnsTimelineLoading(false) setSessionSnsTimelineLoading(false)
if (snsUserPostCountsStatus === 'ready') { if (snsUserPostCountsStatus === 'ready') {
const count = Number(snsUserPostCounts[normalizedSessionId] || 0) const count = Number(snsUserPostCounts[target.username] || 0)
setSessionSnsTimelineTotalPosts(Number.isFinite(count) ? Math.max(0, Math.floor(count)) : 0) setSessionSnsTimelineTotalPosts(Number.isFinite(count) ? Math.max(0, Math.floor(count)) : 0)
setSessionSnsTimelineStatsLoading(false) setSessionSnsTimelineStatsLoading(false)
} else { } else {
@@ -2188,11 +2179,33 @@ function ExportPage() {
}, [ }, [
loadSessionSnsTimelinePosts, loadSessionSnsTimelinePosts,
loadSnsUserPostCounts, loadSnsUserPostCounts,
sessionDetail,
snsUserPostCounts, snsUserPostCounts,
snsUserPostCountsStatus snsUserPostCountsStatus
]) ])
const openSessionSnsTimeline = useCallback(() => {
const normalizedSessionId = String(sessionDetail?.wxid || '').trim()
if (!isSingleContactSession(normalizedSessionId) || !sessionDetail) return
const target: SessionSnsTimelineTarget = {
username: normalizedSessionId,
displayName: sessionDetail.displayName || sessionDetail.remark || sessionDetail.nickName || normalizedSessionId,
avatarUrl: sessionDetail.avatarUrl
}
openSessionSnsTimelineByTarget(target)
}, [openSessionSnsTimelineByTarget, sessionDetail])
const openContactSnsTimeline = useCallback((contact: ContactInfo) => {
const normalizedSessionId = String(contact?.username || '').trim()
if (!isSingleContactSession(normalizedSessionId)) return
openSessionSnsTimelineByTarget({
username: normalizedSessionId,
displayName: contact.displayName || contact.remark || contact.nickname || normalizedSessionId,
avatarUrl: contact.avatarUrl
})
}, [openSessionSnsTimelineByTarget])
const loadMoreSessionSnsTimeline = useCallback(() => { const loadMoreSessionSnsTimeline = useCallback(() => {
if (!sessionSnsTimelineTarget || sessionSnsTimelineLoading || sessionSnsTimelineLoadingMore || !sessionSnsTimelineHasMore) return if (!sessionSnsTimelineTarget || sessionSnsTimelineLoading || sessionSnsTimelineLoadingMore || !sessionSnsTimelineHasMore) return
void loadSessionSnsTimelinePosts(sessionSnsTimelineTarget, { reset: false }) void loadSessionSnsTimelinePosts(sessionSnsTimelineTarget, { reset: false })
@@ -4058,6 +4071,9 @@ function ExportPage() {
if (activeTab === 'former_friend') return '曾经的好友' if (activeTab === 'former_friend') return '曾经的好友'
return '公众号' return '公众号'
}, [activeTab]) }, [activeTab])
const shouldShowSnsColumn = useMemo(() => (
activeTab === 'private' || activeTab === 'former_friend'
), [activeTab])
const sessionRowByUsername = useMemo(() => { const sessionRowByUsername = useMemo(() => {
const map = new Map<string, SessionRow>() const map = new Map<string, SessionRow>()
@@ -5004,6 +5020,10 @@ function ExportPage() {
const voiceMetric = metricToDisplay(mediaMetric?.voiceMessages) const voiceMetric = metricToDisplay(mediaMetric?.voiceMessages)
const imageMetric = metricToDisplay(mediaMetric?.imageMessages) const imageMetric = metricToDisplay(mediaMetric?.imageMessages)
const videoMetric = metricToDisplay(mediaMetric?.videoMessages) const videoMetric = metricToDisplay(mediaMetric?.videoMessages)
const isSnsCountLoading = snsUserPostCountsStatus === 'loading' || snsUserPostCountsStatus === 'idle'
const snsRawCount = Number(snsUserPostCounts[contact.username] || 0)
const snsCount = Number.isFinite(snsRawCount) ? Math.max(0, Math.floor(snsRawCount)) : 0
const supportsSnsTimeline = isSingleContactSession(contact.username)
const openChatLabel = contact.type === 'friend' const openChatLabel = contact.type === 'friend'
? '打开私聊' ? '打开私聊'
: contact.type === 'group' : contact.type === 'group'
@@ -5086,6 +5106,24 @@ function ExportPage() {
: videoMetric.text} : videoMetric.text}
</strong> </strong>
</div> </div>
{shouldShowSnsColumn && (
<div className="row-media-metric">
{supportsSnsTimeline ? (
<button
type="button"
className={`row-sns-metric-btn ${isSnsCountLoading ? 'loading' : ''}`}
title={`查看 ${contact.displayName || contact.username} 的朋友圈`}
onClick={() => openContactSnsTimeline(contact)}
>
{isSnsCountLoading
? <Loader2 size={12} className="spin row-media-metric-icon" aria-label="朋友圈统计加载中" />
: `${snsCount.toLocaleString('zh-CN')}`}
</button>
) : (
<strong className="row-media-metric-value">--</strong>
)}
</div>
)}
<div className="row-action-cell"> <div className="row-action-cell">
<div className="row-action-main"> <div className="row-action-main">
<button <button
@@ -5121,6 +5159,7 @@ function ExportPage() {
}, [ }, [
lastExportBySession, lastExportBySession,
nowTick, nowTick,
openContactSnsTimeline,
openSessionDetail, openSessionDetail,
openSingleExport, openSingleExport,
queuedSessionIds, queuedSessionIds,
@@ -5131,6 +5170,9 @@ function ExportPage() {
sessionMessageCounts, sessionMessageCounts,
sessionRowByUsername, sessionRowByUsername,
showSessionDetailPanel, showSessionDetailPanel,
shouldShowSnsColumn,
snsUserPostCounts,
snsUserPostCountsStatus,
toggleSelectSession toggleSelectSession
]) ])
const handleContactsListWheelCapture = useCallback((event: WheelEvent<HTMLDivElement>) => { const handleContactsListWheelCapture = useCallback((event: WheelEvent<HTMLDivElement>) => {
@@ -5395,6 +5437,9 @@ function ExportPage() {
<span className="contacts-list-header-media"></span> <span className="contacts-list-header-media"></span>
<span className="contacts-list-header-media"></span> <span className="contacts-list-header-media"></span>
<span className="contacts-list-header-media"></span> <span className="contacts-list-header-media"></span>
{shouldShowSnsColumn && (
<span className="contacts-list-header-media"></span>
)}
<span className="contacts-list-header-actions"> <span className="contacts-list-header-actions">
{selectedCount > 0 && ( {selectedCount > 0 && (
<> <>