| 1 | /* |
| 2 | * @Author: nevin |
| 3 | * @Date: 2025-01-17 20:05:00 |
| 4 | * @LastEditTime: 2025-02-24 12:15:15 |
| 5 | * @LastEditors: nevin |
| 6 | * @Description: 数据页 |
| 7 | */ |
| 8 | import { useEffect, useState, useRef } from 'react'; |
| 9 | import './statistics.css'; |
| 10 | import { icpGetAccountDashboard, icpGetAccountStatistics } from '@/icp/account'; |
| 11 | import { DashboardData, StatisticsInfo } from './comment'; |
| 12 | import { Button, Card, Layout, Avatar, DatePicker, Spin } from 'antd'; |
| 13 | import { |
| 14 | InfoCircleOutlined, |
| 15 | DownloadOutlined, |
| 16 | SearchOutlined, |
| 17 | ReloadOutlined, |
| 18 | QuestionCircleOutlined, |
| 19 | } from '@ant-design/icons'; |
| 20 | import { Dayjs } from 'dayjs'; |
| 21 | import * as echarts from 'echarts'; |
| 22 | import dayjs from 'dayjs'; |
| 23 | import { message } from 'antd'; |
| 24 | import WebView from '@/components/WebView'; |
| 25 | |
| 26 | import douyinIcon from '../../assets/svgs/account/douyin.svg'; |
| 27 | import xhsIcon from '../../assets/svgs/account/xhs.svg'; |
| 28 | import wxSphIcon from '../../assets/svgs/account/wx-sph.svg'; |
| 29 | import ksIcon from '../../assets/svgs/account/ks.svg'; |
| 30 | |
| 31 | const { Content } = Layout; |
| 32 | |
| 33 | const Statistics = () => { |
| 34 | const [statisticsInfo, setStatisticsInfo] = |
| 35 | useState<Partial<StatisticsInfo>>(); |
| 36 | |
| 37 | const [dashboardData, setDashboardData] = useState<DashboardData[]>([]); |
| 38 | const [dashboardData7, setDashboardData7] = useState<any[]>([]); |
| 39 | |
| 40 | const [selectedDateRange, setSelectedDateRange] = useState< |
| 41 | [Dayjs | null, Dayjs | null] |
| 42 | >([dayjs().subtract(7, 'day'), dayjs().subtract(1, 'day')]); |
| 43 | |
| 44 | const chartRef = useRef<HTMLDivElement>(null); |
| 45 | const chartInstance = useRef<echarts.ECharts>(); |
| 46 | |
| 47 | // 添加选中的账户ID列表 |
| 48 | const [selectedAccounts, setSelectedAccounts] = useState<number[]>([]); |
| 49 | // 添加选中的指标类型 |
| 50 | const [selectedMetric, setSelectedMetric] = useState<string>('fans'); |
| 51 | |
| 52 | // 添加状态来控制WebView的显示 |
| 53 | const [examineVideoData, setExamineVideoData] = useState<{ |
| 54 | url: string; |
| 55 | account: any; |
| 56 | open: boolean; |
| 57 | }>({ |
| 58 | url: '', |
| 59 | account: null, |
| 60 | open: false, |
| 61 | }); |
| 62 | |
| 63 | const [loading, setLoading] = useState(true); |
| 64 | const [loadingMore, setLoadingMore] = useState(false); |
| 65 | |
| 66 | useEffect(() => { |
| 67 | getAccountStatistics(); |
| 68 | console.log('selectedAccounts', selectedAccounts); |
| 69 | }, []); |
| 70 | |
| 71 | useEffect(() => { |
| 72 | // 获取账户信息后立即调用 getAccountStatistics7 |
| 73 | console.log('###', 123); |
| 74 | getAccountStatistics7(); |
| 75 | }, [selectedAccounts]); |
| 76 | |
| 77 | // 初始化图表 |
| 78 | useEffect(() => { |
| 79 | if (chartRef.current) { |
| 80 | chartInstance.current = echarts.init(chartRef.current); |
| 81 | } |
| 82 | return () => { |
| 83 | chartInstance.current?.dispose(); |
| 84 | }; |
| 85 | }, []); |
| 86 | |
| 87 | // 在获取账户统计信息后,默认选中所有账户 |
| 88 | useEffect(() => { |
| 89 | if (statisticsInfo?.list) { |
| 90 | setSelectedAccounts(statisticsInfo.list.map((account) => account.id)); |
| 91 | } |
| 92 | }, [statisticsInfo]); |
| 93 | |
| 94 | // 更新图表数据 |
| 95 | useEffect(() => { |
| 96 | if (chartInstance.current) { |
| 97 | // 如果没有选中的账户或没有数据,显示空图表 |
| 98 | if (selectedAccounts.length === 0 || dashboardData7.length === 0) { |
| 99 | const emptyOption = { |
| 100 | title: { |
| 101 | text: '数据趋势', |
| 102 | left: 'center', |
| 103 | }, |
| 104 | grid: { |
| 105 | left: '3%', |
| 106 | right: '4%', |
| 107 | bottom: '15%', |
| 108 | top: '10%', |
| 109 | containLabel: true, |
| 110 | }, |
| 111 | xAxis: { |
| 112 | type: 'category', |
| 113 | data: [], |
| 114 | }, |
| 115 | yAxis: { |
| 116 | type: 'value', |
| 117 | }, |
| 118 | series: [], |
| 119 | }; |
| 120 | chartInstance.current.setOption(emptyOption, true); |
| 121 | return; |
| 122 | } |
| 123 | |
| 124 | // 获取所有日期 |
| 125 | const allDates = |
| 126 | dashboardData7[0]?.data.map((_: any, index: number) => { |
| 127 | const date = selectedDateRange[0]?.clone().add(index, 'day'); |
| 128 | return date?.format('YYYY-MM-DD'); |
| 129 | }) || []; |
| 130 | |
| 131 | // 平台类型对应的颜色 |
| 132 | const platformColors: Record<string, string> = { |
| 133 | douyin: '#183641', // 抖音 |
| 134 | xhs: '#FF2442', // 小红书 |
| 135 | wxSph: '#FA9A32', // 微信视频号 |
| 136 | KWAI: '#F64806', // 快手 |
| 137 | }; |
| 138 | |
| 139 | const option = { |
| 140 | title: { |
| 141 | text: '数据趋势', |
| 142 | left: 'center', |
| 143 | }, |
| 144 | tooltip: { |
| 145 | trigger: 'axis', |
| 146 | axisPointer: { |
| 147 | type: 'shadow', |
| 148 | }, |
| 149 | }, |
| 150 | legend: { |
| 151 | data: dashboardData7.map((item) => item.name), |
| 152 | bottom: 0, |
| 153 | textStyle: { |
| 154 | color: '#666', |
| 155 | }, |
| 156 | }, |
| 157 | grid: { |
| 158 | left: '3%', |
| 159 | right: '4%', |
| 160 | bottom: '15%', |
| 161 | top: '10%', |
| 162 | containLabel: true, |
| 163 | }, |
| 164 | xAxis: { |
| 165 | type: 'category', |
| 166 | data: allDates, |
| 167 | axisLabel: { |
| 168 | interval: 0, |
| 169 | rotate: 30, |
| 170 | color: '#666', |
| 171 | fontSize: 12, |
| 172 | }, |
| 173 | axisTick: { |
| 174 | alignWithLabel: true, |
| 175 | }, |
| 176 | }, |
| 177 | yAxis: { |
| 178 | type: 'value', |
| 179 | axisLabel: { |
| 180 | color: '#666', |
| 181 | fontSize: 12, |
| 182 | }, |
| 183 | }, |
| 184 | series: dashboardData7.map((account) => ({ |
| 185 | name: account.name, |
| 186 | type: 'bar', |
| 187 | data: account.data.map( |
| 188 | (item: Record<string, any>) => Number(item[selectedMetric]) || 0, |
| 189 | ), |
| 190 | barMaxWidth: 30, |
| 191 | itemStyle: { |
| 192 | color: platformColors[account.type] || '#a66ae4', // 根据账户类型设置颜色,默认使用紫色 |
| 193 | borderRadius: [4, 4, 0, 0], |
| 194 | }, |
| 195 | })), |
| 196 | }; |
| 197 | chartInstance.current.setOption(option, true); |
| 198 | } |
| 199 | }, [dashboardData7, selectedMetric]); |
| 200 | |
| 201 | // 获取日期数组 |
| 202 | const getDatesArray = (start: Dayjs | null, end: Dayjs | null) => { |
| 203 | if (!start || !end) return []; |
| 204 | const dates = []; |
| 205 | let curr = start; |
| 206 | while (curr <= end) { |
| 207 | dates.push(curr.format('YYYY-MM-DD')); |
| 208 | curr = curr.add(1, 'day'); |
| 209 | } |
| 210 | return dates; |
| 211 | }; |
| 212 | |
| 213 | // 获取指标趋势数据 |
| 214 | const getMetricTrend = (metric: string) => { |
| 215 | if (!selectedDateRange?.[0] || !selectedDateRange?.[1]) return []; |
| 216 | const dates = getDatesArray(selectedDateRange[0], selectedDateRange[1]); |
| 217 | return dates.map((date) => { |
| 218 | return dashboardData7.reduce((sum, item: any) => { |
| 219 | // 这里需要根据实际数据结构调整 |
| 220 | return sum + Number(item[metric] || 0); |
| 221 | }, 0); |
| 222 | }); |
| 223 | }; |
| 224 | |
| 225 | // 获取昨日新增总和 |
| 226 | const getTotalYesterdayIncrease = (metric: string) => { |
| 227 | return dashboardData.reduce((sum, item: any) => { |
| 228 | // 这里需要根据实际数据结构调整 |
| 229 | return Number(sum) + Number(item[`${metric}`] || 0); |
| 230 | }, 0); |
| 231 | }; |
| 232 | |
| 233 | // 获取日期范围内的总计值 |
| 234 | const getRangeTotalMetricValue = (metric: string) => { |
| 235 | if (!dashboardData7.length) return 0; |
| 236 | return dashboardData7.reduce((sum, account) => { |
| 237 | const accountSum = account.data.reduce( |
| 238 | (acc: number, item: Record<string, any>) => |
| 239 | acc + Number(item[metric] || 0), |
| 240 | 0, |
| 241 | ); |
| 242 | return sum + accountSum; |
| 243 | }, 0); |
| 244 | }; |
| 245 | |
| 246 | // 获取昨日数据总和 |
| 247 | const getYesterdayTotalValue = (metric: string) => { |
| 248 | return dashboardData.reduce( |
| 249 | (sum, item: any) => sum + Number(item[metric] || 0), |
| 250 | 0, |
| 251 | ); |
| 252 | }; |
| 253 | |
| 254 | // 导出数据 |
| 255 | const handleExportData = () => { |
| 256 | const data = dashboardData.map((item: any) => ({ |
| 257 | 账户ID: item.id, |
| 258 | 粉丝数: item.fans, |
| 259 | 播放数: item.read, |
| 260 | 评论数: item.comment, |
| 261 | 点赞数: item.like, |
| 262 | 分享数: item.collect, |
| 263 | 主页访问: item.forward, |
| 264 | })); |
| 265 | |
| 266 | const csvContent = |
| 267 | 'data:text/csv;charset=utf-8,' + |
| 268 | Object.keys(data[0]).join(',') + |
| 269 | '\n' + |
| 270 | data.map((row) => Object.values(row).join(',')).join('\n'); |
| 271 | |
| 272 | const encodedUri = encodeURI(csvContent); |
| 273 | const link = document.createElement('a'); |
| 274 | link.setAttribute('href', encodedUri); |
| 275 | link.setAttribute( |
| 276 | 'download', |
| 277 | `数据统计_${new Date().toLocaleDateString()}.csv`, |
| 278 | ); |
| 279 | document.body.appendChild(link); |
| 280 | link.click(); |
| 281 | document.body.removeChild(link); |
| 282 | }; |
| 283 | |
| 284 | async function getAccountStatistics() { |
| 285 | setLoading(true); |
| 286 | setDashboardData([]); |
| 287 | const res: StatisticsInfo = await icpGetAccountStatistics(); |
| 288 | console.log('zhanghu res:', res); |
| 289 | const list = res.list.filter((account: any) => account.status === 0); |
| 290 | setStatisticsInfo({ ...res, list }); |
| 291 | // 获取到账号列表后,遍历获取每个账号的看板数据 |
| 292 | if (res.list && res.list.length > 0) { |
| 293 | for (const account of res.list) { |
| 294 | if (account.status === 1) continue; |
| 295 | const dashboardRes = await getAccountDashboard(account.id); |
| 296 | console.log('dashboardRes:', dashboardRes); |
| 297 | // const accountInfo = await icpGetAccountInfo(account.type, account.uid); |
| 298 | setDashboardData((prev) => [ |
| 299 | ...prev, |
| 300 | { ...dashboardRes[0], id: account.id }, |
| 301 | ]); |
| 302 | } |
| 303 | } |
| 304 | setLoading(false); |
| 305 | } |
| 306 | |
| 307 | async function getAccountStatistics7() { |
| 308 | setLoading(true); |
| 309 | setDashboardData7([]); |
| 310 | const res: any = statisticsInfo; |
| 311 | const dataAll = []; |
| 312 | |
| 313 | try { |
| 314 | // 获取到账号列表后,遍历获取每个账号的看板数据 |
| 315 | if (res?.list && res.list.length > 0) { |
| 316 | // 只获取选中账户的数据 |
| 317 | const selectedAccountsList = res.list.filter((account: any) => |
| 318 | selectedAccounts.includes(account.id), |
| 319 | ); |
| 320 | |
| 321 | for (const account of selectedAccountsList) { |
| 322 | const startDate = selectedDateRange[0]?.format('YYYY-MM-DD'); |
| 323 | const endDate = selectedDateRange[1]?.format('YYYY-MM-DD'); |
| 324 | const dashboardRes = await icpGetAccountDashboard(account.id, [ |
| 325 | startDate, |
| 326 | endDate, |
| 327 | ]); |
| 328 | |
| 329 | const datas = { |
| 330 | id: account.id, |
| 331 | type: account.type, |
| 332 | name: account.nickname, |
| 333 | data: dashboardRes, |
| 334 | }; |
| 335 | dataAll.push(datas); |
| 336 | } |
| 337 | |
| 338 | console.log('所有数据', dataAll); |
| 339 | |
| 340 | // 所有数据获取完成后再一次性更新 |
| 341 | setDashboardData7(dataAll); |
| 342 | } |
| 343 | } catch (error) { |
| 344 | console.error('获取数据失败:', error); |
| 345 | setLoading(false); |
| 346 | } finally { |
| 347 | setLoading(false); |
| 348 | } |
| 349 | } |
| 350 | |
| 351 | /** |
| 352 | * 获取账号看板信息 |
| 353 | * @param id |
| 354 | */ |
| 355 | async function getAccountDashboard(id: number) { |
| 356 | const res = await icpGetAccountDashboard(id); |
| 357 | return res; |
| 358 | } |
| 359 | |
| 360 | // 渲染数据卡片 |
| 361 | const renderMetricCard = ( |
| 362 | title: string, |
| 363 | value: number | string, |
| 364 | icon: string, |
| 365 | ) => ( |
| 366 | <div className="p-4 bg-white rounded-lg shadow-sm"> |
| 367 | <div className="flex items-center justify-between mb-2"> |
| 368 | <div className="text-sm text-gray-600">{title}</div> |
| 369 | <span className="text-lg text-gray-400">{icon}</span> |
| 370 | </div> |
| 371 | <div className="text-2xl font-semibold text-[#a66ae4]"> |
| 372 | {value.toLocaleString()} |
| 373 | </div> |
| 374 | </div> |
| 375 | ); |
| 376 | |
| 377 | // 获取一个月前的日期 |
| 378 | const disabledDate = (current: Dayjs) => { |
| 379 | const oneMonthAgo = dayjs().subtract(1, 'month'); |
| 380 | const today = dayjs(); |
| 381 | return current && (current < oneMonthAgo || current > today); |
| 382 | }; |
| 383 | |
| 384 | // 处理账户选择 |
| 385 | const toggleAccountSelection = async (accountId: number) => { |
| 386 | const newSelectedAccounts = selectedAccounts.includes(accountId) |
| 387 | ? selectedAccounts.filter((id) => id !== accountId) |
| 388 | : [...selectedAccounts, accountId]; |
| 389 | |
| 390 | setSelectedAccounts(newSelectedAccounts); |
| 391 | |
| 392 | // 重新获取数据 |
| 393 | setDashboardData7([]); |
| 394 | const res: any = statisticsInfo; |
| 395 | const dataAll = []; |
| 396 | |
| 397 | if (res.list && res.list.length > 0) { |
| 398 | // 只获取选中账户的数据 |
| 399 | const selectedAccountsList = res.list.filter((account: any) => |
| 400 | newSelectedAccounts.includes(account.id), |
| 401 | ); |
| 402 | |
| 403 | for (const account of selectedAccountsList) { |
| 404 | const startDate = selectedDateRange[0]?.format('YYYY-MM-DD'); |
| 405 | const endDate = selectedDateRange[1]?.format('YYYY-MM-DD'); |
| 406 | const dashboardRes = await icpGetAccountDashboard(account.id, [ |
| 407 | startDate, |
| 408 | endDate, |
| 409 | ]); |
| 410 | |
| 411 | const datas = { |
| 412 | id: account.id, |
| 413 | type: account.type, |
| 414 | name: account.nickname, |
| 415 | data: dashboardRes, |
| 416 | }; |
| 417 | dataAll.push(datas); |
| 418 | } |
| 419 | setDashboardData7(dataAll); |
| 420 | } |
| 421 | }; |
| 422 | |
| 423 | // 处理指标类型选择 |
| 424 | const handleMetricSelect = (metric: string) => { |
| 425 | setSelectedMetric(metric); |
| 426 | }; |
| 427 | |
| 428 | // 刷新账户数据 |
| 429 | const refreshAccountData = () => { |
| 430 | // 清空现有数据 |
| 431 | setDashboardData([]); |
| 432 | // 重新获取所有选中账户的数据 |
| 433 | getAccountStatistics(); |
| 434 | message.success('数据已刷新'); |
| 435 | }; |
| 436 | |
| 437 | // 添加获取平台图标的函数 |
| 438 | const getPlatformIcon = (type: string) => { |
| 439 | switch (type) { |
| 440 | case 'douyin': |
| 441 | return douyinIcon; |
| 442 | case 'xhs': |
| 443 | return xhsIcon; |
| 444 | case 'wxSph': |
| 445 | return wxSphIcon; |
| 446 | case 'KWAI': |
| 447 | return ksIcon; |
| 448 | default: |
| 449 | return ''; |
| 450 | } |
| 451 | }; |
| 452 | |
| 453 | // 修改检查视频的函数 |
| 454 | const examineVideo = (account: any) => { |
| 455 | console.log('检查视频', account); |
| 456 | // 根据账户类型确定要打开的URL |
| 457 | let url = ''; |
| 458 | switch (account.type) { |
| 459 | case 'douyin': |
| 460 | url = `https://creator.douyin.com/`; |
| 461 | break; |
| 462 | case 'xhs': |
| 463 | url = `https://www.xiaohongshu.com/`; |
| 464 | break; |
| 465 | case 'wxSph': |
| 466 | url = `https://channels.weixin.qq.com`; |
| 467 | break; |
| 468 | case 'KWAI': |
| 469 | url = `https://id.kuaishou.com/pass/kuaishou/login/passToken?sid=kuaishou.web.cp.api`; |
| 470 | break; |
| 471 | default: |
| 472 | url = ''; |
| 473 | } |
| 474 | |
| 475 | setExamineVideoData({ |
| 476 | url, |
| 477 | account, |
| 478 | open: true, |
| 479 | }); |
| 480 | }; |
| 481 | |
| 482 | // 关闭WebView |
| 483 | const closeWebView = () => { |
| 484 | setExamineVideoData((prev) => ({ ...prev, open: false })); |
| 485 | }; |
| 486 | |
| 487 | return ( |
| 488 | <div className="min-h-screen page-container bg-gray-50"> |
| 489 | <Spin |
| 490 | spinning={loading} |
| 491 | tip="数据加载中..." |
| 492 | size="large" |
| 493 | className="min-h-screen" |
| 494 | > |
| 495 | {/* WebView组件 */} |
| 496 | {examineVideoData.open && examineVideoData.account ? ( |
| 497 | <div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"> |
| 498 | <div className="relative w-4/5 overflow-hidden bg-white rounded-lg h-4/5"> |
| 499 | <button |
| 500 | className="absolute flex items-center justify-center p-2 transition-all duration-300 bg-white rounded-full shadow-md top-4 right-4 hover:shadow-lg" |
| 501 | style={{ |
| 502 | zIndex: 1000, |
| 503 | width: '36px', |
| 504 | height: '36px', |
| 505 | color: '#a66ae4', |
| 506 | border: '1px solid rgba(166, 106, 228, 0.2)', |
| 507 | }} |
| 508 | onClick={closeWebView} |
| 509 | > |
| 510 | <svg |
| 511 | viewBox="0 0 24 24" |
| 512 | width="20" |
| 513 | height="20" |
| 514 | stroke="currentColor" |
| 515 | strokeWidth="2" |
| 516 | fill="none" |
| 517 | strokeLinecap="round" |
| 518 | strokeLinejoin="round" |
| 519 | > |
| 520 | <line x1="18" y1="6" x2="6" y2="18"></line> |
| 521 | <line x1="6" y1="6" x2="18" y2="18"></line> |
| 522 | </svg> |
| 523 | </button> |
| 524 | <WebView |
| 525 | url={examineVideoData.url} |
| 526 | cookieParams={{ |
| 527 | cookies: JSON.parse( |
| 528 | examineVideoData.account.loginCookie || '{}', |
| 529 | ), |
| 530 | }} |
| 531 | key={examineVideoData.url + examineVideoData.open} |
| 532 | /> |
| 533 | </div> |
| 534 | </div> |
| 535 | ) : null} |
| 536 | |
| 537 | <div className="px-6 py-6"> |
| 538 | {/* 顶部标题区域 */} |
| 539 | <div className="flex items-center justify-between mb-6"> |
| 540 | <h1 className="text-2xl font-bold text-gray-900">数据中心</h1> |
| 541 | <div className="flex items-center space-x-2 text-gray-600 cursor-pointer hover:text-[#a66ae4]"> |
| 542 | <InfoCircleOutlined /> |
| 543 | <span>数据说明</span> |
| 544 | </div> |
| 545 | </div> |
| 546 | |
| 547 | {/* 总体数据概览和账户列表 */} |
| 548 | <div className="flex gap-8 mb-8"> |
| 549 | {/* 总体数据概览 */} |
| 550 | <div className="w-[500px] flex-shrink-0"> |
| 551 | <h2 className="mb-4 text-lg font-medium">总体数据概览</h2> |
| 552 | <div className="bg-white rounded-lg shadow-sm p-6 h-[120px] flex items-center"> |
| 553 | <div className="grid w-full grid-cols-2 gap-6"> |
| 554 | <div className="p-4 rounded-lg bg-gray-50"> |
| 555 | <div className="flex items-center justify-between mb-2"> |
| 556 | <div className="text-sm text-gray-600">账户总数</div> |
| 557 | <span className="text-lg text-gray-400">👥</span> |
| 558 | </div> |
| 559 | <div className="relative"> |
| 560 | <div className="text-2xl font-semibold text-[#a66ae4]"> |
| 561 | {statisticsInfo?.accountTotal?.toLocaleString() || 0} |
| 562 | </div> |
| 563 | <div className="absolute bottom-0 right-0 text-xs text-[#ccc]"> |
| 564 | 失效:{' '} |
| 565 | {!!statisticsInfo |
| 566 | ? (statisticsInfo.accountTotal ?? 0) - |
| 567 | (statisticsInfo.list?.length ?? 0) |
| 568 | : 0} |
| 569 | </div> |
| 570 | </div> |
| 571 | </div> |
| 572 | <div className="p-4 rounded-lg bg-gray-50"> |
| 573 | <div className="flex items-center justify-between mb-2"> |
| 574 | <div className="text-sm text-gray-600">粉丝总数</div> |
| 575 | <span className="text-lg text-gray-400">🌟</span> |
| 576 | </div> |
| 577 | <div className="text-2xl font-semibold text-[#a66ae4]"> |
| 578 | {statisticsInfo?.fansCount?.toLocaleString() || 0} |
| 579 | </div> |
| 580 | </div> |
| 581 | </div> |
| 582 | </div> |
| 583 | </div> |
| 584 | |
| 585 | {/* 账户列表 */} |
| 586 | <div className="flex-1"> |
| 587 | <h2 className="mb-4 text-lg font-medium">账户列表</h2> |
| 588 | <div className="bg-white rounded-lg shadow-sm p-6 h-[120px]"> |
| 589 | {!statisticsInfo?.list || statisticsInfo.list.length === 0 ? ( |
| 590 | // 无数据状态 - 使用Ant Design图标 |
| 591 | <div className="flex flex-col items-center justify-center h-full"> |
| 592 | <QuestionCircleOutlined |
| 593 | style={{ fontSize: '32px', color: '#CCCCCC' }} |
| 594 | /> |
| 595 | <div className="mt-2 text-sm text-gray-500">暂无数据</div> |
| 596 | </div> |
| 597 | ) : ( |
| 598 | // 有数据状态 |
| 599 | <div |
| 600 | className="flex items-center h-full gap-4 overflow-x-auto custom-scrollbar" |
| 601 | style={{ padding: '2px 10px' }} |
| 602 | > |
| 603 | {statisticsInfo?.list?.map((account) => ( |
| 604 | <div |
| 605 | key={account.id} |
| 606 | className={`flex-shrink-0 flex items-center px-6 py-4 space-x-3 transition-all rounded-lg bg-gray-50 hover:shadow-sm cursor-pointer ${ |
| 607 | selectedAccounts.includes(account.id) |
| 608 | ? 'ring-2 ring-[#a66ae4]' |
| 609 | : '' |
| 610 | }`} |
| 611 | onClick={() => toggleAccountSelection(account.id)} |
| 612 | > |
| 613 | <div style={{ position: 'relative' }}> |
| 614 | <img |
| 615 | className="w-12 h-12 rounded-full" |
| 616 | src={account.avatar} |
| 617 | alt="" |
| 618 | /> |
| 619 | {getPlatformIcon(account.type) && ( |
| 620 | <img |
| 621 | src={getPlatformIcon(account.type)} |
| 622 | alt={account.type} |
| 623 | className="w-4 h-4 ml-2" |
| 624 | style={{ |
| 625 | position: 'absolute', |
| 626 | bottom: '0', |
| 627 | right: '0', |
| 628 | }} |
| 629 | /> |
| 630 | )} |
| 631 | </div> |
| 632 | |
| 633 | <div className="space-y-1"> |
| 634 | <div className="text-base font-medium text-left text-gray-900"> |
| 635 | {account.nickname} |
| 636 | </div> |
| 637 | |
| 638 | <div className="flex text-sm text-left"> |
| 639 | <span className="text-gray-500"> |
| 640 | 粉丝:{' '} |
| 641 | {(account as any).fansCount?.toLocaleString() || |
| 642 | 0} |
| 643 | </span> |
| 644 | </div> |
| 645 | |
| 646 | <div |
| 647 | className="text-left text-gray-500" |
| 648 | style={{ fontSize: '12px' }} |
| 649 | > |
| 650 | ID: {account.uid} |
| 651 | </div> |
| 652 | </div> |
| 653 | </div> |
| 654 | ))} |
| 655 | </div> |
| 656 | )} |
| 657 | </div> |
| 658 | </div> |
| 659 | </div> |
| 660 | |
| 661 | {/* 数据明细 */} |
| 662 | <div className="mb-8"> |
| 663 | <div className="flex items-center justify-between mb-4"> |
| 664 | <h2 className="text-lg font-medium">数据明细</h2> |
| 665 | <div className="flex items-center space-x-4"> |
| 666 | <DatePicker.RangePicker |
| 667 | value={selectedDateRange} |
| 668 | onChange={(dates) => { |
| 669 | if (dates) { |
| 670 | setSelectedDateRange([ |
| 671 | dates[0] as Dayjs, |
| 672 | dates[1] as Dayjs, |
| 673 | ]); |
| 674 | } |
| 675 | }} |
| 676 | disabledDate={disabledDate} |
| 677 | className="w-64" |
| 678 | /> |
| 679 | <Button |
| 680 | type="primary" |
| 681 | icon={<SearchOutlined />} |
| 682 | onClick={() => { |
| 683 | // 清空现有数据 |
| 684 | setDashboardData7([]); |
| 685 | // 重新获取所有选中账户的数据 |
| 686 | getAccountStatistics7(); |
| 687 | }} |
| 688 | className="bg-[#a66ae4] hover:bg-[#9559d1]" |
| 689 | > |
| 690 | 搜索 |
| 691 | </Button> |
| 692 | <Button |
| 693 | type="primary" |
| 694 | icon={<DownloadOutlined />} |
| 695 | onClick={handleExportData} |
| 696 | className="bg-[#a66ae4] hover:bg-[#9559d1]" |
| 697 | > |
| 698 | 导出数据 |
| 699 | </Button> |
| 700 | </div> |
| 701 | </div> |
| 702 | |
| 703 | {/* 总体数据卡片 */} |
| 704 | <div className="grid grid-cols-6 gap-4 mb-6"> |
| 705 | {[ |
| 706 | { key: 'fans', title: '总粉丝数', icon: '👥' }, |
| 707 | { key: 'read', title: '总播放数', icon: '▶️' }, |
| 708 | { key: 'comment', title: '总评论数', icon: '💬' }, |
| 709 | { key: 'like', title: '总点赞数', icon: '👍' }, |
| 710 | { key: 'collect', title: '总分享数', icon: '🔄' }, |
| 711 | { key: 'forward', title: '总主页访问', icon: '🏠' }, |
| 712 | ].map((metric) => ( |
| 713 | <div |
| 714 | key={metric.key} |
| 715 | className={`p-4 bg-white rounded-lg shadow-sm cursor-pointer transition-all ${ |
| 716 | selectedMetric === metric.key ? 'ring-2 ring-[#a66ae4]' : '' |
| 717 | }`} |
| 718 | onClick={() => handleMetricSelect(metric.key)} |
| 719 | > |
| 720 | <div className="flex items-center justify-between mb-2"> |
| 721 | <div className="text-sm text-gray-600">{metric.title}</div> |
| 722 | <div className="flex items-center space-x-2"> |
| 723 | <div className="text-xs text-gray-400"> |
| 724 | 昨日:{' '} |
| 725 | {getYesterdayTotalValue(metric.key).toLocaleString()} |
| 726 | </div> |
| 727 | <div className="text-lg text-gray-400">{metric.icon}</div> |
| 728 | </div> |
| 729 | </div> |
| 730 | <div className="text-2xl font-semibold text-[#a66ae4]"> |
| 731 | {getRangeTotalMetricValue(metric.key).toLocaleString()} |
| 732 | </div> |
| 733 | </div> |
| 734 | ))} |
| 735 | </div> |
| 736 | |
| 737 | {/* 趋势图表 */} |
| 738 | <div className="p-6 mb-6 bg-white rounded-lg shadow-sm"> |
| 739 | <div ref={chartRef} style={{ height: '400px' }} /> |
| 740 | </div> |
| 741 | </div> |
| 742 | |
| 743 | {/* 账户数据 */} |
| 744 | <div className="mb-8" style={{ paddingBottom: '80px' }}> |
| 745 | <div className="flex items-center justify-between mb-4"> |
| 746 | <h2 className="text-lg font-medium">昨日账户数据</h2> |
| 747 | <Button |
| 748 | type="text" |
| 749 | icon={<ReloadOutlined />} |
| 750 | onClick={refreshAccountData} |
| 751 | className="flex items-center transition-colors hover:text-blue-500" |
| 752 | > |
| 753 | 刷新 |
| 754 | </Button> |
| 755 | </div> |
| 756 | |
| 757 | {!statisticsInfo?.list || statisticsInfo.list.length === 0 ? ( |
| 758 | // 无数据状态 - 使用Ant Design图标 |
| 759 | <div className="flex flex-col items-center justify-center p-12 bg-white rounded-lg shadow-sm"> |
| 760 | <QuestionCircleOutlined |
| 761 | style={{ fontSize: '32px', color: '#CCCCCC' }} |
| 762 | /> |
| 763 | <div className="mt-2 text-sm text-gray-500">暂无数据</div> |
| 764 | </div> |
| 765 | ) : ( |
| 766 | // 有数据状态 |
| 767 | <div className="space-y-4"> |
| 768 | {statisticsInfo?.list?.map((account) => { |
| 769 | const accountData = dashboardData.find( |
| 770 | (item: any) => item.id == account.id, |
| 771 | ); |
| 772 | return ( |
| 773 | <Card |
| 774 | key={account.id} |
| 775 | className="transition-shadow hover:shadow-md" |
| 776 | > |
| 777 | <div className="flex items-center"> |
| 778 | {/* 账户信息 */} |
| 779 | <div className="flex items-center flex-shrink-0 w-64 space-x-3"> |
| 780 | <Avatar size={48} src={account.avatar} /> |
| 781 | <div> |
| 782 | <div className="font-medium"> |
| 783 | {account.nickname} |
| 784 | </div> |
| 785 | |
| 786 | <div className="text-sm text-gray-500"> |
| 787 | ID: {account.uid}{' '} |
| 788 | </div> |
| 789 | </div> |
| 790 | </div> |
| 791 | |
| 792 | {/* 数据展示 */} |
| 793 | <div className="grid flex-1 grid-cols-7 gap-4"> |
| 794 | <div className="text-center"> |
| 795 | <div className="text-sm text-gray-500">粉丝数</div> |
| 796 | <div className="font-medium text-[#a66ae4]"> |
| 797 | {(accountData?.fans || 0) > 0 ? '+ ' : '- '} |
| 798 | {Math.abs(accountData?.fans || 0)} |
| 799 | </div> |
| 800 | </div> |
| 801 | <div className="text-center"> |
| 802 | <div className="text-sm text-gray-500">阅读数</div> |
| 803 | <div className="font-medium text-[#a66ae4]"> |
| 804 | {accountData?.read || 0} |
| 805 | </div> |
| 806 | </div> |
| 807 | <div className="text-center"> |
| 808 | <div className="text-sm text-gray-500">评论数</div> |
| 809 | <div className="font-medium text-[#a66ae4]"> |
| 810 | {accountData?.comment || 0} |
| 811 | </div> |
| 812 | </div> |
| 813 | <div className="text-center"> |
| 814 | <div className="text-sm text-gray-500">点赞数</div> |
| 815 | <div className="font-medium text-[#a66ae4]"> |
| 816 | {accountData?.like || 0} |
| 817 | </div> |
| 818 | </div> |
| 819 | <div className="text-center"> |
| 820 | <div className="text-sm text-gray-500">分享数</div> |
| 821 | <div className="font-medium text-[#a66ae4]"> |
| 822 | {accountData?.collect || 0} |
| 823 | </div> |
| 824 | </div> |
| 825 | <div className="text-center"> |
| 826 | <div className="text-sm text-gray-500"> |
| 827 | 主页访问 |
| 828 | </div> |
| 829 | <div className="font-medium text-[#a66ae4]"> |
| 830 | {accountData?.forward || 0} |
| 831 | </div> |
| 832 | </div> |
| 833 | <div className="text-center"> |
| 834 | <div |
| 835 | className="text-sm text-gray-500" |
| 836 | style={{ |
| 837 | marginTop: '10px', |
| 838 | color: '#a66ae4', |
| 839 | cursor: 'pointer', |
| 840 | }} |
| 841 | onClick={() => examineVideo(account)} |
| 842 | > |
| 843 | 查看详情 |
| 844 | </div> |
| 845 | </div> |
| 846 | </div> |
| 847 | </div> |
| 848 | </Card> |
| 849 | ); |
| 850 | })} |
| 851 | </div> |
| 852 | )} |
| 853 | </div> |
| 854 | </div> |
| 855 | </Spin> |
| 856 | </div> |
| 857 | ); |
| 858 | }; |
| 859 | |
| 860 | export default Statistics; |
| 861 |