mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
feat: unify chat analytics navigation
This commit is contained in:
18
src/App.tsx
18
src/App.tsx
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react'
|
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 TitleBar from './components/TitleBar'
|
||||||
import Sidebar from './components/Sidebar'
|
import Sidebar from './components/Sidebar'
|
||||||
import RouteGuard from './components/RouteGuard'
|
import RouteGuard from './components/RouteGuard'
|
||||||
@@ -8,6 +8,7 @@ import HomePage from './pages/HomePage'
|
|||||||
import ChatPage from './pages/ChatPage'
|
import ChatPage from './pages/ChatPage'
|
||||||
import AnalyticsPage from './pages/AnalyticsPage'
|
import AnalyticsPage from './pages/AnalyticsPage'
|
||||||
import AnalyticsWelcomePage from './pages/AnalyticsWelcomePage'
|
import AnalyticsWelcomePage from './pages/AnalyticsWelcomePage'
|
||||||
|
import ChatAnalyticsHubPage from './pages/ChatAnalyticsHubPage'
|
||||||
import AnnualReportPage from './pages/AnnualReportPage'
|
import AnnualReportPage from './pages/AnnualReportPage'
|
||||||
import AnnualReportWindow from './pages/AnnualReportWindow'
|
import AnnualReportWindow from './pages/AnnualReportWindow'
|
||||||
import DualReportPage from './pages/DualReportPage'
|
import DualReportPage from './pages/DualReportPage'
|
||||||
@@ -37,6 +38,12 @@ import { GlobalSessionMonitor } from './components/GlobalSessionMonitor'
|
|||||||
import { BatchTranscribeGlobal } from './components/BatchTranscribeGlobal'
|
import { BatchTranscribeGlobal } from './components/BatchTranscribeGlobal'
|
||||||
import { BatchImageDecryptGlobal } from './components/BatchImageDecryptGlobal'
|
import { BatchImageDecryptGlobal } from './components/BatchImageDecryptGlobal'
|
||||||
|
|
||||||
|
function RouteStateRedirect({ to }: { to: string }) {
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
return <Navigate to={to} replace state={location.state} />
|
||||||
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
@@ -562,9 +569,12 @@ function App() {
|
|||||||
<Route path="/home" element={<HomePage />} />
|
<Route path="/home" element={<HomePage />} />
|
||||||
<Route path="/chat" element={<ChatPage />} />
|
<Route path="/chat" element={<ChatPage />} />
|
||||||
|
|
||||||
<Route path="/analytics" element={<AnalyticsWelcomePage />} />
|
<Route path="/analytics" element={<ChatAnalyticsHubPage />} />
|
||||||
<Route path="/analytics/view" element={<AnalyticsPage />} />
|
<Route path="/analytics/private" element={<AnalyticsWelcomePage />} />
|
||||||
<Route path="/group-analytics" element={<GroupAnalyticsPage />} />
|
<Route path="/analytics/private/view" element={<AnalyticsPage />} />
|
||||||
|
<Route path="/analytics/group" element={<GroupAnalyticsPage />} />
|
||||||
|
<Route path="/analytics/view" element={<RouteStateRedirect to="/analytics/private/view" />} />
|
||||||
|
<Route path="/group-analytics" element={<RouteStateRedirect to="/analytics/group" />} />
|
||||||
<Route path="/annual-report" element={<AnnualReportPage />} />
|
<Route path="/annual-report" element={<AnnualReportPage />} />
|
||||||
<Route path="/annual-report/view" element={<AnnualReportWindow />} />
|
<Route path="/annual-report/view" element={<AnnualReportWindow />} />
|
||||||
<Route path="/dual-report" element={<DualReportPage />} />
|
<Route path="/dual-report" element={<DualReportPage />} />
|
||||||
|
|||||||
95
src/components/ChatAnalysisHeader.scss
Normal file
95
src/components/ChatAnalysisHeader.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
65
src/components/ChatAnalysisHeader.tsx
Normal file
65
src/components/ChatAnalysisHeader.tsx
Normal file
@@ -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<ChatAnalysisMode, { label: string; path: string }> = {
|
||||||
|
private: {
|
||||||
|
label: '私聊分析',
|
||||||
|
path: '/analytics/private'
|
||||||
|
},
|
||||||
|
group: {
|
||||||
|
label: '群聊分析',
|
||||||
|
path: '/analytics/group'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChatAnalysisHeader({ currentMode }: ChatAnalysisHeaderProps) {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const currentLabel = MODE_CONFIG[currentMode].label
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="chat-analysis-header">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="chat-analysis-back"
|
||||||
|
onClick={() => navigate('/analytics')}
|
||||||
|
>
|
||||||
|
<ChevronLeft size={16} />
|
||||||
|
<span>返回聊天分析</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="chat-analysis-breadcrumb">
|
||||||
|
<span>聊天分析</span>
|
||||||
|
<span className="chat-analysis-breadcrumb-separator">/</span>
|
||||||
|
<span className="current">{currentLabel}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="chat-analysis-switcher" role="tablist" aria-label="聊天分析类型">
|
||||||
|
{(Object.entries(MODE_CONFIG) as Array<[ChatAnalysisMode, { label: string; path: string }]>).map(([mode, config]) => (
|
||||||
|
<button
|
||||||
|
key={mode}
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
aria-selected={mode === currentMode}
|
||||||
|
className={`chat-analysis-switcher-item ${mode === currentMode ? 'active' : ''}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (mode !== currentMode) {
|
||||||
|
navigate(config.path)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{config.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChatAnalysisHeader
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect, useRef } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import { NavLink, useLocation, useNavigate } from 'react-router-dom'
|
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 { useAppStore } from '../stores/appStore'
|
||||||
import * as configService from '../services/config'
|
import * as configService from '../services/config'
|
||||||
import { onExportSessionStatus, requestExportSessionStatus } from '../services/exportBridge'
|
import { onExportSessionStatus, requestExportSessionStatus } from '../services/exportBridge'
|
||||||
@@ -375,24 +375,14 @@ function Sidebar() {
|
|||||||
<span className="nav-label">通讯录</span>
|
<span className="nav-label">通讯录</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
||||||
{/* 私聊分析 */}
|
{/* 聊天分析 */}
|
||||||
<NavLink
|
<NavLink
|
||||||
to="/analytics"
|
to="/analytics"
|
||||||
className={`nav-item ${isActive('/analytics') ? 'active' : ''}`}
|
className={`nav-item ${isActive('/analytics') ? 'active' : ''}`}
|
||||||
title={collapsed ? '私聊分析' : undefined}
|
title={collapsed ? '聊天分析' : undefined}
|
||||||
>
|
>
|
||||||
<span className="nav-icon"><BarChart3 size={20} /></span>
|
<span className="nav-icon"><BarChart3 size={20} /></span>
|
||||||
<span className="nav-label">私聊分析</span>
|
<span className="nav-label">聊天分析</span>
|
||||||
</NavLink>
|
|
||||||
|
|
||||||
{/* 群聊分析 */}
|
|
||||||
<NavLink
|
|
||||||
to="/group-analytics"
|
|
||||||
className={`nav-item ${isActive('/group-analytics') ? 'active' : ''}`}
|
|
||||||
title={collapsed ? '群聊分析' : undefined}
|
|
||||||
>
|
|
||||||
<span className="nav-icon"><Users size={20} /></span>
|
|
||||||
<span className="nav-label">群聊分析</span>
|
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
||||||
{/* 年度报告 */}
|
{/* 年度报告 */}
|
||||||
|
|||||||
@@ -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,
|
.loading-container,
|
||||||
.error-container {
|
.error-container {
|
||||||
|
|||||||
@@ -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 { useLocation } from 'react-router-dom'
|
||||||
import { Users, Clock, MessageSquare, Send, Inbox, Calendar, Loader2, RefreshCw, Medal, UserMinus, Search, X } from 'lucide-react'
|
import { Users, Clock, MessageSquare, Send, Inbox, Calendar, Loader2, RefreshCw, Medal, UserMinus, Search, X } from 'lucide-react'
|
||||||
import ReactECharts from 'echarts-for-react'
|
import ReactECharts from 'echarts-for-react'
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
} from '../services/backgroundTaskMonitor'
|
} from '../services/backgroundTaskMonitor'
|
||||||
import './AnalyticsPage.scss'
|
import './AnalyticsPage.scss'
|
||||||
import { Avatar } from '../components/Avatar'
|
import { Avatar } from '../components/Avatar'
|
||||||
|
import ChatAnalysisHeader from '../components/ChatAnalysisHeader'
|
||||||
|
|
||||||
interface ExcludeCandidate {
|
interface ExcludeCandidate {
|
||||||
username: string
|
username: string
|
||||||
@@ -416,8 +417,15 @@ function AnalyticsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderPageShell = (content: ReactNode) => (
|
||||||
|
<div className="analytics-page-shell">
|
||||||
|
<ChatAnalysisHeader currentMode="private" />
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
if (isLoading && !isLoaded) {
|
if (isLoading && !isLoaded) {
|
||||||
return (
|
return renderPageShell(
|
||||||
<div className="loading-container">
|
<div className="loading-container">
|
||||||
<Loader2 size={48} className="spin" />
|
<Loader2 size={48} className="spin" />
|
||||||
<p className="loading-status">{loadingStatus}</p>
|
<p className="loading-status">{loadingStatus}</p>
|
||||||
@@ -430,7 +438,7 @@ function AnalyticsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (error && !isLoaded && isNoSessionError && excludedUsernames.size > 0) {
|
if (error && !isLoaded && isNoSessionError && excludedUsernames.size > 0) {
|
||||||
return (
|
return renderPageShell(
|
||||||
<div className="error-container">
|
<div className="error-container">
|
||||||
<p>{error}</p>
|
<p>{error}</p>
|
||||||
<div className="error-actions">
|
<div className="error-actions">
|
||||||
@@ -446,11 +454,16 @@ function AnalyticsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (error && !isLoaded) {
|
if (error && !isLoaded) {
|
||||||
return (<div className="error-container"><p>{error}</p><button className="btn btn-primary" onClick={() => loadData(true)}>重试</button></div>)
|
return renderPageShell(
|
||||||
|
<div className="error-container">
|
||||||
|
<p>{error}</p>
|
||||||
|
<button className="btn btn-primary" onClick={() => loadData(true)}>重试</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return renderPageShell(
|
||||||
<>
|
<>
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<h1>私聊分析</h1>
|
<h1>私聊分析</h1>
|
||||||
|
|||||||
@@ -1,13 +1,30 @@
|
|||||||
|
.analytics-entry-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.analytics-welcome-container {
|
.analytics-welcome-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 100%;
|
min-height: 0;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
animation: fadeIn 0.4s ease-out;
|
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 {
|
.welcome-content {
|
||||||
text-align: center;
|
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 {
|
@keyframes fadeIn {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { BarChart2, History, RefreshCcw } from 'lucide-react'
|
import { BarChart2, History, RefreshCcw } from 'lucide-react'
|
||||||
import { useAnalyticsStore } from '../stores/analyticsStore'
|
import { useAnalyticsStore } from '../stores/analyticsStore'
|
||||||
|
import ChatAnalysisHeader from '../components/ChatAnalysisHeader'
|
||||||
import './AnalyticsWelcomePage.scss'
|
import './AnalyticsWelcomePage.scss'
|
||||||
|
|
||||||
function AnalyticsWelcomePage() {
|
function AnalyticsWelcomePage() {
|
||||||
@@ -14,11 +15,11 @@ function AnalyticsWelcomePage() {
|
|||||||
const { lastLoadTime } = useAnalyticsStore()
|
const { lastLoadTime } = useAnalyticsStore()
|
||||||
|
|
||||||
const handleLoadCache = () => {
|
const handleLoadCache = () => {
|
||||||
navigate('/analytics/view')
|
navigate('/analytics/private/view')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleNewAnalysis = () => {
|
const handleNewAnalysis = () => {
|
||||||
navigate('/analytics/view', { state: { forceRefresh: true } })
|
navigate('/analytics/private/view', { state: { forceRefresh: true } })
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatLastTime = (ts: number | null) => {
|
const formatLastTime = (ts: number | null) => {
|
||||||
@@ -27,33 +28,37 @@ function AnalyticsWelcomePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="analytics-welcome-container">
|
<div className="analytics-entry-page">
|
||||||
<div className="welcome-content">
|
<ChatAnalysisHeader currentMode="private" />
|
||||||
<div className="icon-wrapper">
|
|
||||||
<BarChart2 size={40} />
|
|
||||||
</div>
|
|
||||||
<h1>私聊数据分析</h1>
|
|
||||||
<p>
|
|
||||||
WeFlow 可以分析你的聊天记录,生成详细的统计报表。<br />
|
|
||||||
你可以选择加载上次的分析结果(速度快),或者开始新的分析(数据最新)。
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="action-cards">
|
<div className="analytics-welcome-container analytics-welcome-container--mode">
|
||||||
<button onClick={handleLoadCache}>
|
<div className="welcome-content">
|
||||||
<div className="card-icon">
|
<div className="icon-wrapper">
|
||||||
<History size={24} />
|
<BarChart2 size={40} />
|
||||||
</div>
|
</div>
|
||||||
<h3>加载缓存</h3>
|
<h1>私聊数据分析</h1>
|
||||||
<span>查看上次分析结果<br />(上次更新: {formatLastTime(lastLoadTime)})</span>
|
<p>
|
||||||
</button>
|
WeFlow 可以分析你的好友聊天记录,生成详细的统计报表。<br />
|
||||||
|
你可以选择加载上次的分析结果,或者重新开始一次新的私聊分析。
|
||||||
|
</p>
|
||||||
|
|
||||||
<button onClick={handleNewAnalysis}>
|
<div className="action-cards">
|
||||||
<div className="card-icon">
|
<button onClick={handleLoadCache}>
|
||||||
<RefreshCcw size={24} />
|
<div className="card-icon">
|
||||||
</div>
|
<History size={24} />
|
||||||
<h3>新的分析</h3>
|
</div>
|
||||||
<span>重新扫描并计算数据<br />(可能需要几分钟)</span>
|
<h3>加载缓存</h3>
|
||||||
</button>
|
<span>查看上次分析结果<br />(上次更新: {formatLastTime(lastLoadTime)})</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button onClick={handleNewAnalysis}>
|
||||||
|
<div className="card-icon">
|
||||||
|
<RefreshCcw size={24} />
|
||||||
|
</div>
|
||||||
|
<h3>新的分析</h3>
|
||||||
|
<span>重新扫描并计算数据<br />(可能需要几分钟)</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
123
src/pages/ChatAnalyticsHubPage.scss
Normal file
123
src/pages/ChatAnalyticsHubPage.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/pages/ChatAnalyticsHubPage.tsx
Normal file
59
src/pages/ChatAnalyticsHubPage.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="chat-analytics-hub-page">
|
||||||
|
<div className="chat-analytics-hub-content">
|
||||||
|
<div className="chat-analytics-hub-badge">
|
||||||
|
<BarChart3 size={16} />
|
||||||
|
<span>聊天分析</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1>选择你要进入的分析视角</h1>
|
||||||
|
<p className="chat-analytics-hub-desc">
|
||||||
|
私聊分析更适合看好友聊天统计和趋势,群聊分析则用于查看群成员、发言排行和活跃时段。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="chat-analytics-hub-grid">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="chat-analytics-entry-card"
|
||||||
|
onClick={() => navigate('/analytics/private')}
|
||||||
|
>
|
||||||
|
<div className="entry-card-icon">
|
||||||
|
<MessageSquare size={24} />
|
||||||
|
</div>
|
||||||
|
<div className="entry-card-header">
|
||||||
|
<h2>私聊分析</h2>
|
||||||
|
<ArrowRight size={18} />
|
||||||
|
</div>
|
||||||
|
<p>查看好友聊天统计、消息趋势、活跃时段与联系人排名。</p>
|
||||||
|
<span className="entry-card-cta">进入私聊分析</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="chat-analytics-entry-card"
|
||||||
|
onClick={() => navigate('/analytics/group')}
|
||||||
|
>
|
||||||
|
<div className="entry-card-icon group">
|
||||||
|
<Users size={24} />
|
||||||
|
</div>
|
||||||
|
<div className="entry-card-header">
|
||||||
|
<h2>群聊分析</h2>
|
||||||
|
<ArrowRight size={18} />
|
||||||
|
</div>
|
||||||
|
<p>查看群成员信息、发言排行、活跃时段和媒体内容统计。</p>
|
||||||
|
<span className="entry-card-cta">进入群聊分析</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChatAnalyticsHubPage
|
||||||
@@ -3323,7 +3323,7 @@ function ChatPage(props: ChatPageProps) {
|
|||||||
|
|
||||||
const handleGroupAnalytics = useCallback(() => {
|
const handleGroupAnalytics = useCallback(() => {
|
||||||
if (!currentSessionId || !isGroupChatSession(currentSessionId)) return
|
if (!currentSessionId || !isGroupChatSession(currentSessionId)) return
|
||||||
navigate('/group-analytics', {
|
navigate('/analytics/group', {
|
||||||
state: {
|
state: {
|
||||||
preselectGroupIds: [currentSessionId]
|
preselectGroupIds: [currentSessionId]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -463,6 +463,10 @@
|
|||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
.chat-analysis-header {
|
||||||
|
margin: 16px 16px 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.resize-handle {
|
.resize-handle {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Users, BarChart3, Clock, Image, Loader2, RefreshCw, Medal, Search, X, C
|
|||||||
import { Avatar } from '../components/Avatar'
|
import { Avatar } from '../components/Avatar'
|
||||||
import ReactECharts from 'echarts-for-react'
|
import ReactECharts from 'echarts-for-react'
|
||||||
import DateRangePicker from '../components/DateRangePicker'
|
import DateRangePicker from '../components/DateRangePicker'
|
||||||
|
import ChatAnalysisHeader from '../components/ChatAnalysisHeader'
|
||||||
import * as configService from '../services/config'
|
import * as configService from '../services/config'
|
||||||
import {
|
import {
|
||||||
finishBackgroundTask,
|
finishBackgroundTask,
|
||||||
@@ -1189,6 +1190,7 @@ function GroupAnalyticsPage() {
|
|||||||
{renderGroupList()}
|
{renderGroupList()}
|
||||||
<div className="resize-handle" onMouseDown={() => setIsResizing(true)} />
|
<div className="resize-handle" onMouseDown={() => setIsResizing(true)} />
|
||||||
<div className="detail-area">
|
<div className="detail-area">
|
||||||
|
<ChatAnalysisHeader currentMode="group" />
|
||||||
{renderDetailPanel()}
|
{renderDetailPanel()}
|
||||||
</div>
|
</div>
|
||||||
{renderMemberModal()}
|
{renderMemberModal()}
|
||||||
|
|||||||
Reference in New Issue
Block a user