diff --git a/src/App.tsx b/src/App.tsx index 416d3e1..36f2fc0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { Routes, Route, useNavigate, useLocation } from 'react-router-dom' +import { Routes, Route, Navigate, useNavigate, useLocation } from 'react-router-dom' import TitleBar from './components/TitleBar' import Sidebar from './components/Sidebar' import RouteGuard from './components/RouteGuard' @@ -8,6 +8,7 @@ import HomePage from './pages/HomePage' import ChatPage from './pages/ChatPage' import AnalyticsPage from './pages/AnalyticsPage' import AnalyticsWelcomePage from './pages/AnalyticsWelcomePage' +import ChatAnalyticsHubPage from './pages/ChatAnalyticsHubPage' import AnnualReportPage from './pages/AnnualReportPage' import AnnualReportWindow from './pages/AnnualReportWindow' import DualReportPage from './pages/DualReportPage' @@ -37,6 +38,12 @@ import { GlobalSessionMonitor } from './components/GlobalSessionMonitor' import { BatchTranscribeGlobal } from './components/BatchTranscribeGlobal' import { BatchImageDecryptGlobal } from './components/BatchImageDecryptGlobal' +function RouteStateRedirect({ to }: { to: string }) { + const location = useLocation() + + return +} + function App() { const navigate = useNavigate() const location = useLocation() @@ -562,9 +569,12 @@ function App() { } /> } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> + } /> + } /> } /> } /> } /> diff --git a/src/components/ChatAnalysisHeader.scss b/src/components/ChatAnalysisHeader.scss new file mode 100644 index 0000000..f5a127a --- /dev/null +++ b/src/components/ChatAnalysisHeader.scss @@ -0,0 +1,95 @@ +.chat-analysis-header { + display: flex; + flex-direction: column; + gap: 12px; + padding: 20px 24px 16px; + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 16px; + flex-shrink: 0; +} + +.chat-analysis-back { + width: fit-content; + display: inline-flex; + align-items: center; + gap: 6px; + padding: 0; + border: none; + background: transparent; + color: var(--text-secondary); + cursor: pointer; + transition: color 0.2s ease; + font-size: 13px; + font-weight: 500; + + &:hover { + color: var(--text-primary); + } +} + +.chat-analysis-breadcrumb { + display: flex; + align-items: center; + gap: 8px; + color: var(--text-tertiary); + font-size: 13px; + + .chat-analysis-breadcrumb-separator { + opacity: 0.6; + } + + .current { + color: var(--text-primary); + font-weight: 600; + } +} + +.chat-analysis-switcher { + display: inline-flex; + align-items: center; + gap: 6px; + width: fit-content; + padding: 4px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 999px; +} + +.chat-analysis-switcher-item { + min-width: 104px; + padding: 8px 16px; + border: none; + border-radius: 999px; + background: transparent; + color: var(--text-secondary); + cursor: pointer; + font-size: 13px; + font-weight: 500; + transition: all 0.2s ease; + + &:hover { + color: var(--text-primary); + background: var(--bg-hover); + } + + &.active { + background: var(--primary-light); + color: var(--primary); + } +} + +@media (max-width: 768px) { + .chat-analysis-header { + padding: 16px 18px 14px; + } + + .chat-analysis-switcher { + width: 100%; + } + + .chat-analysis-switcher-item { + flex: 1; + min-width: 0; + } +} diff --git a/src/components/ChatAnalysisHeader.tsx b/src/components/ChatAnalysisHeader.tsx new file mode 100644 index 0000000..e9097a5 --- /dev/null +++ b/src/components/ChatAnalysisHeader.tsx @@ -0,0 +1,65 @@ +import { ChevronLeft } from 'lucide-react' +import { useNavigate } from 'react-router-dom' +import './ChatAnalysisHeader.scss' + +export type ChatAnalysisMode = 'private' | 'group' + +interface ChatAnalysisHeaderProps { + currentMode: ChatAnalysisMode +} + +const MODE_CONFIG: Record = { + private: { + label: '私聊分析', + path: '/analytics/private' + }, + group: { + label: '群聊分析', + path: '/analytics/group' + } +} + +function ChatAnalysisHeader({ currentMode }: ChatAnalysisHeaderProps) { + const navigate = useNavigate() + const currentLabel = MODE_CONFIG[currentMode].label + + return ( +
+ + +
+ 聊天分析 + / + {currentLabel} +
+ +
+ {(Object.entries(MODE_CONFIG) as Array<[ChatAnalysisMode, { label: string; path: string }]>).map(([mode, config]) => ( + + ))} +
+
+ ) +} + +export default ChatAnalysisHeader diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 76547fb..af6f4f5 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useRef } from 'react' import { NavLink, useLocation, useNavigate } from 'react-router-dom' -import { Home, MessageSquare, BarChart3, Users, FileText, Settings, ChevronLeft, ChevronRight, Download, Aperture, UserCircle, Lock, LockOpen, ChevronUp, Trash2 } from 'lucide-react' +import { Home, MessageSquare, BarChart3, FileText, Settings, ChevronLeft, ChevronRight, Download, Aperture, UserCircle, Lock, LockOpen, ChevronUp, Trash2 } from 'lucide-react' import { useAppStore } from '../stores/appStore' import * as configService from '../services/config' import { onExportSessionStatus, requestExportSessionStatus } from '../services/exportBridge' @@ -375,24 +375,14 @@ function Sidebar() { 通讯录 - {/* 私聊分析 */} + {/* 聊天分析 */} - 私聊分析 - - - {/* 群聊分析 */} - - - 群聊分析 + 聊天分析 {/* 年度报告 */} diff --git a/src/pages/AnalyticsPage.scss b/src/pages/AnalyticsPage.scss index 73d7ca9..1135123 100644 --- a/src/pages/AnalyticsPage.scss +++ b/src/pages/AnalyticsPage.scss @@ -1,3 +1,19 @@ +.analytics-page-shell { + display: flex; + flex-direction: column; + gap: 16px; + min-height: 100%; + + .loading-container, + .error-container { + flex: 1; + } + + .page-header { + margin-bottom: 16px; + } +} + // 加载和错误状态 .loading-container, .error-container { diff --git a/src/pages/AnalyticsPage.tsx b/src/pages/AnalyticsPage.tsx index 977f183..5702bb2 100644 --- a/src/pages/AnalyticsPage.tsx +++ b/src/pages/AnalyticsPage.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react' +import { useState, useEffect, useCallback, type ReactNode } from 'react' import { useLocation } from 'react-router-dom' import { Users, Clock, MessageSquare, Send, Inbox, Calendar, Loader2, RefreshCw, Medal, UserMinus, Search, X } from 'lucide-react' import ReactECharts from 'echarts-for-react' @@ -12,6 +12,7 @@ import { } from '../services/backgroundTaskMonitor' import './AnalyticsPage.scss' import { Avatar } from '../components/Avatar' +import ChatAnalysisHeader from '../components/ChatAnalysisHeader' interface ExcludeCandidate { username: string @@ -416,8 +417,15 @@ function AnalyticsPage() { } } + const renderPageShell = (content: ReactNode) => ( +
+ + {content} +
+ ) + if (isLoading && !isLoaded) { - return ( + return renderPageShell(

{loadingStatus}

@@ -430,7 +438,7 @@ function AnalyticsPage() { } if (error && !isLoaded && isNoSessionError && excludedUsernames.size > 0) { - return ( + return renderPageShell(

{error}

@@ -446,11 +454,16 @@ function AnalyticsPage() { } if (error && !isLoaded) { - return (

{error}

) + return renderPageShell( +
+

{error}

+ +
+ ) } - return ( + return renderPageShell( <>

私聊分析

diff --git a/src/pages/AnalyticsWelcomePage.scss b/src/pages/AnalyticsWelcomePage.scss index 7a61efb..0e698bc 100644 --- a/src/pages/AnalyticsWelcomePage.scss +++ b/src/pages/AnalyticsWelcomePage.scss @@ -1,13 +1,30 @@ +.analytics-entry-page { + display: flex; + flex-direction: column; + gap: 16px; + min-height: 100%; +} + .analytics-welcome-container { display: flex; flex-direction: column; + flex: 1; align-items: center; justify-content: center; - height: 100%; + min-height: 0; padding: 40px; background: var(--bg-primary); color: var(--text-primary); animation: fadeIn 0.4s ease-out; + overflow-y: auto; + + &.analytics-welcome-container--mode { + border-radius: 20px; + border: 1px solid var(--border-color); + background: + radial-gradient(circle at top, rgba(7, 193, 96, 0.06), transparent 48%), + var(--bg-primary); + } .welcome-content { text-align: center; @@ -106,6 +123,18 @@ } } +@media (max-width: 768px) { + .analytics-welcome-container { + padding: 28px 18px; + + .welcome-content { + .action-cards { + grid-template-columns: 1fr; + } + } + } +} + @keyframes fadeIn { from { opacity: 0; @@ -116,4 +145,4 @@ opacity: 1; transform: translateY(0); } -} \ No newline at end of file +} diff --git a/src/pages/AnalyticsWelcomePage.tsx b/src/pages/AnalyticsWelcomePage.tsx index 38a5f9f..e5344ae 100644 --- a/src/pages/AnalyticsWelcomePage.tsx +++ b/src/pages/AnalyticsWelcomePage.tsx @@ -1,6 +1,7 @@ import { useNavigate } from 'react-router-dom' import { BarChart2, History, RefreshCcw } from 'lucide-react' import { useAnalyticsStore } from '../stores/analyticsStore' +import ChatAnalysisHeader from '../components/ChatAnalysisHeader' import './AnalyticsWelcomePage.scss' function AnalyticsWelcomePage() { @@ -14,11 +15,11 @@ function AnalyticsWelcomePage() { const { lastLoadTime } = useAnalyticsStore() const handleLoadCache = () => { - navigate('/analytics/view') + navigate('/analytics/private/view') } const handleNewAnalysis = () => { - navigate('/analytics/view', { state: { forceRefresh: true } }) + navigate('/analytics/private/view', { state: { forceRefresh: true } }) } const formatLastTime = (ts: number | null) => { @@ -27,33 +28,37 @@ function AnalyticsWelcomePage() { } return ( -
-
-
- -
-

私聊数据分析

-

- WeFlow 可以分析你的聊天记录,生成详细的统计报表。
- 你可以选择加载上次的分析结果(速度快),或者开始新的分析(数据最新)。 -

+
+ -
- +
+
+
+ +
+

私聊数据分析

+

+ WeFlow 可以分析你的好友聊天记录,生成详细的统计报表。
+ 你可以选择加载上次的分析结果,或者重新开始一次新的私聊分析。 +

- +
+ + + +
diff --git a/src/pages/ChatAnalyticsHubPage.scss b/src/pages/ChatAnalyticsHubPage.scss new file mode 100644 index 0000000..4d970cd --- /dev/null +++ b/src/pages/ChatAnalyticsHubPage.scss @@ -0,0 +1,123 @@ +.chat-analytics-hub-page { + min-height: 100%; + display: flex; + align-items: center; + justify-content: center; + padding: 40px 24px; +} + +.chat-analytics-hub-content { + width: min(860px, 100%); + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.chat-analytics-hub-badge { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 14px; + border-radius: 999px; + background: var(--primary-light); + color: var(--primary); + font-size: 13px; + font-weight: 600; +} + +.chat-analytics-hub-content h1 { + margin: 20px 0 12px; + font-size: 32px; + line-height: 1.2; + color: var(--text-primary); +} + +.chat-analytics-hub-desc { + max-width: 620px; + margin: 0 0 32px; + color: var(--text-secondary); + font-size: 15px; + line-height: 1.7; +} + +.chat-analytics-hub-grid { + width: 100%; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 20px; +} + +.chat-analytics-entry-card { + display: flex; + flex-direction: column; + align-items: flex-start; + text-align: left; + gap: 14px; + min-height: 260px; + padding: 28px; + border: 1px solid var(--border-color); + border-radius: 20px; + background: + linear-gradient(180deg, rgba(7, 193, 96, 0.08) 0%, rgba(7, 193, 96, 0.02) 100%), + var(--card-bg); + color: var(--text-primary); + cursor: pointer; + transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; + + &:hover { + transform: translateY(-4px); + border-color: rgba(7, 193, 96, 0.35); + box-shadow: 0 20px 36px rgba(7, 193, 96, 0.12); + } + + .entry-card-icon { + width: 52px; + height: 52px; + border-radius: 16px; + display: flex; + align-items: center; + justify-content: center; + background: rgba(7, 193, 96, 0.12); + color: #07c160; + + &.group { + background: rgba(24, 119, 242, 0.12); + color: #1877f2; + } + } + + .entry-card-header { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + } + + h2 { + margin: 0; + font-size: 24px; + line-height: 1.2; + } + + p { + margin: 0; + color: var(--text-secondary); + font-size: 14px; + line-height: 1.7; + } + + .entry-card-cta { + margin-top: auto; + color: var(--primary); + font-size: 13px; + font-weight: 600; + } +} + +@media (max-width: 900px) { + .chat-analytics-hub-grid { + grid-template-columns: 1fr; + } +} diff --git a/src/pages/ChatAnalyticsHubPage.tsx b/src/pages/ChatAnalyticsHubPage.tsx new file mode 100644 index 0000000..6c4456e --- /dev/null +++ b/src/pages/ChatAnalyticsHubPage.tsx @@ -0,0 +1,59 @@ +import { ArrowRight, BarChart3, MessageSquare, Users } from 'lucide-react' +import { useNavigate } from 'react-router-dom' +import './ChatAnalyticsHubPage.scss' + +function ChatAnalyticsHubPage() { + const navigate = useNavigate() + + return ( +
+
+
+ + 聊天分析 +
+ +

选择你要进入的分析视角

+

+ 私聊分析更适合看好友聊天统计和趋势,群聊分析则用于查看群成员、发言排行和活跃时段。 +

+ +
+ + + +
+
+
+ ) +} + +export default ChatAnalyticsHubPage diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index 6256d9b..357e1df 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -3323,7 +3323,7 @@ function ChatPage(props: ChatPageProps) { const handleGroupAnalytics = useCallback(() => { if (!currentSessionId || !isGroupChatSession(currentSessionId)) return - navigate('/group-analytics', { + navigate('/analytics/group', { state: { preselectGroupIds: [currentSessionId] } diff --git a/src/pages/GroupAnalyticsPage.scss b/src/pages/GroupAnalyticsPage.scss index b9cd651..867b4c2 100644 --- a/src/pages/GroupAnalyticsPage.scss +++ b/src/pages/GroupAnalyticsPage.scss @@ -463,6 +463,10 @@ background: var(--bg-secondary); border-radius: 16px; overflow: hidden; + + .chat-analysis-header { + margin: 16px 16px 0; + } } .resize-handle { diff --git a/src/pages/GroupAnalyticsPage.tsx b/src/pages/GroupAnalyticsPage.tsx index 4d2c65e..c706822 100644 --- a/src/pages/GroupAnalyticsPage.tsx +++ b/src/pages/GroupAnalyticsPage.tsx @@ -4,6 +4,7 @@ import { Users, BarChart3, Clock, Image, Loader2, RefreshCw, Medal, Search, X, C import { Avatar } from '../components/Avatar' import ReactECharts from 'echarts-for-react' import DateRangePicker from '../components/DateRangePicker' +import ChatAnalysisHeader from '../components/ChatAnalysisHeader' import * as configService from '../services/config' import { finishBackgroundTask, @@ -1189,6 +1190,7 @@ function GroupAnalyticsPage() { {renderGroupList()}
setIsResizing(true)} />
+ {renderDetailPanel()}
{renderMemberModal()}