|
|
@@ -1,9 +1,367 @@
|
|
|
+import { PlatformConstants } from '@/constants/PlatformConstants';
|
|
|
+import API from '@/services';
|
|
|
+import { Card, Col, DatePicker, Empty, Row, Space, Spin, Table, Tag } from 'antd';
|
|
|
+import type { ColumnsType } from 'antd/es/table';
|
|
|
+import dayjs, { Dayjs } from 'dayjs';
|
|
|
+import React, { useEffect, useMemo, useState } from 'react';
|
|
|
import styles from './index.less';
|
|
|
|
|
|
+const { RangePicker } = DatePicker;
|
|
|
+
|
|
|
+interface DashboardRow {
|
|
|
+ id?: number;
|
|
|
+ statDate?: string;
|
|
|
+ platform: string;
|
|
|
+ totalPages?: number;
|
|
|
+ totalCount?: number;
|
|
|
+ totalTasks?: number;
|
|
|
+ completedTasks?: number;
|
|
|
+ completionRate?: number;
|
|
|
+ activeAccounts?: number;
|
|
|
+ reportCount?: number;
|
|
|
+ exceptionTotal?: number;
|
|
|
+ exceptionRate?: number;
|
|
|
+ insertTime?: number;
|
|
|
+ updateTime?: number;
|
|
|
+}
|
|
|
+
|
|
|
const HomePage: React.FC = () => {
|
|
|
+ const [date, setDate] = useState<Dayjs>(dayjs());
|
|
|
+ const [range, setRange] = useState<[Dayjs, Dayjs]>([
|
|
|
+ dayjs().subtract(6, 'day'),
|
|
|
+ dayjs(),
|
|
|
+ ]);
|
|
|
+ const [loading, setLoading] = useState(false);
|
|
|
+ const [list, setList] = useState<DashboardRow[]>([]);
|
|
|
+
|
|
|
+ const fetchDashboard = async (queryDate: Dayjs) => {
|
|
|
+ setLoading(true);
|
|
|
+ try {
|
|
|
+ const res: any = await API.Dashboard.getByDate({
|
|
|
+ date: queryDate.format('YYYY-MM-DD'),
|
|
|
+ });
|
|
|
+ if (res?.code === 'success') {
|
|
|
+ setList((res.data || []) as DashboardRow[]);
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ setLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const fetchTrend = async (startDate: Dayjs, endDate: Dayjs) => {
|
|
|
+ const res: any = await API.Dashboard.getTrend({
|
|
|
+ start_date: startDate.format('YYYY-MM-DD'),
|
|
|
+ end_date: endDate.format('YYYY-MM-DD'),
|
|
|
+ });
|
|
|
+ if (res?.code === 'success') {
|
|
|
+ return (res.data || []) as DashboardRow[];
|
|
|
+ }
|
|
|
+ return [];
|
|
|
+ };
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ fetchDashboard(date);
|
|
|
+ }, [date]);
|
|
|
+
|
|
|
+ const summary = useMemo(() => {
|
|
|
+ return list.reduce(
|
|
|
+ (acc, cur) => {
|
|
|
+ acc.totalTasks += cur.totalTasks || 0;
|
|
|
+ acc.completedTasks += cur.completedTasks || 0;
|
|
|
+ acc.totalPages += cur.totalPages || 0;
|
|
|
+ acc.totalCount += cur.totalCount || 0;
|
|
|
+ acc.activeAccounts += cur.activeAccounts || 0;
|
|
|
+ acc.exceptionTotal += cur.exceptionTotal || 0;
|
|
|
+ acc.reportCount += cur.reportCount || 0;
|
|
|
+ return acc;
|
|
|
+ },
|
|
|
+ {
|
|
|
+ totalTasks: 0,
|
|
|
+ completedTasks: 0,
|
|
|
+ totalPages: 0,
|
|
|
+ totalCount: 0,
|
|
|
+ activeAccounts: 0,
|
|
|
+ exceptionTotal: 0,
|
|
|
+ reportCount: 0,
|
|
|
+ },
|
|
|
+ );
|
|
|
+ }, [list]);
|
|
|
+
|
|
|
+ const summaryCompletionRate = summary.totalTasks
|
|
|
+ ? ((summary.completedTasks / summary.totalTasks) * 100).toFixed(2)
|
|
|
+ : '0.00';
|
|
|
+ const summaryExceptionRate = summary.reportCount
|
|
|
+ ? ((summary.exceptionTotal / summary.reportCount) * 100).toFixed(2)
|
|
|
+ : '0.00';
|
|
|
+
|
|
|
+ const columns: ColumnsType<DashboardRow> = [
|
|
|
+ {
|
|
|
+ title: '平台',
|
|
|
+ dataIndex: 'platform',
|
|
|
+ key: 'platform',
|
|
|
+ width: 140,
|
|
|
+ render: (val) => (
|
|
|
+ <span className={styles.platformName}>
|
|
|
+ {(PlatformConstants as any)[val] ?? `平台${val}`}
|
|
|
+ </span>
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '总任务数',
|
|
|
+ dataIndex: 'totalTasks',
|
|
|
+ key: 'totalTasks',
|
|
|
+ align: 'right',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '已完成',
|
|
|
+ dataIndex: 'completedTasks',
|
|
|
+ key: 'completedTasks',
|
|
|
+ align: 'right',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '完成率',
|
|
|
+ dataIndex: 'completionRate',
|
|
|
+ key: 'completionRate',
|
|
|
+ align: 'right',
|
|
|
+ render: (val) =>
|
|
|
+ val == null ? '-' : (
|
|
|
+ <span
|
|
|
+ className={
|
|
|
+ Number(val) >= 80
|
|
|
+ ? styles.rateUp
|
|
|
+ : Number(val) >= 50
|
|
|
+ ? undefined
|
|
|
+ : styles.rateDown
|
|
|
+ }
|
|
|
+ >
|
|
|
+ {`${Number(val).toFixed(2)}%`}
|
|
|
+ </span>
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '今日爬取页数',
|
|
|
+ dataIndex: 'totalPages',
|
|
|
+ key: 'totalPages',
|
|
|
+ align: 'right',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '今日爬取条数',
|
|
|
+ dataIndex: 'totalCount',
|
|
|
+ key: 'totalCount',
|
|
|
+ align: 'right',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '活跃账号',
|
|
|
+ dataIndex: 'activeAccounts',
|
|
|
+ key: 'activeAccounts',
|
|
|
+ align: 'right',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '回告次数',
|
|
|
+ dataIndex: 'reportCount',
|
|
|
+ key: 'reportCount',
|
|
|
+ align: 'right',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '异常次数',
|
|
|
+ dataIndex: 'exceptionTotal',
|
|
|
+ key: 'exceptionTotal',
|
|
|
+ align: 'right',
|
|
|
+ render: (val) =>
|
|
|
+ val > 0 ? (
|
|
|
+ <Tag color="red">{val}</Tag>
|
|
|
+ ) : (
|
|
|
+ <Tag color="green">0</Tag>
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '异常率',
|
|
|
+ dataIndex: 'exceptionRate',
|
|
|
+ key: 'exceptionRate',
|
|
|
+ align: 'right',
|
|
|
+ render: (val) =>
|
|
|
+ val == null ? '-' : (
|
|
|
+ <span
|
|
|
+ className={
|
|
|
+ Number(val) <= 5 ? styles.rateUp : styles.rateDown
|
|
|
+ }
|
|
|
+ >
|
|
|
+ {`${Number(val).toFixed(2)}%`}
|
|
|
+ </span>
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 简易趋势展示:每日总任务数 vs 已完成任务数
|
|
|
+ const [trendData, setTrendData] = useState<DashboardRow[]>([]);
|
|
|
+ const [trendLoading, setTrendLoading] = useState(false);
|
|
|
+
|
|
|
+ const handleRangeChange = async (val: any) => {
|
|
|
+ if (!val || val.length !== 2) return;
|
|
|
+ const [start, end] = val as [Dayjs, Dayjs];
|
|
|
+ setRange([start, end]);
|
|
|
+ setTrendLoading(true);
|
|
|
+ try {
|
|
|
+ const rows = await fetchTrend(start, end);
|
|
|
+ setTrendData(rows);
|
|
|
+ } finally {
|
|
|
+ setTrendLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ handleRangeChange(range);
|
|
|
+ // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ const trendColumns: ColumnsType<any> = [
|
|
|
+ { title: '统计日期', dataIndex: 'statDate', key: 'statDate', width: 140 },
|
|
|
+ {
|
|
|
+ title: '平台',
|
|
|
+ dataIndex: 'platform',
|
|
|
+ key: 'platform',
|
|
|
+ width: 140,
|
|
|
+ render: (val) => (PlatformConstants as any)[val] ?? `平台${val}`,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '总任务数',
|
|
|
+ dataIndex: 'totalTasks',
|
|
|
+ key: 'totalTasks',
|
|
|
+ align: 'right',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '已完成',
|
|
|
+ dataIndex: 'completedTasks',
|
|
|
+ key: 'completedTasks',
|
|
|
+ align: 'right',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '爬取页数',
|
|
|
+ dataIndex: 'totalPages',
|
|
|
+ key: 'totalPages',
|
|
|
+ align: 'right',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '爬取条数',
|
|
|
+ dataIndex: 'totalCount',
|
|
|
+ key: 'totalCount',
|
|
|
+ align: 'right',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '异常',
|
|
|
+ dataIndex: 'exceptionTotal',
|
|
|
+ key: 'exceptionTotal',
|
|
|
+ align: 'right',
|
|
|
+ },
|
|
|
+ ];
|
|
|
+
|
|
|
return (
|
|
|
<div className={styles.container}>
|
|
|
- <h1></h1>
|
|
|
+ <div className={styles.filterBar}>
|
|
|
+ <Space>
|
|
|
+ <span>统计日期:</span>
|
|
|
+ <DatePicker
|
|
|
+ value={date}
|
|
|
+ allowClear={false}
|
|
|
+ onChange={(d) => d && setDate(d)}
|
|
|
+ />
|
|
|
+ </Space>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <Row gutter={[16, 16]} className={styles.summaryRow}>
|
|
|
+ <Col xs={24} sm={12} md={6}>
|
|
|
+ <div className={styles.summaryCard}>
|
|
|
+ <span className={styles.summaryTitle}>今日总任务数</span>
|
|
|
+ <span className={styles.summaryValue}>{summary.totalTasks}</span>
|
|
|
+ <span className={styles.summaryHint}>
|
|
|
+ 已完成 {summary.completedTasks}({summaryCompletionRate}%)
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </Col>
|
|
|
+ <Col xs={24} sm={12} md={6}>
|
|
|
+ <div className={styles.summaryCard}>
|
|
|
+ <span className={styles.summaryTitle}>今日爬取条数</span>
|
|
|
+ <span className={styles.summaryValue}>{summary.totalCount}</span>
|
|
|
+ <span className={styles.summaryHint}>
|
|
|
+ 爬取页数 {summary.totalPages}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </Col>
|
|
|
+ <Col xs={24} sm={12} md={6}>
|
|
|
+ <div className={styles.summaryCard}>
|
|
|
+ <span className={styles.summaryTitle}>今日活跃账号</span>
|
|
|
+ <span className={styles.summaryValue}>{summary.activeAccounts}</span>
|
|
|
+ <span className={styles.summaryHint}>
|
|
|
+ 回告次数 {summary.reportCount}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </Col>
|
|
|
+ <Col xs={24} sm={12} md={6}>
|
|
|
+ <div className={styles.summaryCard}>
|
|
|
+ <span className={styles.summaryTitle}>今日异常率</span>
|
|
|
+ <span
|
|
|
+ className={styles.summaryValue}
|
|
|
+ style={{
|
|
|
+ color:
|
|
|
+ Number(summaryExceptionRate) <= 5 ? '#52c41a' : '#ff4d4f',
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {`${summaryExceptionRate}%`}
|
|
|
+ </span>
|
|
|
+ <span className={styles.summaryHint}>
|
|
|
+ 异常 {summary.exceptionTotal} 次
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </Col>
|
|
|
+ </Row>
|
|
|
+
|
|
|
+ <div className={styles.platformPanel}>
|
|
|
+ <div className={styles.panelTitle}>平台数据明细({date.format('YYYY-MM-DD')})</div>
|
|
|
+ <Spin spinning={loading}>
|
|
|
+ {list.length ? (
|
|
|
+ <Table<DashboardRow>
|
|
|
+ rowKey={(r) => `${r.platform}-${r.id ?? ''}`}
|
|
|
+ dataSource={list}
|
|
|
+ columns={columns}
|
|
|
+ size="middle"
|
|
|
+ pagination={false}
|
|
|
+ />
|
|
|
+ ) : (
|
|
|
+ <Empty description="暂无数据" />
|
|
|
+ )}
|
|
|
+ </Spin>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <Card
|
|
|
+ size="small"
|
|
|
+ title={
|
|
|
+ <Space>
|
|
|
+ <span>趋势查询</span>
|
|
|
+ <RangePicker
|
|
|
+ value={range}
|
|
|
+ onChange={handleRangeChange}
|
|
|
+ allowClear={false}
|
|
|
+ />
|
|
|
+ </Space>
|
|
|
+ }
|
|
|
+ style={{ marginTop: 16 }}
|
|
|
+ >
|
|
|
+ <Spin spinning={trendLoading}>
|
|
|
+ {trendData.length ? (
|
|
|
+ <Table
|
|
|
+ rowKey={(r: any) =>
|
|
|
+ `${r.statDate || ''}-${r.platform || ''}`
|
|
|
+ }
|
|
|
+ dataSource={trendData}
|
|
|
+ columns={trendColumns}
|
|
|
+ size="small"
|
|
|
+ pagination={{ pageSize: 20 }}
|
|
|
+ />
|
|
|
+ ) : (
|
|
|
+ <Empty description="暂无趋势数据" />
|
|
|
+ )}
|
|
|
+ </Spin>
|
|
|
+ </Card>
|
|
|
</div>
|
|
|
);
|
|
|
};
|