wjx 4 週間 前
コミット
4e69af2d07

+ 234 - 841
src/Hook/useEcharts.tsx

@@ -1,869 +1,262 @@
-import React, { useEffect, useRef, useState } from 'react'
-// import * as echarts from 'echarts'
-import { EChartsOption, init, dispose, registerMap } from 'echarts'
-import { Empty } from 'antd'
-import { china } from '../utils/dictionary'
-/**通用直接传入原始配置EChartsOption*/
-function Echarts(props: { children: EChartsOption | null, style?: { width: number | string, height: number | string } }) {
-    const { style } = props
-    const ref: { current: HTMLDivElement | null } = useRef(null)
-    const [myChart, setMyChart] = useState<any>()
-    /**创建myChart实例 */
-    useEffect(() => {
-        let myChart:any = null
-        if (ref?.current && props?.children) {
+import React, { useEffect, useRef } from 'react';
+import * as echarts from 'echarts';
+import { Empty } from 'antd';
 
-            myChart = init(ref.current)
-            setMyChart(myChart)
-            myChart.setOption(props.children);
-            myChart.resize()
-        }
-        return () => {
-            if (myChart?.id) {
-                dispose(myChart.id)
-            }
-        }
-    }, [ref?.current, props?.children])
-    /**卸载myChart */
-    useEffect(() => {
-        return () => {
-            if (myChart?.id) {
-                dispose(myChart.id)
-            }
-        }
-    }, [myChart?.id])
-    return <>
-        {
-            props.children
-                ?
-                <div ref={ref} style={style ? style : { width: '100%', height: '100%' }} />
-                :
-                <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
-                    <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
-                </div>
-        }
-    </>
-}
-/**
- * Map粉丝统计 
- * @param data name省名称 vlaue值
- * @param style width ,height
-*/
-function Map(props: { data: { name: string, value: number }[], style?: { width: number | string, height: number | string } }) {
-    const { data, style } = props
-    const ref: { current: HTMLDivElement | null } = useRef(null)
-    const [myChart, setMyChart] = useState<any>()
-    /**创建myChart实例 */
-    useEffect(() => {
-        let myChart:any = null
-        if (ref?.current && data.length > 0) {
-             myChart = init(ref.current)//初始化chart
-            registerMap('China', require('../public/map.json'))//获取地图数据
-            let num: number[] = []//获取所有值方便取最大最小值
-            let newData: any = china//处理匹配接口返回数据和地图数据的映射避免名字不同
-            data.forEach((item: { name: string, value: number }) => {
-                newData.forEach((data: { name: string, value: number }) => {
-                    if (item.name && data.name.indexOf(item.name) !== -1) {
-                        data.value = item.value
-                        num.push(item.value)
-                    }
-                })
-            })
-            setMyChart(myChart)//存放Chart实例以备卸载
-            myChart.setOption({//设置Chart配置
-                /**悬浮提示 */
-                tooltip: {
-                    trigger: 'item',
-                    formatter: '{b}:{c}'
-                },
-                /**右侧操作工具 */
-                toolbox: {
-                    show: true,
-                    orient: 'vertical',
-                    right: 50,
-                    top: 'center',
-                    feature: {
-                        dataView: {
-                            readOnly: false,
-                            title: '查看详情'
-                        },
-                        restore: {
-                            title: '刷新'
-                        },
-                        saveAsImage: {
-                            title: '保存为图片'
-                        }
-                    }
-                },
-                /**左侧操作工具 */
-                visualMap: {
-                    min: 0,
-                    max: Math.max(...num),
-                    text: ['High', 'Low'],
-                    realtime: false,
-                    calculable: true,
-                    top: 'middle',
-                    inRange: {
-                        color: ['lightskyblue', 'yellow', 'orangered']
-                    }
-                },
-                /**地图参数设置 */
-                series: [
-                    {
-                        name: '粉丝统计',
-                        type: 'map',
-                        map: 'China', // 自定义扩展图表类型
-                        zoom: 1.2,
-                        scaleLimit: { min: 1, max: 10 },
-                        roam: true,
-                        nameProperty: 'name',
-                        data: newData,
-                    }
-                ],
-            });
-        }
-        return () => {
-            if (myChart?.id) {
-                dispose(myChart.id)
-            }
-        }
-    }, [ref?.current, data])
-    /**卸载myChart */
+/* =====================================================
+   🔧 通用初始化 Hook (核心)
+===================================================== */
+const useEchartInit = (theme?: string | object) => {
+    const chartRef = useRef<HTMLDivElement>(null);
+    const chartInstance = useRef<echarts.ECharts | null>(null);
+
+    // 初始化 ECharts 实例(只执行一次)
     useEffect(() => {
-        if (data.length === 0) {
-            if (myChart?.id) {
-                dispose(myChart.id)
-            }
-        }
+        if (!chartRef.current) return;
+        const chart = echarts.init(chartRef.current, theme);
+        chartInstance.current = chart;
+
+        const handleResize = () => chart.resize();
+        window.addEventListener('resize', handleResize);
+
         return () => {
-            if (myChart?.id) {
-                dispose(myChart.id)
-            }
-        }
-    }, [myChart?.id, data])
-    return <>
-        {
-            data.length > 0 && <div ref={ref} style={style ? style : { width: '100%', height: '100%' }} />
-        }
-        {
-            data.length === 0 && <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
-                <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
-            </div>
-        }
-    </>
+            window.removeEventListener('resize', handleResize);
+            chart.dispose();
+            chartInstance.current = null;
+        };
+    }, [theme]);
+
+    return { chartRef, chartInstance };
+};
+
+/* =====================================================
+   🎨 默认颜色色板
+===================================================== */
+const DEFAULT_COLORS = [
+    '#5470C6', '#91CC75', '#EE6666', '#FAC858',
+    '#73C0DE', '#3BA272', '#FC8452', '#9A60B4', '#EA7CCC'
+];
+
+/* =====================================================
+   📊 通用 Echarts 容器组件
+===================================================== */
+interface EchartsProps {
+    option?: echarts.EChartsOption | null;
+    style?: React.CSSProperties;
+    theme?: string | object;
+    loading?: boolean;
 }
-/**
- * 饼图总关注 
- * @param data name 名称 value 值
- * @param style width,height
- * @param name tooltip上的名字即饼图的名字
- * @param centerName 饼图中心统计的名称
-*/
-function PieFocus(props: {
-    /**
-     * @param data name 名称 value 值
-     */
-    data: { name: string, value: number }[],
-    /**
-     * @param name tooltip上的名字即饼图的名字
-     */
-    name: string,
-    /**
-     * @param centerName 饼图中心统计的名称
-     */
-    centerName: string,
-    /**
-    * @param style width,height
-    */
-    style?: { width: number | string, height: number | string },
-}) {
-    const { data, style, name, centerName } = props
-    const ref: { current: HTMLDivElement | null } = useRef(null)
-    const [myChart, setMyChart] = useState<any>()
-    /**创建myChart实例 */
-    useEffect(() => {
-        let myChart: any = null
-        if (ref?.current && data.length > 0) {
-            myChart = init(ref.current)
-            setMyChart(myChart)
-            myChart.setOption({
-                /**鼠标悬浮 */
-                tooltip: {
-                    // trigger: 'item',
-                    // formatter: '{a} <br />{b}: {c} ({d}%)'
-                },
-                /**图列 */
-                legend: {
-                    left: 'center',
-                    bottom: 0,
-                    data: data?.map((item: { name: string }) => item.name)
-                },
-                /**工具保存为图片 */
-                toolbox: {
-                    right: 50,
-                    feature: {
-                        saveAsImage: {
-                            title: '保存为图片'
-                        }
-                    }
-                },
-                /**饼图参数 */
-                series: [
-                    {
-                        name,
-                        type: 'pie',
-                        radius: ['50%', '70%'],
-                        // avoidLabelOverlap: false,
-                        label: {
-                            show: true,
-                            // position: 'center',
-                            // fontSize: '30',
-                            // fontWeight: 'bold',
-                            formatter: `{b}:{c} ({d}%)`,
-                        },
-                        data
-                    },
-                    {
-                        name,
-                        type: 'pie',
-                        radius: ['50%', '70%'],
-                        label: {
-                            show: true,
-                            position: 'center',
-                            fontSize: '30',
-                            fontWeight: 'bold',
-                            formatter: `${data.length > 1 ? data?.reduce((a: any, b: { value: number }) => {
-                                return (a?.value || a) + (b?.value || 0)
-                            }) : data[0].value}\n${centerName}`,
-                        },
-                        data
-                    }
-                ]
-            });
-        }
-        return () => {
-            if (myChart?.id) {
-                dispose(myChart?.id)
-            }
-        }
-    }, [ref?.current, data])
-    /**卸载myChart */
+
+const Echarts: React.FC<EchartsProps> = ({ option, style = { width: '100%', height: '100%' }, theme, loading }) => {
+    const { chartRef, chartInstance } = useEchartInit(theme);
+
     useEffect(() => {
-        if (data.length === 0) {
-            if (myChart?.id) {
-                dispose(myChart?.id)
-            }
-        }
-        return () => {
-            if (myChart?.id) {
-                dispose(myChart?.id)
-            }
+        if (!chartInstance.current) return;
+        if (!option) {
+            chartInstance.current.clear();
+            return;
         }
-    }, [myChart?.id, data])
-    return <>
-        {
-            data.length > 0 && <div ref={ref} style={style ? style : { width: '100%', height: '100%' }} />
-        }
-        {
-            data.length === 0 && <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }} >
-                <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
-            </div>
-        }
-    </>
+
+        loading ? chartInstance.current.showLoading() : chartInstance.current.hideLoading();
+        chartInstance.current.setOption(option, true);
+    }, [option, loading]);
+
+    return <div ref={chartRef} style={style} />;
+};
+
+/* =====================================================
+   📈 折线图 Line
+===================================================== */
+export interface LineData {
+    legendName: string;
+    [key: string]: any;
 }
-/**
- * Line 折线图
- * @param data legendName 图列名称 y:x(key为Y轴的展示的名称,value对应X轴的值。例:{legendName:'取消关注','2020-12-12':10,'2020-12-13':11,'2020-12-14':33}[])
- * @param style width,height
-*/
-function Line(props: {
-    data?: { legendName: string, [key: string]: any }[],
-    style?: React.CSSProperties,
-    areaStyle?: boolean,//线变块颜色
-    color?: string | string[],//线颜色
-    markPoint?: any,//浮点
-    fontColor?: string,//文字颜色
-    series?: boolean,//堆叠
-    title?: string,//标题
-    smooth?: boolean,//true 圆弧线 false 直角
-}) {
-    const { data, style, areaStyle = false, color, markPoint, fontColor, series, title, smooth = false } = props
-    const ref: { current: HTMLDivElement | null } = useRef(null)
-    const [myChart, setMyChart] = useState<any>()
-    let textStyle = fontColor ? { textStyle: { color: fontColor } } : {}
-    /**创建myChart实例 */
-    useEffect(() => {
-        let myChart: any = null
-        if (ref?.current && (data as any[]).length > 0) {
-            let Xdata: any = []
-            let Ydatas: any[] = []
-            data?.forEach((item, index) => {
-                Ydatas.push([])
-                Object.keys(item).forEach((key: string) => {
-                    if (index === 0 && key !== 'legendName') {
-                        Xdata.push(key)
-                    }
-                    if (key !== 'legendName') {
-                        Ydatas[index].push(item[key])
-                    }
-                })
-            })
-
-            myChart = init(ref.current)
-            setMyChart(myChart)
-            myChart.setOption({
-                /**鼠标悬浮 */
-                tooltip: {
-                    trigger: 'axis'
-                },
-                /**图列 */
-                legend: {
-                    data: (data as { legendName: string, [key: string]: any }[]).map((item: { legendName: string }) => item.legendName),
-                    bottom: 0,
-                    left: 'center',
-                    ...textStyle
-                },
-                xAxis: {
-                    data: Xdata,
-                    boundaryGap: false,//贴边
-                },
-                yAxis: [
-                    {
-                        type: 'value',
-                    },
-                ],
-                grid: {
-                    left: 100,
-                    right: 50,
-                },
-                ...textStyle,
-                /**工具保存为图片 */
-                toolbox: {
-                    right: 50,
-                    feature: {
-                        saveAsImage: {
-                            title: '保存为图片'
-                        }
-                    }
-                },
-                title: {
-                    text: title || '',
-                    textAlign: 'center',
-                    right: 'center'
-                },
-                /**线图参数 */
-                series: (data as { legendName: string, [key: string]: any }[]).map((item: { legendName: string }, index: number) => {
-                    return {
-                        name: item.legendName,//名称
-                        type: 'line',//图形
-                        data: Ydatas[index],//数据
-                        areaStyle: areaStyle ? {} : undefined,//是否开启块级颜色
-                        color: Array.isArray(color) ? color[index] : color,//颜色
-                        markPoint: {//最大值最小值
-                            data: markPoint ? markPoint : []
-                        },
-                        emphasis: series ? { focus: 'series' } : {}, //堆叠
-                        smooth
-                    }
-                })
-            });
-        }
-        return () => {
-            if (myChart?.id) {
-                dispose(myChart.id)
-            }
-        }
-    }, [ref?.current, data])
-    /**卸载myChart */
-    useEffect(() => {
-        if ((data as any[])?.length === 0) {
-            if (myChart?.id) {
-                dispose(myChart?.id)
-            }
-        }
-        return () => {
-            if (myChart?.id) {
-                dispose(myChart.id)
-            }
-        }
-    }, [myChart?.id, data])
-    return <>
-        {
-            (data as any[])?.length > 0 && <div ref={ref} style={style ? style : { width: '100%', height: '100%' }} />
 
-        }
-        {
-            (data as any[])?.length === 0 && <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
-                <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
-            </div>
-        }
-    </>
+interface LineProps {
+    data?: LineData[];
+    title?: string;
+    smooth?: boolean;
+    areaStyle?: boolean;
+    seriesFocus?: boolean;
+    color?: string[];
+    fontColor?: string;
+    style?: React.CSSProperties;
+    loading?: boolean;
 }
 
-/**
- * Bar 柱状图
- * @param data legendName 图列名称 y:x(key为Y轴的展示的名称,value对应X轴的值。例:{legendName:'取消关注','2020-12-12':10,'2020-12-13':11,'2020-12-14':33}[])
- * @param style width,height
-*/
-function Bar(props: {
-    data?: { legendName: string, [key: string]: any }[],
-    style?: React.CSSProperties,
-    fontColor?: string,//文字颜色
-    title?: string,//标题
-}) {
-    const { data, style, fontColor, title } = props
-    const ref: { current: HTMLDivElement | null } = useRef(null)
-    const [myChart, setMyChart] = useState<any>()
-    let textStyle = fontColor ? { textStyle: { color: fontColor } } : {}
-    /**创建myChart实例 */
-    useEffect(() => {
-        let myChart: any = null
-        if (ref?.current && (data as any[]).length > 0) {
-            let Xdata: any = []     // name
-            let Ydatas: any[] = []  // value
-            data?.forEach((item) => {
-                Xdata.push(item.name)
-                Ydatas.push(item.value)
-            })
-            let sum = eval(Ydatas.join("+"))
-            Ydatas = Ydatas.map((item: number) => {
-                return (item / sum * 100).toFixed(0)
-            })
-            myChart = init(ref.current)
-            setMyChart(myChart)
-            myChart.setOption({
-                /**鼠标悬浮 */
-                // tooltip: {
-                //     trigger: 'axis',
-                // },
-                xAxis: {
-                    max: 'dataMax'
-                },
-                yAxis: {
-                    type: 'category',
-                    data: Xdata,
-                    inverse: true
-                },
-                grid: {
-                    left: 100,
-                    right: 50,
-                },
-                ...textStyle,
-                title: {
-                    text: title || '',
-                    textAlign: 'center',
-                    right: 'center'
-                },
-                /**线图参数 */
-                series: [
-                    {
-                        type: 'bar',//图形
-                        realtimeSort: true,
-                        data: Ydatas,//数据
-                        name: 'X',
-                        label: {
-                            show: true,
-                            position: 'right',
-                            valueAnimation: true,
-                        }
-                    }
-                ]
-            });
-        }
-        return () => {
-            if (myChart?.id) {
-                dispose(myChart.id)
-            }
-        }
-    }, [ref?.current, data])
-    /**卸载myChart */
+const Line: React.FC<LineProps> = ({
+    data = [],
+    title,
+    smooth = false,
+    areaStyle = false,
+    seriesFocus = false,
+    color = DEFAULT_COLORS,
+    fontColor,
+    style = { width: '100%', height: 400 },
+    loading = false
+}) => {
+    const { chartRef, chartInstance } = useEchartInit();
+
     useEffect(() => {
-        if ((data as any[])?.length === 0) {
-            if (myChart?.id) {
-                dispose(myChart?.id)
-            }
-        }
-        return () => {
-            if (myChart?.id) {
-                dispose(myChart.id)
-            }
+        if (!chartInstance.current) return;
+        if (!data.length) {
+            chartInstance.current.clear();
+            return;
         }
-    }, [myChart?.id, data])
-    return <>
-        {
-            (data as any[])?.length > 0 && <div ref={ref} style={style ? style : { width: '100%', height: '100%' }} />
 
-        }
-        {
-            (data as any[])?.length === 0 && <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
-                <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
-            </div>
-        }
-    </>
+        const textStyle = fontColor ? { color: fontColor } : {};
+        const xData = Object.keys(data[0]).filter(k => k !== 'legendName');
+        const seriesData = data.map((item, i) => ({
+            name: item.legendName,
+            type: 'line',
+            data: xData.map(k => item[k]),
+            smooth,
+            areaStyle: areaStyle ? {} : undefined,
+            emphasis: seriesFocus ? { focus: 'series' } : undefined
+        }));
+
+        const option: echarts.EChartsOption = {
+            color,
+            title: { text: title, left: 'center', textStyle },
+            tooltip: { trigger: 'axis' },
+            legend: { data: data.map(i => i.legendName), bottom: 0, textStyle },
+            xAxis: { type: 'category', data: xData, boundaryGap: false },
+            yAxis: { type: 'value' },
+            grid: { left: 60, right: 40, bottom: 80 },
+            series: seriesData as any
+        };
+
+        loading ? chartInstance.current.showLoading() : chartInstance.current.hideLoading();
+        chartInstance.current.setOption(option, true);
+    }, [data, title, smooth, areaStyle, color, fontColor, seriesFocus, loading]);
+
+    return <div ref={chartRef} style={style} />;
+};
+
+/* =====================================================
+   🥧 饼图 Pie
+===================================================== */
+interface PieProps {
+    data?: { name: string; value: number }[];
+    title?: string;
+    color?: string[];
+    style?: React.CSSProperties;
+    loading?: boolean;
 }
 
+const Pie: React.FC<PieProps> = ({
+    data = [],
+    title,
+    color = DEFAULT_COLORS,
+    style = { width: '100%', height: 400 },
+    loading = false
+}) => {
+    const { chartRef, chartInstance } = useEchartInit();
 
-/**
- * Bar 柱状图
- * @param data legendName 图列名称 y:x(key为Y轴的展示的名称,value对应X轴的值。例:{legendName:'取消关注','2020-12-12':10,'2020-12-13':11,'2020-12-14':33}[])
- * @param style width,height
-*/
-// [
-//     { name: '4341556373', value: 300 }, { name: '4341523456', value: 240 },
-// ]
-function BarMonitor(props: {
-    data?: { legendName: string, [key: string]: any }[],
-    style?: React.CSSProperties,
-    fontColor?: string,//文字颜色
-    title?: string,//标题
-    isGraphic?: boolean, // 是否渐变
-    xName?: string,
-    yName?: string,
-    planID?: string,
-    onChange?: (id?: string) => void,
-}) {
-    const { data, style, fontColor, title, xName, yName, planID, onChange } = props
-    const colors = ['#f8128d', '#fa40a3', '#f56db5', '#f58bc4', '#f7a1cf', '#f5b7d8', '#facee5', '#f8c8e2', '#f8d6e8', '#fae8f1']
-    const ref: { current: HTMLDivElement | null } = useRef(null)
-    const [myChart, setMyChart] = useState<any>()
-    let textStyle = fontColor ? { textStyle: { color: fontColor } } : {}
-
-    /**创建myChart实例 */
     useEffect(() => {
-        let myChart: any = null
-        if (ref?.current && (data as any[]).length > 0) {
-            let Xdata: any = []     // name
-            let Ydatas: any[] = []  // value
-            data?.forEach((item) => {
-                Xdata.push(item.adName)
-                Ydatas.push(item.value)
-            })
-            let dataIndex = data?.findIndex((item: any) => item?.name == planID)
-            Ydatas = Ydatas.map((item: number, index: number) => {
-                if (dataIndex !== -1 && dataIndex === index) {
-                    return {
-                        value: item,
-                        itemStyle: {
-                            color: '#37A2FF',
-                            shadowColor: 'rgba(0, 0, 0, 0.8)',
-                            shadowBlur: 10,
-                        },
-                    }
-                } else {
-                    return {
-                        value: item,
-                        itemStyle: {
-                            color: colors[index],
-                        }
+        if (!chartInstance.current) return;
+        if (!data.length) {
+            chartInstance.current.clear();
+            return;
+        }
+
+        const option: echarts.EChartsOption = {
+            color,
+            title: { text: title, left: 'center' },
+            tooltip: { trigger: 'item' },
+            legend: { bottom: 0, left: 'center' },
+            series: [
+                {
+                    type: 'pie',
+                    radius: '60%',
+                    data,
+                    label: { formatter: '{b}: {d}%', color: '#333' },
+                    emphasis: {
+                        itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0,0,0,0.3)' }
                     }
                 }
+            ]
+        };
 
-            })
-            myChart = init(ref.current)
-            setMyChart(myChart)
-            myChart?.setOption({
-                /**鼠标悬浮 */
-                // tooltip: {
-                //     trigger: 'axis',
-                // },
-                xAxis: {
-                    name: xName || '',
-                    max: 'dataMax',
-                    nameTextStyle: {  // y轴名字文字样式设置
-                        verticalAlign: "bottom",
-                        lineHeight: 5,
-                        fontWeight: 'bold',
-                    },
-                    axisLine: {  // 坐标轴轴线相关设置
-                        show: true   // 是否显示坐标轴轴线
-                    },
-                },
-                yAxis: {
-                    name: yName || '',
-                    type: 'category',
-                    data: Xdata,
-                    inverse: true,
-                    nameLocation: 'start',  // y轴名字显示位置 
-                    nameTextStyle: {  // y轴名字文字样式设置
-                        verticalAlign: "bottom",
-                        lineHeight: 0,
-                        fontWeight: 'bold',
-                        color: '#000',
-                        padding: [0, 80, 0, 0]
-                    },
-                    nameGap: 20,
-                    triggerEvent: true,
-                    axisLabel: {//名称
-                        margin: 10,
-                        width: 80,
-                        overflow: 'truncate',
-                        ellipsis: '...',
-                        color: (value: any, index: any) => {
-                            if (index === dataIndex) {
-                                return '#f8128d'
-                            }
-                            return '#3946c3'
-                        },
-                    },
-                },
-                grid: {
-                    left: 100,
-                    right: 70
-                },
-                ...textStyle,
-                title: {
-                    text: title || '起量广告排行榜',
-                    textStyle: {
-                        color: '#3946c3',
-                        fontSize: 16
-                    },
-                    left: '45%',
-                    top: 330,
-                },
-                tooltip: {
-                    show: true,
-                    // formatter:'{b0}: {c0}',
-                    formatter: function (a: any) {
-                        return `${a.marker}  <strong> ${a.name} : ${a.value}</strong>`
-                    }
-                },
-                series: [
-                    {
-                        type: 'bar',//图形
-                        realtimeSort: true,
-                        data: Ydatas,//数据
-                        name: 'X',
-                        showBackground: true,
-                        backgroundStyle: {
-                            color: 'transparent'
-                        },
-                        label: {
-                            show: true,
-                            valueAnimation: true,
-                            position: 'right',
-                            fontSize: 14,
-                            fontWeight: 900
-                        }
-                    }
-                ]
-            });
-            myChart.on('click', (params: any) => {
-                let v = params.dataIndex
-                let d: any = (data as any[])[v]
-                onChange && onChange(d.name)
-            });
-        }
-        return () => {
-            if (myChart?.id) {
-                dispose(myChart.id)
-            }
-        }
-    }, [ref?.current, data, planID])
-    /**卸载myChart */
-    useEffect(() => {
-        if ((data as any[])?.length === 0) {
-            if (myChart?.id) {
-                dispose(myChart?.id)
-            }
-        }
-        return () => {
-            if (myChart?.id) {
-                dispose(myChart.id)
-            }
-        }
-    }, [myChart?.id, data])
-    return <>
-        {
-            (data as any[])?.length > 0 && <div ref={ref} style={style ? style : { width: '100%', height: '100%' }} />
+        loading ? chartInstance.current.showLoading() : chartInstance.current.hideLoading();
+        chartInstance.current.setOption(option, true);
+    }, [data, title, color, loading]);
 
-        }
-        {
-            (data as any[])?.length === 0 && <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
-                <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
-            </div>
-        }
-    </>
+    return <div ref={chartRef} style={style} />;
+};
+
+/* =====================================================
+   📊 柱状图 Bar(支持横向)
+===================================================== */
+interface BarProps {
+    data?: { name: string; value: number }[];
+    title?: string;
+    color?: string[];
+    horizontal?: boolean;
+    style?: React.CSSProperties;
+    loading?: boolean;
 }
 
-/**
- * Line 折线图
- * @param data legendName 图列名称 y:x(key为Y轴的展示的名称,value对应X轴的值。例:{legendName:'取消关注','2020-12-12':10,'2020-12-13':11,'2020-12-14':33}[])
- * @param style width,height
-*/
-function LineMonitor(props: {
-    data?: { legendName: string, [key: string]: any }[],
-    style?: React.CSSProperties,
-    areaStyle?: boolean,//线变块颜色
-    color?: string | string[],//线颜色
-    markPoint?: any,//浮点
-    fontColor?: string,//文字颜色
-    series?: boolean,//堆叠
-    title?: string,//标题
-    smooth?: boolean,//true 圆弧线 false 直角
-    dataZoomInside?: boolean,  // 控制是否开启滚轮缩放
-    dataZoomSlider?: boolean,  // 控制是否开启组件缩放
-}) {
-    const { data, style, areaStyle = false, color, markPoint, fontColor, series, title, smooth = false, dataZoomInside, dataZoomSlider } = props
-    const ref: { current: HTMLDivElement | null } = useRef(null)
-    const [myChart, setMyChart] = useState<any>()
-    let textStyle = fontColor ? { textStyle: { color: fontColor } } : {}
-    /**创建myChart实例 */
+const Bar: React.FC<BarProps> = ({
+    data = [],
+    title,
+    color = DEFAULT_COLORS,
+    horizontal = false,
+    style = { width: '100%', height: 400 },
+    loading = false
+}) => {
+    const { chartRef, chartInstance } = useEchartInit();
+
     useEffect(() => {
-        let myChart: any = null
-        if (ref?.current && (data as any[]).length > 0) {
-            let Xdata: any = []
-            let Ydatas: any[] = []
-            data?.forEach((item, index) => {
-                Ydatas.push([])
-                Object.keys(item).forEach((key: string) => {
-                    if (index === 0 && key !== 'legendName') {
-                        Xdata.push(key)
-                    }
-                    if (key !== 'legendName') {
-                        Ydatas[index].push(item[key])
+        if (!chartInstance.current) return;
+        if (!data.length) {
+            chartInstance.current.clear();
+            return;
+        }
+
+        const names = data.map(d => d.name);
+        const values = data.map(d => d.value);
+
+        const option: echarts.EChartsOption = {
+            color,
+            title: { text: title, left: 'center' },
+            tooltip: { trigger: 'axis' },
+            grid: horizontal
+                ? { left: 80, right: 50, top: 60, bottom: 60 }
+                : { left: 60, right: 40, top: 60, bottom: 100 },
+            xAxis: horizontal
+                ? { type: 'value' }
+                : {
+                    type: 'category',
+                    data: names,
+                    axisLabel: {
+                        interval: 0,
+                        rotate: 30, // 防止长文本遮挡
+                        overflow: 'truncate'
                     }
-                })
-            })
-            myChart = init(ref.current)
-            // 设置是否可缩放
-            let dataZoom = []
-            if (dataZoomInside) {
-                dataZoom.push({
-                    type: 'inside'
-                })
-            }
-            if (dataZoomSlider) {
-                dataZoom.push({
-                    type: 'slider',
-                    width: 200,
-                    height: 20,
-                    top: 0,
-                    left: 140,
-                    disabled: dataZoomSlider
-                })
-            }
-            setMyChart(myChart)
-            myChart.setOption({
-                /**鼠标悬浮 */
-                tooltip: {
-                    trigger: 'axis'
-                },
-                /**图列 */
-                legend: {
-                    data: (data as { legendName: string, [key: string]: any }[]).map((item: { legendName: string }) => item.legendName),
-                    bottom: 0,
-                    left: 'center',
-                    show: data && data?.length > 1 ? true : false,//数据只有一条就隐藏
-                    ...textStyle
-                },
-                xAxis: {
-                    data: Xdata,
-                    boundaryGap: false,//贴边
-                },
-                yAxis: [
-                    {
-                        type: 'value',
-                        axisLabel: {//名称
-                            fontWeight: 900,
-                            fontSize: 15,
-                            color: '#333333',
-                            textBorderColor: '#dfdfdf',
-                            textBorderWidth: 1
-                        },
-                    },
-                ],
-                dataZoom,
-                grid: {
-                    left: 100,
-                    right: 50,
                 },
-                ...textStyle,
-                /**工具保存为图片 */
-                toolbox: {
-                    right: 50,
-                    // feature: {
-                    //     saveAsImage: {
-                    //         title: '保存为图片'
-                    //     }
-                    // }
-                },
-                title: {
-                    text: title || '',
-                    left: '45%',
-                    top: 330,
-                    textStyle: {
-                        color: '#3946c3',
-                        fontSize: 16
-                    },
-                },
-                /**线图参数 */
-                series: (data as { legendName: string, [key: string]: any }[]).map((item: { legendName: string }, index: number) => {
-                    return {
-                        name: item.legendName,//名称
-                        type: 'line',//图形
-                        data: Ydatas[index],//数据
-                        areaStyle: areaStyle ? {} : undefined,//是否开启块级颜色
-                        color: Array.isArray(color) ? color[index] : color,//颜色
-                        markPoint: {//最大值最小值
-                            data: markPoint ? markPoint : []
-                        },
-                        emphasis: series ? { focus: 'series' } : {}, //堆叠
-                        smooth
-                    }
-                })
-            });
-        }
-        return () => {
-            if (myChart?.id) {
-                dispose(myChart.id)
-            }
-        }
-    }, [ref?.current, data])
-    /**卸载myChart */
-    useEffect(() => {
-        if ((data as any[])?.length === 0) {
-            if (myChart?.id) {
-                dispose(myChart?.id)
-            }
-        }
-        return () => {
-            if (myChart?.id) {
-                dispose(myChart.id)
-            }
-        }
-    }, [myChart?.id, data])
-    return <>
-        {
-            (data as any[])?.length > 0 && <div ref={ref} style={style ? style : { width: '100%', height: '100%' }} />
+            yAxis: horizontal
+                ? { type: 'category', data: names }
+                : { type: 'value' },
+            series: [
+                {
+                    type: 'bar',
+                    data: values,
+                    barWidth: '50%',
+                    label: { show: true, position: horizontal ? 'right' : 'top' },
+                    itemStyle: { borderRadius: 4 }
+                }
+            ]
+        };
 
-        }
-        {
-            (data as any[])?.length === 0 && <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
-                <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
-            </div>
-        }
-    </>
-}
+        loading ? chartInstance.current.showLoading() : chartInstance.current.hideLoading();
+        chartInstance.current.setOption(option, true);
+    }, [data, title, color, horizontal, loading]);
 
-/**
- * 常用option配置//https://echarts.apache.org/next/zh/option.html#legend
- * @param legend 图例组件 用于用户操作筛选图表展示的组件
- * @param title 图标标题 
- * @param grid 网格控制 可在一个渲染框架中渲染多个图表 或单个图表的间距宽高等值的设定
- * @param xAxis grid 中的 x 轴的设置
- * @param yAxis grid 中的 y 轴的设置
- * @param tooltip 鼠标悬浮的提示组件
- * @param series 系列数据配置
- */
-function useEcharts() {
-    return {
-        Echarts,
-        Map,
-        PieFocus,
-        Line,
-        Bar,
-        BarMonitor,
-        LineMonitor
-    }
-}
-export default useEcharts
+    return <div ref={chartRef} style={style} />;
+};
+
+/* =====================================================
+   导出 Hook
+===================================================== */
+const useEcharts = () => ({ Echarts, Line, Pie, Bar });
+export default useEcharts;

+ 49 - 0
src/pages/weComTask/API/home/index.ts

@@ -0,0 +1,49 @@
+import request from "@/utils/request";
+const { api } = process.env.CONFIG;
+
+/**
+ * 用户重粉次数统计 企业维度
+ * @returns 
+ */
+export function getExternalUserRepeatCorpApi() {
+    return request({
+        url: api + `/corpOperation/ads/corp/externalUserRepeatCorp`,
+        method: 'GET'
+    })
+}
+
+/**
+ * 用户重粉次数统计 客服号维度
+ * @returns 
+ */
+export function getExternalUserRepeatCorpUserApi() {
+    return request({
+        url: api + `/corpOperation/ads/corp/externalUserRepeatCorpUser`,
+        method: 'GET'
+    })
+}
+
+/**
+ * 用户重粉次数统计 主体维度
+ * @returns 
+ */
+export function getCorpExternalUserRepeatListApi(data: { pageNum: number, pageSize: number, corpName?: string }) {
+    return request({
+        url: api + `/corpOperation/ads/corp/corpExternalUserRepeat`,
+        method: 'POST',
+        data
+    })
+}
+
+/**
+ * 主体重粉率统计
+ * @param data 
+ * @returns 
+ */
+export function getExternalUserRepeatByCorpListApi(data: { pageNum: number, pageSize: number, corpName?: string }) {
+    return request({
+        url: api + `/corpOperation/ads/corp/externalUserRepeatByCorp`,
+        method: 'POST',
+        data
+    })
+}

+ 14 - 0
src/pages/weComTask/page/home/index.less

@@ -0,0 +1,14 @@
+.item {
+    display: flex;
+    padding: 10px;
+    background-color: #f9fafb;
+    border-radius: 8px;
+    justify-content: space-between;
+    align-items: center;
+    font-size: 16px;
+
+    .num {
+        display: flex;
+        font-weight: 700;
+    }
+}

+ 534 - 0
src/pages/weComTask/page/home/index.tsx

@@ -0,0 +1,534 @@
+import { useAjax } from '@/Hook/useAjax';
+import React, { useEffect, useState } from 'react';
+import { getCorpExternalUserRepeatListApi, getExternalUserRepeatByCorpListApi, getExternalUserRepeatCorpApi, getExternalUserRepeatCorpUserApi } from '../../API/home';
+import { Avatar, Card, Col, Flex, Input, Row, Spin, Statistic, Table, Tabs, Typography } from 'antd';
+import { BarChartOutlined, GlobalOutlined, RetweetOutlined, UserOutlined } from '@ant-design/icons';
+import useEcharts from '@/Hook/useEcharts';
+const { Title } = Typography;
+import style from './index.less'
+
+const Home: React.FC = () => {
+
+    /*******************************************/
+    const { Bar, Pie } = useEcharts()
+    const [queryParmas, setQueryParmas] = useState<{ pageNum: number, pageSize: number, corpName?: string }>({ pageNum: 1, pageSize: 20 })
+    const [queryParmasZt, setQueryParmasZt] = useState<{ pageNum: number, pageSize: number, corpName?: string }>({ pageNum: 1, pageSize: 20 })
+    const [corpRepeat, setCorpRepeat] = useState<{ [x: string]: any }>({})
+    const [corpUserRepeat, setCorpUserRepeat] = useState<{ [x: string]: any }>({})
+    const [barCorpData, setBarCorpData] = useState<{ name: string, value: number }[]>([])
+    const [barCorpUserData, setBarCorpUserData] = useState<{ name: string, value: number }[]>([])
+    const [activeKey, setActiveKey] = useState<string>('1')
+    const [pieData, setPieData] = useState<{ name: string, value: number }[]>([])
+    const [overflowData, setOverflowData] = useState<{ avgCorpRepeatUserRate: number, repeatUserRate: number, userCount: number }>({ avgCorpRepeatUserRate: 0, repeatUserRate: 0, userCount: 0 })
+
+    const getExternalUserRepeatCorp = useAjax(() => getExternalUserRepeatCorpApi())
+    const getExternalUserRepeatCorpUser = useAjax(() => getExternalUserRepeatCorpUserApi())
+    const getExternalUserRepeatByCorpList = useAjax((params) => getExternalUserRepeatByCorpListApi(params))
+    const getCorpExternalUserRepeatList = useAjax((params) => getCorpExternalUserRepeatListApi(params))
+    /*******************************************/
+
+    useEffect(() => {
+        getExternalUserRepeatByCorpList.run(queryParmas).then(res => {
+            if (res?.data?.records?.length) {
+                setPieData(res?.data?.records?.map(item => {
+                    return { name: item.corpName, value: item.externalUserRepeatCount }
+                }))
+            } else {
+                setPieData([])
+            }
+        })
+    }, [queryParmas])
+
+    useEffect(() => {
+        getCorpExternalUserRepeatList.run(queryParmasZt)
+    }, [queryParmasZt])
+
+    useEffect(() => {
+        getExternalUserRepeatCorp.run().then(res => {
+            if (res?.data) {
+                const cr = res.data
+                setCorpRepeat(cr)
+                setBarCorpData([
+                    { name: '非重复添加', value: cr?.oneRepeatCount },
+                    { name: '重复添加2名', value: cr?.twoRepeatCount },
+                    { name: '重复添加3名', value: cr?.threeRepeatCount },
+                    { name: '重复添加4名', value: cr?.fourRepeatCount },
+                    { name: '重复添加5名', value: cr?.fiveRepeatCount },
+                    { name: '重复添加5名以上', value: cr?.gtFiveRepeatCount }
+                ])
+            } else {
+                setCorpRepeat({})
+                setBarCorpData([])
+            }
+
+        })
+        getExternalUserRepeatCorpUser.run().then(res => {
+            if (res?.data) {
+                const cur = res.data
+                setCorpUserRepeat(cur)
+                setOverflowData({ avgCorpRepeatUserRate: cur?.avgCorpRepeatUserRate || 0, repeatUserRate: cur?.repeatUserRate || 0, userCount: cur?.userCount || 0 })
+                setBarCorpUserData([
+                    { name: '非重复添加', value: cur?.oneRepeatCount },
+                    { name: '重复添加2名', value: cur?.twoRepeatCount },
+                    { name: '重复添加3名', value: cur?.threeRepeatCount },
+                    { name: '重复添加4名', value: cur?.fourRepeatCount },
+                    { name: '重复添加5名', value: cur?.fiveRepeatCount },
+                    { name: '重复添加5名以上', value: cur?.gtFiveRepeatCount }
+                ])
+            } else {
+                setCorpUserRepeat({})
+                setOverflowData({ avgCorpRepeatUserRate: 0, repeatUserRate: 0, userCount: 0 })
+                setBarCorpUserData([])
+            }
+
+        })
+    }, [])
+
+    return <div>
+        <Spin spinning={getExternalUserRepeatCorpUser.loading}>
+            <Row gutter={16}>
+                <Col span={8}>
+                    <Card variant="borderless">
+                        <Flex justify='space-between'>
+                            <Statistic
+                                title={<strong style={{ fontSize: 14 }}>集团总粉丝数</strong>}
+                                value={overflowData.userCount}
+                            />
+                            <Avatar style={{ backgroundColor: '#DBEAFE', color: '#2563eb' }} size={40}><UserOutlined /></Avatar>
+                        </Flex>
+                    </Card>
+                </Col>
+                <Col span={8}>
+                    <Card variant="borderless">
+                        <Flex justify='space-between'>
+                            <Statistic
+                                title={<strong style={{ fontSize: 14 }}>平均主体重粉率</strong>}
+                                value={overflowData.avgCorpRepeatUserRate ? overflowData.avgCorpRepeatUserRate * 100 : 0}
+                                precision={4}
+                                suffix="%"
+                            />
+                            <Avatar style={{ backgroundColor: '#DCFCE7', color: '#16a34a' }} size={40}><RetweetOutlined /></Avatar>
+                        </Flex>
+                    </Card>
+                </Col>
+                <Col span={8}>
+                    <Card variant="borderless">
+                        <Flex justify='space-between'>
+                            <Statistic
+                                title={<strong style={{ fontSize: 14 }}>集团重粉率</strong>}
+                                value={overflowData.repeatUserRate ? overflowData.repeatUserRate * 100 : 0}
+                                precision={4}
+                                suffix="%"
+                            />
+                            <Avatar style={{ backgroundColor: '#F3E8FF', color: '#9333ea' }} size={40}><GlobalOutlined /></Avatar>
+                        </Flex>
+
+                    </Card>
+                </Col>
+            </Row>
+        </Spin>
+
+        <Spin spinning={getExternalUserRepeatByCorpList.loading}>
+            <Flex justify='space-between' style={{ margin: '20px 0 10px' }}>
+                <Title level={3} style={{ margin: 0 }}><RetweetOutlined style={{ color: '#1890ff' }} /> 主体重粉次数统计</Title>
+                <Input.Search
+                    placeholder="请输入企业名称"
+                    onSearch={(e) => { setQueryParmas({ ...queryParmas, corpName: e, pageNum: 1 }); }}
+                    style={{ width: 200 }}
+                    allowClear
+                />
+            </Flex>
+            <Row gutter={16}>
+                <Col span={12}>
+                    <Card style={{ height: '100%' }}>
+                        <Pie data={pieData} title="主体重粉占比" />
+                    </Card>
+                </Col>
+                <Col span={12}>
+                    <Card style={{ height: '100%' }}>
+                        <Table
+                            columns={[
+                                {
+                                    title: '企业名称',
+                                    dataIndex: 'corpName',
+                                    key: 'corpName',
+                                    ellipsis: true,
+                                    width: 150,
+                                },
+                                {
+                                    title: '集团粉丝总数',
+                                    dataIndex: 'totalExternalUserCount',
+                                    key: 'totalExternalUserCount',
+                                    align: 'center',
+                                    render: (text: any) => <Statistic value={text || 0} valueStyle={{ fontSize: 12 }} />
+                                },
+                                {
+                                    title: '集团内重粉数',
+                                    dataIndex: 'externalUserRepeatCount',
+                                    key: 'externalUserRepeatCount',
+                                    align: 'center',
+                                    render: (text: any) => <Statistic value={text || 0} valueStyle={{ fontSize: 12 }} />
+                                },
+                                {
+                                    title: '集团内重粉率',
+                                    dataIndex: 'externalUserRepeatRate',
+                                    key: 'externalUserRepeatRate',
+                                    align: 'center',
+                                    render: (text: any) => <Statistic
+                                        value={text ? text * 100 : 0}
+                                        valueStyle={text > 0.2 ? { color: '#cf1322', fontSize: 12 } : { color: '#3f8600', fontSize: 12 }}
+                                        suffix="%"
+                                        precision={4}
+                                    />
+                                },
+                                {
+                                    title: '主体粉丝总数',
+                                    dataIndex: 'corpExternalUserCount',
+                                    key: 'corpExternalUserCount',
+                                    align: 'center',
+                                    render: (text: any) => <Statistic value={text || 0} valueStyle={{ fontSize: 12 }} />
+                                },
+                                {
+                                    title: '主体粉丝在集团占比',
+                                    dataIndex: 'corpExternalUserRate',
+                                    key: 'corpExternalUserRate',
+                                    align: 'center',
+                                    render: (text: any) => <Statistic
+                                        value={text ? text * 100 : 0}
+                                        valueStyle={text > 0.2 ? { color: '#cf1322', fontSize: 12 } : { color: '#3f8600', fontSize: 12 }}
+                                        suffix="%"
+                                        precision={4}
+                                    />
+                                },
+                                {
+                                    title: '主体客服号数量',
+                                    dataIndex: 'corpUserCount',
+                                    key: 'corpUserCount',
+                                    align: 'center',
+                                    render: (text: any) => <Statistic value={text || 0} valueStyle={{ fontSize: 12 }} />
+                                },
+                                {
+                                    title: '主体内重粉数',
+                                    dataIndex: 'corpExternalUserRepeatCount',
+                                    key: 'corpExternalUserRepeatCount',
+                                    align: 'center',
+                                    render: (text: any) => <Statistic value={text || 0} valueStyle={{ fontSize: 12 }} />
+                                },
+                                {
+                                    title: '主体内重粉率',
+                                    dataIndex: 'corpExternalUserRepeatRate',
+                                    key: 'corpExternalUserRepeatRate',
+                                    align: 'center',
+                                    render: (text: any) => <Statistic
+                                        value={text ? text * 100 : 0}
+                                        valueStyle={text > 0.2 ? { color: '#cf1322', fontSize: 12 } : { color: '#3f8600', fontSize: 12 }}
+                                        suffix="%"
+                                        precision={4}
+                                    />
+                                },
+                            ]}
+                            scroll={{ y: 300, x: 900 }}
+                            bordered
+                            dataSource={getExternalUserRepeatByCorpList.data?.data?.records}
+                            loading={getExternalUserRepeatByCorpList.loading}
+                            rowKey="corpId"
+                            pagination={{
+                                total: getExternalUserRepeatByCorpList.data?.data?.total,
+                                current: getExternalUserRepeatByCorpList?.data?.data?.current || 1,
+                                pageSize: getExternalUserRepeatByCorpList?.data?.data?.size || 20,
+                                onChange: (page: number, pageSize: number) => {
+                                    setQueryParmas({ ...queryParmas, pageNum: page, pageSize })
+                                }
+                            }}
+                        />
+                    </Card>
+                </Col>
+            </Row>
+        </Spin>
+
+        <Spin spinning={getExternalUserRepeatCorp.loading || getExternalUserRepeatCorpUser.loading || getCorpExternalUserRepeatList.loading}>
+            <Title level={3}><BarChartOutlined style={{ color: '#22c55e' }} /> 用户重粉次数统计</Title>
+            <Tabs
+                tabBarExtraContent={activeKey === '1' && <Input.Search
+                    placeholder="请输入企业名称"
+                    onSearch={(e) => { setQueryParmasZt({ ...queryParmasZt, corpName: e, pageNum: 1 }); }}
+                    style={{ width: 200 }}
+                    allowClear
+                />}
+                items={[
+                    {
+                        key: '1',
+                        label: '主体维度',
+                        children: <Card>
+                            <Table
+                                columns={[
+                                    {
+                                        title: '企业名称',
+                                        dataIndex: 'corpName',
+                                        key: 'corpName',
+                                        ellipsis: true,
+                                        width: 150,
+                                    },
+                                    {
+                                        title: '粉丝总数',
+                                        dataIndex: 'userCount',
+                                        key: 'userCount',
+                                        align: 'center',
+                                        render: (text: any) => <Statistic value={text || 0} valueStyle={{ fontSize: 12 }} />
+                                    },
+                                    {
+                                        title: '非重复添加人数',
+                                        dataIndex: 'oneRepeatCount',
+                                        key: 'oneRepeatCount',
+                                        align: 'center',
+                                        render: (text: any) => <Statistic value={text || 0} valueStyle={{ fontSize: 12 }} />
+                                    },
+                                    {
+                                        title: '非重复添加人数比例',
+                                        dataIndex: 'oneRepeatCountRate',
+                                        key: 'oneRepeatCountRate',
+                                        align: 'center',
+                                        render: (text: any) => <Statistic
+                                            value={text ? text * 100 : 0}
+                                            valueStyle={text > 0.5 ? { color: '#cf1322', fontSize: 12 } : { color: '#3f8600', fontSize: 12 }}
+                                            suffix="%"
+                                            precision={4}
+                                        />
+                                    },
+                                    {
+                                        title: '重复添加2名人数',
+                                        dataIndex: 'twoRepeatCount',
+                                        key: 'twoRepeatCount',
+                                        align: 'center',
+                                        render: (text: any) => <Statistic value={text || 0} valueStyle={{ fontSize: 12 }} />
+                                    },
+                                    {
+                                        title: '重复添加2名人数比例',
+                                        dataIndex: 'twoRepeatCountRate',
+                                        key: 'twoRepeatCountRate',
+                                        align: 'center',
+                                        render: (text: any) => <Statistic
+                                            value={text ? text * 100 : 0}
+                                            valueStyle={text > 0.1 ? { color: '#cf1322', fontSize: 12 } : { color: '#3f8600', fontSize: 12 }}
+                                            suffix="%"
+                                            precision={4}
+                                        />
+                                    },
+                                    {
+                                        title: '重复添加3名人数',
+                                        dataIndex: 'threeRepeatCount',
+                                        key: 'threeRepeatCount',
+                                        align: 'center',
+                                        render: (text: any) => <Statistic value={text || 0} valueStyle={{ fontSize: 12 }} />
+                                    },
+                                    {
+                                        title: '重复添加3名人数比例',
+                                        dataIndex: 'threeRepeatCountRate',
+                                        key: 'threeRepeatCountRate',
+                                        align: 'center',
+                                        render: (text: any) => <Statistic
+                                            value={text ? text * 100 : 0}
+                                            valueStyle={text > 0.09 ? { color: '#cf1322', fontSize: 12 } : { color: '#3f8600', fontSize: 12 }}
+                                            suffix="%"
+                                            precision={4}
+                                        />
+                                    },
+                                    {
+                                        title: '重复添加4名人数',
+                                        dataIndex: 'fourRepeatCount',
+                                        key: 'fourRepeatCount',
+                                        align: 'center',
+                                        render: (text: any) => <Statistic value={text || 0} valueStyle={{ fontSize: 12 }} />
+                                    },
+                                    {
+                                        title: '重复添加4名人数比例',
+                                        dataIndex: 'fourRepeatCountRate',
+                                        key: 'fourRepeatCountRate',
+                                        align: 'center',
+                                        render: (text: any) => <Statistic
+                                            value={text ? text * 100 : 0}
+                                            valueStyle={text > 0.08 ? { color: '#cf1322', fontSize: 12 } : { color: '#3f8600', fontSize: 12 }}
+                                            suffix="%"
+                                            precision={4}
+                                        />
+                                    },
+                                    {
+                                        title: '重复添加5名人数',
+                                        dataIndex: 'fiveRepeatCount',
+                                        key: 'fiveRepeatCount',
+                                        align: 'center',
+                                        render: (text: any) => <Statistic value={text || 0} valueStyle={{ fontSize: 12 }} />
+                                    },
+                                    {
+                                        title: '重复添加5名人数比例',
+                                        dataIndex: 'fiveRepeatCountRate',
+                                        key: 'fiveRepeatCountRate',
+                                        align: 'center',
+                                        render: (text: any) => <Statistic
+                                            value={text ? text * 100 : 0}
+                                            valueStyle={text > 0.07 ? { color: '#cf1322', fontSize: 12 } : { color: '#3f8600', fontSize: 12 }}
+                                            suffix="%"
+                                            precision={4}
+                                        />
+                                    },
+                                    {
+                                        title: '重复添加5名以上人数',
+                                        dataIndex: 'gtFiveRepeatCount',
+                                        key: 'gtFiveRepeatCount',
+                                        align: 'center',
+                                        render: (text: any) => <Statistic value={text || 0} valueStyle={{ fontSize: 12 }} />
+                                    },
+                                    {
+                                        title: '重复添加5名以上人数比例',
+                                        dataIndex: 'gtFiveRepeatCountRate',
+                                        key: 'gtFiveRepeatCountRate',
+                                        align: 'center',
+                                        render: (text: any) => <Statistic
+                                            value={text ? text * 100 : 0}
+                                            valueStyle={text > 0.06 ? { color: '#cf1322', fontSize: 12 } : { color: '#3f8600', fontSize: 12 }}
+                                            suffix="%"
+                                            precision={4}
+                                        />
+                                    },
+                                ]}
+                                scroll={{ y: 300, x: 900 }}
+                                bordered
+                                dataSource={getCorpExternalUserRepeatList.data?.data?.records}
+                                loading={getCorpExternalUserRepeatList.loading}
+                                rowKey="corpId"
+                                pagination={{
+                                    total: getCorpExternalUserRepeatList.data?.data?.total,
+                                    current: getCorpExternalUserRepeatList?.data?.data?.current || 1,
+                                    pageSize: getCorpExternalUserRepeatList?.data?.data?.size || 20,
+                                    onChange: (page: number, pageSize: number) => {
+                                        setQueryParmasZt({ ...queryParmasZt, pageNum: page, pageSize })
+                                    }
+                                }}
+                            />
+                        </Card>
+                    },
+                    {
+                        key: '2',
+                        label: '企业维度',
+                        children: <Row gutter={16}>
+                            <Col span={12}>
+                                <Card style={{ height: '100%' }}>
+                                    <Bar data={barCorpData} title="企业维度重粉次数分布" horizontal />
+                                </Card>
+                            </Col>
+                            <Col span={12}>
+                                <DetailsTemplate data={corpRepeat} title='企业维度重粉详细数据' />
+                            </Col>
+                        </Row>
+                    },
+                    {
+                        key: '3',
+                        label: '客服号维度',
+                        children: <Row gutter={16}>
+                            <Col span={12}>
+                                <Card style={{ height: '100%' }}>
+                                    <Bar data={barCorpUserData} title="客服号维度重粉次数分布" horizontal />
+                                </Card>
+                            </Col>
+                            <Col span={12}>
+                                <DetailsTemplate data={corpUserRepeat} title='客服号维度重粉详细数据' />
+                            </Col>
+                        </Row>
+                    },
+                ]}
+                onChange={(e) => { setActiveKey(e) }}
+                activeKey={activeKey}
+            />
+        </Spin>
+    </div>
+};
+
+const DetailsTemplate: React.FC<{ data: { [x: string]: any }, title: string }> = ({ data, title }) => {
+
+
+    return <Card style={{ height: '100%' }}>
+        <Title level={3} style={{ marginTop: 0, textAlign: 'center', fontSize: 18, color: '#313131' }}>{title || '详细数据'}</Title>
+        <Flex vertical gap={7}>
+            <div className={style.item}>
+                <span>粉丝总数</span>
+                <div className={style.num}>
+                    <Statistic value={data?.userCount || 0} valueStyle={{ fontSize: 16 }} />
+                </div>
+            </div>
+            <div className={style.item}>
+                <span>非重复添加人数</span>
+                <div className={style.num}>
+                    <Statistic value={data?.oneRepeatCount || 0} valueStyle={{ fontSize: 16 }} />
+                    (<Statistic
+                        value={data?.oneRepeatCountRate ? data?.oneRepeatCountRate * 100 : 0}
+                        valueStyle={data?.oneRepeatCountRate > 0.5 ? { color: '#cf1322', fontSize: 16 } : { color: '#3f8600', fontSize: 16 }}
+                        suffix="%"
+                        precision={4}
+                    />)
+                </div>
+            </div>
+            <div className={style.item}>
+                <span>重复添加2名人数</span>
+                <div className={style.num}>
+                    <Statistic value={data?.twoRepeatCount || 0} valueStyle={{ fontSize: 16 }} />
+                    (<Statistic
+                        value={data?.twoRepeatCountRate ? data?.twoRepeatCountRate * 100 : 0}
+                        valueStyle={data?.twoRepeatCountRate > 0.1 ? { color: '#cf1322', fontSize: 16 } : { color: '#3f8600', fontSize: 16 }}
+                        suffix="%"
+                        precision={4}
+                    />)
+                </div>
+            </div>
+            <div className={style.item}>
+                <span>重复添加3名人数</span>
+                <div className={style.num}>
+                    <Statistic value={data?.threeRepeatCount || 0} valueStyle={{ fontSize: 16 }} />
+                    (<Statistic
+                        value={data?.threeRepeatCountRate ? data?.threeRepeatCountRate * 100 : 0}
+                        valueStyle={data?.threeRepeatCountRate > 0.09 ? { color: '#cf1322', fontSize: 16 } : { color: '#3f8600', fontSize: 16 }}
+                        suffix="%"
+                        precision={4}
+                    />)
+                </div>
+            </div>
+            <div className={style.item}>
+                <span>重复添加4名人数</span>
+                <div className={style.num}>
+                    <Statistic value={data?.fourRepeatCount || 0} valueStyle={{ fontSize: 16 }} />
+                    (<Statistic
+                        value={data?.fourRepeatCountRate ? data?.fourRepeatCountRate * 100 : 0}
+                        valueStyle={data?.fourRepeatCountRate > 0.08 ? { color: '#cf1322', fontSize: 16 } : { color: '#3f8600', fontSize: 16 }}
+                        suffix="%"
+                        precision={4}
+                    />)
+                </div>
+            </div>
+            <div className={style.item}>
+                <span>重复添加5名人数</span>
+                <div className={style.num}>
+                    <Statistic value={data?.fiveRepeatCount || 0} valueStyle={{ fontSize: 16 }} />
+                    (<Statistic
+                        value={data?.fiveRepeatCountRate ? data?.fiveRepeatCountRate * 100 : 0}
+                        valueStyle={data?.fiveRepeatCountRate > 0.07 ? { color: '#cf1322', fontSize: 16 } : { color: '#3f8600', fontSize: 16 }}
+                        suffix="%"
+                        precision={4}
+                    />)
+                </div>
+            </div>
+            <div className={style.item}>
+                <span>重复添加5名以上人数</span>
+                <div className={style.num}>
+                    <Statistic value={data?.gtFiveRepeatCount || 0} valueStyle={{ fontSize: 16 }} />
+                    (<Statistic
+                        value={data?.gtFiveRepeatCountRate ? data?.gtFiveRepeatCountRate * 100 : 0}
+                        valueStyle={data?.gtFiveRepeatCountRate > 0.06 ? { color: '#cf1322', fontSize: 16 } : { color: '#3f8600', fontSize: 16 }}
+                        suffix="%"
+                        precision={4}
+                    />)
+                </div>
+            </div>
+        </Flex>
+    </Card>
+};
+
+export default Home;