diff --git a/src/pages/ExportPage.scss b/src/pages/ExportPage.scss index 93a49e5..35a0472 100644 --- a/src/pages/ExportPage.scss +++ b/src/pages/ExportPage.scss @@ -2280,6 +2280,103 @@ color: var(--text-secondary); } + .sns-dialog-header-actions { + display: flex; + align-items: flex-start; + gap: 8px; + flex-shrink: 0; + } + + .sns-dialog-rank-switch { + position: relative; + display: inline-flex; + align-items: center; + gap: 6px; + } + + .sns-dialog-rank-btn { + border: 1px solid var(--border-color); + border-radius: 8px; + background: var(--bg-primary); + color: var(--text-secondary); + height: 28px; + padding: 0 10px; + font-size: 12px; + line-height: 1; + cursor: pointer; + white-space: nowrap; + + &:hover { + color: var(--text-primary); + border-color: color-mix(in srgb, var(--primary) 42%, var(--border-color)); + } + + &.active { + color: var(--primary); + border-color: color-mix(in srgb, var(--primary) 52%, var(--border-color)); + background: color-mix(in srgb, var(--primary) 10%, var(--bg-primary)); + } + } + + .sns-dialog-rank-panel { + position: absolute; + top: calc(100% + 8px); + right: 0; + width: 248px; + max-height: 220px; + overflow-y: auto; + border: 1px solid color-mix(in srgb, var(--primary) 30%, var(--border-color)); + border-radius: 10px; + background: var(--bg-primary-solid, #fff); + box-shadow: 0 14px 26px rgba(0, 0, 0, 0.18); + padding: 8px; + z-index: 12; + } + + .sns-dialog-rank-empty { + font-size: 12px; + color: var(--text-tertiary); + line-height: 1.5; + text-align: center; + padding: 6px 0; + } + + .sns-dialog-rank-row { + display: grid; + grid-template-columns: 20px minmax(0, 1fr) auto; + align-items: center; + gap: 8px; + min-height: 28px; + padding: 0 4px; + border-radius: 7px; + + &:hover { + background: var(--bg-hover); + } + } + + .sns-dialog-rank-index { + font-size: 12px; + color: var(--text-tertiary); + text-align: right; + font-variant-numeric: tabular-nums; + } + + .sns-dialog-rank-name { + font-size: 12px; + color: var(--text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .sns-dialog-rank-count { + font-size: 12px; + color: var(--text-secondary); + font-variant-numeric: tabular-nums; + white-space: nowrap; + } + .close-btn { border: none; background: transparent; @@ -3254,6 +3351,21 @@ padding: 12px; } + .sns-dialog-header-actions { + gap: 6px; + } + + .sns-dialog-rank-btn { + height: 26px; + padding: 0 8px; + font-size: 11px; + } + + .sns-dialog-rank-panel { + width: min(78vw, 232px); + max-height: 190px; + } + .sns-dialog-tip { padding: 10px 12px; line-height: 1.55; diff --git a/src/pages/ExportPage.tsx b/src/pages/ExportPage.tsx index 4db0498..4026c5a 100644 --- a/src/pages/ExportPage.tsx +++ b/src/pages/ExportPage.tsx @@ -47,6 +47,7 @@ type TaskStatus = 'queued' | 'running' | 'success' | 'error' type TaskScope = 'single' | 'multi' | 'content' | 'sns' type ContentType = 'text' | 'voice' | 'image' | 'video' | 'emoji' type ContentCardType = ContentType | 'sns' +type SnsRankMode = 'likes' | 'comments' type SessionLayout = 'shared' | 'per-session' @@ -1335,6 +1336,7 @@ function ExportPage() { const [sessionSnsTimelineHasMore, setSessionSnsTimelineHasMore] = useState(false) const [sessionSnsTimelineTotalPosts, setSessionSnsTimelineTotalPosts] = useState(null) const [sessionSnsTimelineStatsLoading, setSessionSnsTimelineStatsLoading] = useState(false) + const [sessionSnsRankMode, setSessionSnsRankMode] = useState(null) const [exportFolder, setExportFolder] = useState('') const [writeLayout, setWriteLayout] = useState('B') @@ -2153,6 +2155,7 @@ function ExportPage() { const closeSessionSnsTimeline = useCallback(() => { sessionSnsTimelineRequestTokenRef.current += 1 sessionSnsTimelineLoadingRef.current = false + setSessionSnsRankMode(null) setSessionSnsTimelineTarget(null) setSessionSnsTimelinePosts([]) setSessionSnsTimelineLoading(false) @@ -2163,6 +2166,7 @@ function ExportPage() { }, []) const openSessionSnsTimelineByTarget = useCallback((target: SessionSnsTimelineTarget) => { + setSessionSnsRankMode(null) setSessionSnsTimelineTarget(target) setSessionSnsTimelinePosts([]) setSessionSnsTimelineHasMore(false) @@ -2243,6 +2247,62 @@ function ExportPage() { sessionSnsTimelineTotalPosts ]) + const sessionSnsLikeRankings = useMemo(() => { + const rankMap = new Map() + for (const post of sessionSnsTimelinePosts) { + const createTime = Number(post?.createTime) || 0 + const likes = Array.isArray(post?.likes) ? post.likes : [] + for (const likeNameRaw of likes) { + const name = String(likeNameRaw || '').trim() || '未知用户' + const current = rankMap.get(name) + if (current) { + current.count += 1 + if (createTime > current.latestTime) current.latestTime = createTime + continue + } + rankMap.set(name, { name, count: 1, latestTime: createTime }) + } + } + return [...rankMap.values()].sort((a, b) => { + if (b.count !== a.count) return b.count - a.count + if (b.latestTime !== a.latestTime) return b.latestTime - a.latestTime + return a.name.localeCompare(b.name, 'zh-CN') + }) + }, [sessionSnsTimelinePosts]) + + const sessionSnsCommentRankings = useMemo(() => { + const rankMap = new Map() + for (const post of sessionSnsTimelinePosts) { + const createTime = Number(post?.createTime) || 0 + const comments = Array.isArray(post?.comments) ? post.comments : [] + for (const comment of comments) { + const name = String(comment?.nickname || '').trim() || '未知用户' + const current = rankMap.get(name) + if (current) { + current.count += 1 + if (createTime > current.latestTime) current.latestTime = createTime + continue + } + rankMap.set(name, { name, count: 1, latestTime: createTime }) + } + } + return [...rankMap.values()].sort((a, b) => { + if (b.count !== a.count) return b.count - a.count + if (b.latestTime !== a.latestTime) return b.latestTime - a.latestTime + return a.name.localeCompare(b.name, 'zh-CN') + }) + }, [sessionSnsTimelinePosts]) + + const toggleSessionSnsRankMode = useCallback((mode: SnsRankMode) => { + setSessionSnsRankMode((prev) => (prev === mode ? null : mode)) + }, []) + + const sessionSnsActiveRankings = useMemo(() => { + if (sessionSnsRankMode === 'likes') return sessionSnsLikeRankings + if (sessionSnsRankMode === 'comments') return sessionSnsCommentRankings + return [] + }, [sessionSnsCommentRankings, sessionSnsLikeRankings, sessionSnsRankMode]) + const mergeSessionContentMetrics = useCallback((input: Record) => { const entries = Object.entries(input) if (entries.length === 0) return @@ -6025,9 +6085,51 @@ function ExportPage() {
{renderSessionSnsTimelineStats()}
- +
+
+ + + {sessionSnsRankMode && ( +
+ {sessionSnsActiveRankings.length === 0 ? ( +
+ {sessionSnsRankMode === 'likes' ? '暂无点赞数据' : '暂无评论数据'} +
+ ) : ( + sessionSnsActiveRankings.slice(0, 10).map((item, index) => ( +
+ {index + 1} + {item.name} + + {item.count.toLocaleString('zh-CN')} + {sessionSnsRankMode === 'likes' ? '次' : '条'} + +
+ )) + )} +
+ )} +
+ +