react框架ice和ant背景图浮层显示坐标定位锚点世界地图数据动画滚动显示

react框架ice和ant背景图浮层显示坐标定位锚点世界地图数据动画滚动显示

文件index.module.css

.companiesCard {
  :global {
    .ant-tag {
      cursor: pointer;
    }
  }
}
.workforceCompanyCard {
  :global {
    .ant-pro-card-body {
      padding-inline: 10px;
      padding-block: 10px;
    }
    .ant-statistic-title {
      font-size: 12px;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      width: 71.5px;
    }
    .ant-pro-card-statistic-content .ant-statistic-content-value-int {
      font-size: 18px;
    }
  }
}
.workforceExcludeTitle {
  :global {
    .ant-statistic-title {
      width: auto;
    }
  }
}
.title {
  margin-block-start: 0.5em;
  margin-block-end: 0.5em;
  text-align: center;
  font-size: 2em;
}
.chartButtons {
  text-align: center;
}

.countryButtons {
  margin-block-start: 1em;
  :global {
    .china-region {
      background-color: #e57373;
      span {
        color: #fff;
      }
    }

    .china-region:hover {
      background-color: #ef5350 !important;
      span {
        color: #fff;
      }
    }

    .southeast-asia-region {
      background-color: #ba68c8;
      span {
        color: #fff;
      }
    }

    .southeast-asia-region:hover {
      background-color: #ab47bc !important;
      span {
        color: #fff;
      }
    }

    .south-asia-region {
      background-color: #81c784;
      span {
        color: #fff;
      }
    }

    .south-asia-region:hover {
      background-color: #66bb6a !important;
      span {
        color: #fff;
      }
    }

    .america-region {
      background-color: #4fc3f7;
      span {
        color: #fff;
      }
    }

    .america-region:hover {
      background-color: #29b6f6 !important;
      span {
        color: #fff;
      }
    }

    .europe-region {
      background-color: #ffd54f;
      span {
        color: #fff;
      }
    }

    .europe-region:hover {
      background-color: #ffca28 !important;
      span {
        color: #fff;
      }
    }
  }
}

.mapContainer {
  position: relative;
  width: 100%;
  height: 0;
  padding-bottom: 50%; /* 保持 2:1 的宽高比 (600/1200=0.5) */
}

.worldMap {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.annotation {
  position: absolute;
  transform: translate(-50%, -50%); /* 使标注点居中 */
  cursor: pointer;
}

.text {
  font-size: 10px;
  white-space: nowrap;
  padding: 2px;
  color: #e4007f;
}

.text2 {
  font-size: 16px;
  white-space: nowrap;
  padding: 2px;
  color: #000000;
  font-weight: bold;
}

.tooltip {
  position: absolute;
  left: 100%;
  top: 0;
  margin-left: 8px;
  background: white;
  border-radius: 4px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  padding: 8px;
  min-width: 120px;
  z-index: 10;
}

.tooltipItem {
  display: flex;
  justify-content: space-between;
  padding: 2px;
  font-size: 11px;
  white-space: nowrap;
}

.tooltipItem:not(:last-child) {
  border-bottom: 1px solid #f0f0f0;
}

文件NumberRoller.tsx

import React, { useState, useEffect } from 'react';

const NumberRoller = ({ value, duration = 1000 }) => {
  const [displayValue, setDisplayValue] = useState(0);

  useEffect(() => {
    let start = 0;
    const end = parseFloat(value) || 0;
    const increment = end / (duration / 16); // 假设60fps

    const timer = setInterval(() => {
      start += increment;
      if (start >= end) {
        start = end;
        clearInterval(timer);
      }
      setDisplayValue(Math.round(start * 10) / 10); // 保留一位小数
    }, 16);

    return () => clearInterval(timer);
  }, [value, duration]);

  return <span>{displayValue}%</span>;
};

export default NumberRoller;

文件index.tsx

import { ComposableMap, Geographies, Geography } from 'react-simple-maps';
import { ProCard, StatisticCard } from '@ant-design/pro-components';
import { Row, Col, Button, Modal, Popover, Space, Spin, Tag } from 'antd';
import { BankOutlined, CompassOutlined, GlobalOutlined } from '@ant-design/icons';
import { useContext, useEffect, useState } from 'react';
import { useRequest, defineDataLoader, request } from 'ice';
import styles from './index.module.css';
import countryGeoJSON from './ne_110m_admin_0_countries.json';
import NumberRoller from './NumberRoller';
import OrgCharts from '@/components/OrgCharts';
import { ComponentLocaleContext } from '@/contexts/ComponentLocale/ComponentLocaleContext';
import useResponsive from '@/hooks/useResponsive';

const Index = () => {
  const { __ } = useContext(ComponentLocaleContext);
  const { isDesktopOrAbove } = useResponsive();
  const [highlightCountries, setHighlightCountries] = useState<string[]>([]);
  const [previewModoalOpen, setPreviewModoalOpen] = useState(false);
  const [chartId, setChartId] = useState(null);

  const initRequest = useRequest({
    url: '/api/admin/organizational-chart-entities',
    params: {
      pagination: false,
    },
  });

  const workforceRequest = useRequest({
    url: '/api/user/monthly-reports/latest-workforce',
    params: {
      pagination: false,
    },
  });

  useEffect(() => {
    initRequest.request();
    workforceRequest.request();

    return () => {
      initRequest.cancel();
      workforceRequest.cancel();
    };
  }, []);

  const data = initRequest?.data?.data;
  const workforceData = workforceRequest?.data?.data;

  // 世界地图
  // 假设地图图片原始尺寸为 1200x600px
  const mapWidth = 1200;
  const mapHeight = 600;

  // 定义标注点的位置和文字内容(使用百分比)
  const points = [
    {
      id: 11,
      x: (464 / mapWidth) * 100, // 大中华区
      y: (215 / mapHeight) * 100,
      text: workforceData?.area_per['1'],
      details: [
        { name: '浦东', value: '15%' },
        { name: '徐汇', value: '8%' },
      ],
    },
    {
      id: 12,
      x: (425 / mapWidth) * 100, // 东南亚
      y: (357 / mapHeight) * 100,
      text: workforceData?.area_per['2'],
      details: [],
    },
    {
      id: 13,
      x: (334 / mapWidth) * 100, // 南亚区
      y: (270 / mapHeight) * 100,
      text: workforceData?.area_per['3'],
      details: [],
    },
    {
      id: 14,
      x: (921 / mapWidth) * 100, // 美洲区
      y: (155 / mapHeight) * 100,
      text: workforceData?.area_per['4'],
      details: [],
    },
    {
      id: 15,
      x: (225 / mapWidth) * 100, // 欧洲区
      y: (152 / mapHeight) * 100,
      text: workforceData?.area_per['5'],
      details: [],
    },
  ];
  const pointsText = [
    {
      id: 21,
      x: (512 / mapWidth) * 100,
      y: (245 / mapHeight) * 100,
      text: __('title.greater_china_chart'),
      details: [],
    },
    {
      id: 22,
      x: (490 / mapWidth) * 100, // 东南亚
      y: (377 / mapHeight) * 100,
      text: __('title.southeast_asia_chart'),
      details: [],
    },
    {
      id: 23,
      x: (313 / mapWidth) * 100, // 南亚区
      y: (285 / mapHeight) * 100,
      text: __('title.south_asia_chart'),
      details: [],
    },
    {
      id: 24,
      x: (857 / mapWidth) * 100, // 美洲区
      y: (200 / mapHeight) * 100,
      text: __('title.americas_chart'),
      details: [],
    },
    {
      id: 25,
      x: (210 / mapWidth) * 100, // 欧洲区
      y: (175 / mapHeight) * 100,
      text: __('title.europe_chart'),
      details: [],
    },
  ];

  const [hoveredPoint, setHoveredPoint] = useState(null); // 点击后显示详细的框
  // 世界地图 END

  const handleButtonHover = (type: string) => {
    switch (type) {
      case 'greater-china':
        setHighlightCountries(['CHN', 'HKG', 'MAC', 'TWN']);
        break;
      case 'south-asia':
        setHighlightCountries(['IND', 'BGD']);
        break;
      case 'americas':
        setHighlightCountries(['USA', 'GTM']);
        break;
      case 'southeast-asia':
        setHighlightCountries(['VNM', 'KHM', 'THA', 'IDN']);
        break;
      case 'europe':
        setHighlightCountries(['TUR', 'ESP', 'GBR', 'DEU', 'ARE']);
        break;
      default:
        setHighlightCountries([]);
    }
  };

  const openChart = (id) => {
    const chart = (data || []).find((item) => item.id == id && item.active_chart_id !== 0);
    if (chart) {
      setPreviewModoalOpen(true);
      setChartId(chart.active_chart_id);
    }
  };

  const handleButtonClick = (type: string) => {
    switch (type) {
      case 'group':
        openChart(1);
        break;
      case 'center':
        openChart(2);
        break;
      case 'greater-china':
        openChart(3);
        break;
      case 'southeast-asia':
        openChart(4);
        break;
      case 'south-asia':
        openChart(5);
        break;
      case 'americas':
        openChart(6);
        break;
      case 'europe':
        openChart(7);
        break;
      default:
    }
  };

  const clearHighlight = () => {
    setHighlightCountries([]);
  };

  const getWorkforceCountForCompany = (companyId) => {
    return workforceData?.per_company.find((item) => item.company_id == companyId)?.total_workforce_count || 0;
  };

  const getHoverContent = (companyId) => {
    return (
      <div>
        <strong>{workforceData?.per_company.find((item) => item.company_id == companyId)?.company_name || ''}</strong>
        <br />
        {__('ui.data_source_from_monthly_report')}
        <br />
        {__('ui.report_month', {
          month: workforceData?.per_company.find((item) => item.company_id == companyId)?.report_month || '',
        })}
        <br />
        {__('ui.excluding_third_party_employees')}
        <br />
        {__('ui.click_to_view_org_chart')}
      </div>
    );
  };

  return (
    <div>
      <ProCard className={styles.orgChart} direction={isDesktopOrAbove ? 'row' : 'column'}>
        <ProCard
          colSpan={{
            xl: 18,
            lg: 24,
            md: 24,
          }}
        >
          <Spin spinning={initRequest.loading}>
            <h2 className={styles.title}>{__('title.chart_title')}</h2>
            <div className={styles.chartButtons}>
              <div>
                <Space>
                  <Button
                    icon={<BankOutlined />}
                    size="large"
                    onClick={() => {
                      handleButtonClick('group');
                    }}
                  >
                    {__('title.group_org_chart')}
                  </Button>
                  <Button
                    icon={<CompassOutlined />}
                    size="large"
                    onClick={() => {
                      handleButtonClick('center');
                    }}
                  >
                    {__('title.center_org_chart')}
                  </Button>
                </Space>
              </div>

              <div className={styles.countryButtons}>
                <Space>
                  <Button
                    className="china-region"
                    onMouseEnter={() => {
                      handleButtonHover('greater-china');
                    }}
                    onMouseLeave={() => {
                      clearHighlight();
                    }}
                    onClick={() => {
                      handleButtonClick('greater-china');
                    }}
                    size="large"
                  >
                    {__('title.greater_china_chart')}
                  </Button>
                  <Button
                    className="southeast-asia-region"
                    onMouseEnter={() => {
                      handleButtonHover('southeast-asia');
                    }}
                    onMouseLeave={() => {
                      clearHighlight();
                    }}
                    onClick={() => {
                      handleButtonClick('southeast-asia');
                    }}
                    size="large"
                  >
                    {__('title.southeast_asia_chart')}
                  </Button>
                  <Button
                    className="south-asia-region"
                    onMouseEnter={() => {
                      handleButtonHover('south-asia');
                    }}
                    onMouseLeave={() => {
                      clearHighlight();
                    }}
                    onClick={() => {
                      handleButtonClick('south-asia');
                    }}
                    size="large"
                  >
                    {__('title.south_asia_chart')}
                  </Button>
                  <Button
                    className="america-region"
                    onMouseEnter={() => {
                      handleButtonHover('americas');
                    }}
                    onMouseLeave={() => {
                      clearHighlight();
                    }}
                    onClick={() => {
                      handleButtonClick('americas');
                    }}
                    size="large"
                  >
                    {__('title.americas_chart')}
                  </Button>
                  <Button
                    className="europe-region"
                    onMouseEnter={() => {
                      handleButtonHover('europe');
                    }}
                    onMouseLeave={() => {
                      clearHighlight();
                    }}
                    onClick={() => {
                      handleButtonClick('europe');
                    }}
                    size="large"
                  >
                    {__('title.europe_chart')}
                  </Button>
                </Space>
              </div>
            </div>

            {/* 世界地图百分比 began */}
            <Row>
              {/* 添加 style 标签定义动画 */}
              <style>
                {`
      @keyframes blink {
        0% { opacity: 0.2; }
        50% { opacity: 1; }
        100% { opacity: 0.2; }
      }
    `}
              </style>
              <Col span={24}>
                <div className={styles.mapContainer}>
                  <img
                    src="https://a.com/world1.png"
                    alt="World Map"
                    className={styles.worldMap}
                  />

                  {points.map((point, index) => (
                    <div
                      key={index}
                      className={styles.annotation}
                      style={{
                        top: `${point.y}%`,
                        left: `${point.x}%`,
                      }}
                      onMouseEnter={() => setHoveredPoint(point)}
                      onMouseLeave={() => setHoveredPoint(null)}
                    >
                      <div className={styles.text}>
                        <NumberRoller value={point.text} duration={1500} />
                      </div>

                      {hoveredPoint?.id === point.id && (
                        <div className={styles.tooltip} style={{ display: 'none' }}>
                          {point.details.map((detail, i) => (
                            <div key={i} className={styles.tooltipItem}>
                              <span>{detail.name}</span>
                              <span>{detail.value}</span>
                            </div>
                          ))}
                        </div>
                      )}
                    </div>
                  ))}

                  {false &&
                    pointsText.map((point, index) => (
                      <div
                        key={index}
                        className={styles.annotation}
                        style={{
                          top: `${point.y}%`,
                          left: `${point.x}%`,
                        }}
                        onMouseEnter={() => setHoveredPoint(point)}
                        onMouseLeave={() => setHoveredPoint(null)}
                      >
                        <div className={styles.text2}>{point.text}</div>
                      </div>
                    ))}
                </div>
              </Col>
            </Row>
            {/* 居中显示 */}
            <Row justify="center">
              <Button icon={<GlobalOutlined />} size="middle">
                {__('title.Dynamically')}
              </Button>
            </Row>
            {/* 世界地图百分比 end */}

            <ComposableMap width={1200} height={600} style={{ display: 'none' }}>
              <Geographies geography={countryGeoJSON}>
                {({ geographies }) =>
                  geographies.map((geo) => {
                    const isHighlighted = highlightCountries.includes(geo.properties.iso_a3);
                    return <Geography key={geo.rsmKey} geography={geo} fill={isHighlighted ? '#E4007F' : '#D6D6DA'} />;
                  })
                }
              </Geographies>
            </ComposableMap>
          </Spin>
        </ProCard>
        <ProCard
          colSpan={{
            xl: 6,
            lg: 24,
            md: 24,
          }}
          loading={initRequest.loading}
        >
          <ProCard split="horizontal">
            <StatisticCard
              statistic={{
                title: __('title.total_workforce_count'),
                value: workforceData?.include_count,
              }}
            />
            <StatisticCard
              className={`${styles.workforceCompanyCard} ${styles.workforceExcludeTitle}`}
              statistic={{
                title: __('title.total_workforce_count_exclude'),
                value: workforceData?.exclude_count,
              }}
            />
            <ProCard className={styles.companiesCard}>
              <p>{__('title.greater_china_chart')}</p>
              <Popover content={getHoverContent(1)}>
                <Tag
                  color="#e57373"
                  onClick={() => {
                    openChart(8);
                  }}
                >
                  {__('title.shanghai')} {getWorkforceCountForCompany(1)}
                </Tag>
              </Popover>
              <Popover content={getHoverContent(2)}>
                <Tag
                  color="#e57373"
                  onClick={() => {
                    openChart(9);
                  }}
                >
                  {__('title.zhejiang')} {getWorkforceCountForCompany(2)}
                </Tag>
              </Popover>
              <Popover content={getHoverContent(3)}>
                <Tag
                  color="#e57373"
                  onClick={() => {
                    openChart(10);
                  }}
                >
                  {__('title.shenzhen')} {getWorkforceCountForCompany(3)}
                </Tag>
              </Popover>
              <Popover content={getHoverContent(4)}>
                <Tag
                  color="#e57373"
                  onClick={() => {
                    openChart(11);
                  }}
                >
                  {__('title.hongkong')} {getWorkforceCountForCompany(4)}
                </Tag>
              </Popover>
              <Popover content={getHoverContent(19)}>
                <Tag
                  color="#e57373"
                  onClick={() => {
                    openChart(25);
                  }}
                >
                  {__('title.zhejiang_jiasheng')} {getWorkforceCountForCompany(19)}
                </Tag>
              </Popover>
              <Popover content={getHoverContent(20)}>
                <Tag
                  color="#e57373"
                  onClick={() => {
                    openChart(26);
                  }}
                >
                  {__('title.evermaxglobal')} {getWorkforceCountForCompany(20)}
                </Tag>
              </Popover>
              <p>{__('title.southeast_asia_chart')}</p>
              <Popover content={getHoverContent(5)}>
                <Tag
                  color="#ba68c8"
                  onClick={() => {
                    openChart(12);
                  }}
                >
                  {__('title.vietnam')} {getWorkforceCountForCompany(5)}
                </Tag>
              </Popover>
              <Popover content={getHoverContent(6)}>
                <Tag
                  color="#ba68c8"
                  onClick={() => {
                    openChart(13);
                  }}
                >
                  {__('title.cambodia')} {getWorkforceCountForCompany(6)}
                </Tag>
              </Popover>
              <Popover content={getHoverContent(7)}>
                <Tag
                  color="#ba68c8"
                  onClick={() => {
                    openChart(14);
                  }}
                >
                  {__('title.thailand')} {getWorkforceCountForCompany(7)}
                </Tag>
              </Popover>
              <Popover content={getHoverContent(8)}>
                <Tag
                  color="#ba68c8"
                  onClick={() => {
                    openChart(15);
                  }}
                >
                  {__('title.indonesia_sales')} {getWorkforceCountForCompany(8)}
                </Tag>
              </Popover>
              <Popover content={getHoverContent(9)}>
                <Tag
                  color="#ba68c8"
                  onClick={() => {
                    openChart(15);
                  }}
                >
                  {__('title.indonesia_production')} {getWorkforceCountForCompany(9)}
                </Tag>
              </Popover>
              <p>{__('title.south_asia_chart')}</p>
              <Popover content={getHoverContent(10)}>
                <Tag
                  color="#81c784"
                  onClick={() => {
                    openChart(16);
                  }}
                >
                  {__('title.india')} {getWorkforceCountForCompany(10)}
                </Tag>
              </Popover>
              <Popover content={getHoverContent(11)}>
                <Tag
                  color="#81c784"
                  onClick={() => {
                    openChart(17);
                  }}
                >
                  {__('title.bangladesh')} {getWorkforceCountForCompany(11)}
                </Tag>
              </Popover>
              <p>{__('title.americas_chart')}</p>
              <Popover content={getHoverContent(17)}>
                <Tag
                  color="#4fc3f7"
                  onClick={() => {
                    openChart(23);
                  }}
                >
                  {__('title.united_states')} {getWorkforceCountForCompany(17)}
                </Tag>
              </Popover>
              <Popover content={getHoverContent(18)}>
                <Tag
                  color="#4fc3f7"
                  onClick={() => {
                    openChart(24);
                  }}
                >
                  {__('title.guatemala')} {getWorkforceCountForCompany(18)}
                </Tag>
              </Popover>
              <p>{__('title.europe_chart')}</p>
              <Popover content={getHoverContent(13)}>
                <Tag
                  color="#ffd54f"
                  onClick={() => {
                    openChart(19);
                  }}
                >
                  {__('title.türkiye')} {getWorkforceCountForCompany(13)}
                </Tag>
              </Popover>
              <Popover content={getHoverContent(14)}>
                <Tag
                  color="#ffd54f"
                  onClick={() => {
                    openChart(20);
                  }}
                >
                  {__('title.spain')} {getWorkforceCountForCompany(14)}
                </Tag>
              </Popover>
              <Popover content={getHoverContent(12)}>
                <Tag
                  color="#ffd54f"
                  onClick={() => {
                    openChart(18);
                  }}
                >
                  {__('title.united_kingdom')} {getWorkforceCountForCompany(12)}
                </Tag>
              </Popover>
              <Popover content={getHoverContent(15)}>
                <Tag
                  color="#ffd54f"
                  onClick={() => {
                    openChart(21);
                  }}
                >
                  {__('title.germany')} {getWorkforceCountForCompany(15)}
                </Tag>
              </Popover>
              <Popover content={getHoverContent(16)}>
                <Tag
                  color="#ffd54f"
                  onClick={() => {
                    openChart(22);
                  }}
                >
                  {__('title.dubai')} {getWorkforceCountForCompany(16)}
                </Tag>
              </Popover>
            </ProCard>
          </ProCard>
        </ProCard>
      </ProCard>
      <Modal
        open={previewModoalOpen}
        width={'90%'}
        centered
        footer={false}
        maskClosable
        destroyOnClose
        onCancel={() => {
          setPreviewModoalOpen(false);
        }}
      >
        <OrgCharts id={chartId} config={{}} isEditable={false} entityData={data} />
      </Modal>
    </div>
  );
};

export default Index;

结束

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值