SOURCE

<script lang="jsx">
import {
  shallowRef,
  computed,
  ref,
  toRefs,
  toRaw,
  watch,
  onMounted,
  onUnmounted,
  nextTick
} from "vue";
import * as echarts from 'echarts';
export default {
  props: {
    // 四象限中心点
    center:{
      type: Array,
      default:() => [50, 50],
      validator: (value) => {
        return value.length === 2;
      },
    },
    // 四个象限区域颜色:透明度为1时的颜色
    color: {
      type: Array,
      default:() => ['#339933', '#FF9900', '#FF3333', '#007FFF'],
      validator: (value) => {
        return value.length === 4;
      },
    },
    // 四象限区域categories:注意顺序,第一,第二,第三,第四象限
    categories:{
      type: Array,
      default:() => ['优秀维持区', '次待提升区', '亟待提升区', '优秀次维持区'],
      validator: (value) => {
        return value.length === 4;
      },
    },
    option:{
      type: Object,
      required: true,
      default:()=>{
        return {
          xAxis:[{
            name: '修复成功率',
          }],
          yAxis:[{
            name: '修复提升率',
          }],
          series: [
            {
              data: [{
                name: '克孜勒苏柯尔克孜自治州',
                value: [8.84, 17.3]
              }]
            }
          ]
        }
      }
    },
    // 自定义tooltip层formatter回调
    tooltipFormatter:{
      type: Function,
      default: (params, ticket, callback)=>{
        console.error(params)
        return `<h6><i style="background:${params.data.color}"></i><span>${params.data.name}</span></h6>`
      }
    }
  },
  emits: ['dataZoom'],
  setup(props, { attrs, emit, slots, expose }) {
    const { center, color, categories, option, tooltipFormatter } = toRefs(props)
    const chartRef= shallowRef(null);
    const chartInstance= shallowRef(null);

    const getOption = computed(()=>{
      return {
        tooltip: {
          textStyle: {
            color: "#666f8e",
            fontSize: 12
          },
          padding: 0,
          backgroundColor: "transparent",
          extraCssText: "box-shadow: none; z-index: 101; border-width: 0",
          confine: true,
          formatter:(params, ticket, callback) =>`<div class="custom-chart-tooltip">${tooltipFormatter.value(params, ticket, callback)}</div>`
        },
        grid: {
          padding: "0",
          margin: "0",
          left: "0",
          right: "0",
          bottom: "0",
          top: "0",
        },
        dataZoom: [{
          id: "dataZoomX",
          type: "inside",
          xAxisIndex: [0],
          minSpan: 1,
          filterMode: "empty"
        }, {
          id: "dataZoomY",
          type: "inside",
          yAxisIndex: [0],
          minSpan: 1,
          filterMode: "empty"
        }], 
        xAxis: getXAxis.value,
        yAxis: getYAxis.value, 
        color: color.value,
        series: [{
          labelLine: {
            label: {
              show: false
            }
          },
          name: `${option.value?.xAxis?.[0]?.name || ''}-${option.value?.yAxis?.[0]?.name || ''}`,
          type: "scatter",
          data: getSeriesData.value,
          label: {
            show: true,
            position: "top",
            overflow: "truncate",
            width: 60,
            fontSize: 14,
            fontFamily: 'Source Han Sans CN, Source Han Sans CN-Regular',
            color: '#4D4D4D',
            formatter: (params) => {
              return params.data?.name || '';
            },
          },
          symbolSize: 12,
          color: color.value,
          markArea: {
            silent: true,
            data: getMarkAreaData.value
          }
        }]
      };
    });

    const getSeriesData = computed(()=>{
      return getData.value.map(item=>{
        const color = item?.color || getItemColor(item.value);
        return {
          ...item,
          categories: categories.value[getItemIndex(item.value)],
          color,
          itemStyle:{
            borderColor: color,
            borderWidth: 1,
            color: getHexToRgba(color, 0.5)
          },          
          emphasis: {
            itemStyle: {
              color: color,
              borderColor: getHexToRgba(color, 0.4),
              borderWidth: 4,
            },
          },
        }
      })
    });

    const getTableList = computed(()=>{
      const sortCategoriesIndex = [2, 1, 3, 0];
      const newCategories = sortCategoriesIndex.map(index=>({index, categories:categories.value[index]}));
      return newCategories.map(({categories, index})=>{
        const list = getSeriesData.value.filter(child=> child.categories === categories)
        return list.length ? list : [{ categories, value: [], color: getItemColor(index) }]
      })
    })

    const getData = computed(() => {
      const series = Array.isArray(option.value.series) ? option.value.series[0] : option.value.series;
      return series.data;
    });

    const getXYData = computed(() => {
      return (index) => getData.value.reduce((prev, current)=>prev.concat(current.value[index]), []);
    });
    
    const getMaxAndMin = computed(() => {
      const { axisMin: xAxisMin, axisMax: xAxisMax } = getListMinMax(getXYData.value(0), center.value[0]);
      const { axisMin: yAxisMin, axisMax: yAxisMax } = getListMinMax(getXYData.value(1), center.value[1]);
      return {
        xAxisMax,
        yAxisMax,
        xAxisMin,
        yAxisMin,
      };
    });

    const getXAxis = computed(()=>({ 
      ...(option.value?.xAxis?.[0] || {}),
      type: 'value',
      show: false,
      max: getMaxAndMin.value.xAxisMax,
      min: getMaxAndMin.value.xAxisMin,
    }));

    const getYAxis = computed(()=>({ 
      ...(option.value?.yAxis?.[0] || {}),
      type: 'value',
      show: false,
      max: getMaxAndMin.value.yAxisMax,
      min: getMaxAndMin.value.yAxisMin,
    }));

    const getMarkAreaData = computed(()=>([
      [{
        xAxis: center.value[0],
        yAxis: getMaxAndMin.value.yAxisMax,
        itemStyle: {
          color: getItemColor(0, 0.04)
        }
      }, {
        yAxis: center.value[1]
      }],
      [{
        yAxis: getMaxAndMin.value.yAxisMax,
        itemStyle: {
          color: getItemColor(1, 0.04)
        }
      }, {
        xAxis: center.value[0],
        yAxis: center.value[1]
      }],
      [{
        yAxis: center.value[1],
        itemStyle: {
          color: getItemColor(2, 0.04)
        }
      }, {
        xAxis: center.value[0],
        yAxis: getMaxAndMin.value.yAxisMin
      }],
      [{
        xAxis: center.value[0],
        yAxis: center.value[1],
        itemStyle: {
          color: getItemColor(3, 0.04)
        }
      }, {
        yAxis: getMaxAndMin.value.yAxisMin
      }]
    ]));
    
    const getListMinMax = (list, halfValue) => {
      const minValue = Math.min(...list);
      const maxValue = Math.max(...list);
      if (minValue === halfValue && maxValue === halfValue) {
        return getMaxToHalfValue(halfValue, halfValue*1.25, 2)
      } else if (minValue >= halfValue) {
        return getMaxToHalfValue(halfValue, maxValue);
      } else if (maxValue <= halfValue) {
        return getHalfToMinValue(halfValue, minValue);
      } else {
        const flag = maxValue - halfValue > halfValue - minValue;
        if (flag) {
          return getMaxToHalfValue(halfValue, maxValue);
        } else {
          return getHalfToMinValue(halfValue, minValue);
        }
      }
    };

    const getMaxToHalfValue = (halfValue, maxValue, pointNum = 0) => {
      const powNum = Math.pow(10,pointNum)
      const axisMax = Math.floor(powNum * ((maxValue + (maxValue - halfValue) * 0.25))) / powNum;
      const axisMin =  Math.floor(powNum * (halfValue - (axisMax - halfValue)) / powNum;
      return {
        axisMin,
        axisMax,
      };
    };

    const getHalfToMinValue = (halfValue, minValue) => {
      const axisMin = Math.floor(minValue - (halfValue - minValue) * 0.25);
      const axisMax = halfValue + (halfValue - axisMin);
      return {
        axisMin,
        axisMax,
      };
    };

    const getItemColor = (value, opacity) =>{
      const index = typeof value === 'number' ? value : getItemIndex(value);
      return getHexToRgba(color.value[index], opacity);
    }

    const getItemIndex = (value) =>{  
      const [xCenter, yCenter] = center.value;
      const [x, y] = value;
      if (x >= xCenter && y >= yCenter) {
        return 0;
      } else if (x <= xCenter && y >= yCenter) {
        return 1;
      } else if (x < xCenter && y < yCenter) {
        return 2;
      } else if (x >= xCenter && y <= yCenter) {
        return 3;
      } else {
      }
      return 0;
    }

    const getHexToRgba = (hex, opacity = 1) => {
      const newHex = hex.match(/^#[0-9a-f]{3}$/gi) ? hex.replace(/^(#)([0-9a-f])([0-9a-f])([0-9a-f])$/gi,'$1$2$2$3$3$4$4') : hex;
      if((!opacity && opacity !== 0) || opacity === 1) {
        return newHex;
      }
      return `rgba(${[1, 3, 5].map((h) => parseInt(newHex.substring(h, h + 2), 16)).join(',')},${opacity})`;
    };

    const updateChart = async () => {
      await nextTick();
      if (!chartRef.value) {
        return;
      }
      if (chartInstance.value) {
        chartInstance.value.clear();
      } else {
        chartInstance.value = echarts.init(chartRef.value);
        chartInstance.value.on('dataZoom', handleDataZoom);
      }
      chartInstance.value.showLoading({
        text: '',
        textColor: '#f90',
        fontSize: 14,
        color: '#f90',
        maskColor: 'rgba(255, 255, 255, .5)',
        zlevel: 0,
        spinnerRadius: 12,
        lineWidth: 2,
      });
      try {
        chartInstance.value.setOption(getOption.value, true);
        handleResize();
      } catch (error) {
      }
      chartInstance.value.hideLoading();
    };
    
    const handleResize = () => {
      if (chartInstance.value) {
        chartInstance.value.resize();
      }
    };

    const handleDataZoom = (e) => {
      const option = chartInstance.value.getOption();
      const { xAxis, dataZoom} = option;
      const num = dataZoom[0].endValue - dataZoom[0].startValue;
      const percentage = (xAxis[0].max - xAxis[0].min) / num;
      emit('dataZoom', Math.ceil(percentage * 100));     
    };

    onMounted(() => {
      window.addEventListener("resize", handleResize);
    });

    onUnmounted(() => {
      if (chartInstance.value) {
        chartInstance.value.off('dataZoom', handleDataZoom);
        chartInstance.value.dispose();
        chartInstance.value = null;
      }
      window.removeEventListener("resize", handleResize);
    });

    watch(
      () => option.value,
      (val) => {
        updateChart();
      },
      { deep: true, immediate: true }
    );

    return () => (
      <div class="base-scatter-chart">
        <div class="base-scatter-el" ref={chartRef}></div>
        {!!slots?.default && slots.default()}
      </div>
    );
  },
};
</script>

<style lang="less" >
.base-scatter-chart{
  width: 100%;
  height: 100%;
  position: relative;
  .base-scatter-el{
    width: 100%;
    height: 100%;
    position: relative;
  }
  .custom-chart-tooltip{
    background: #ffffff;
    padding: 16px 12px 12px;
    border-radius: 8px;
    font-size: 14px;
    box-shadow: 0px 2px 8px 0px rgba(0, 0, 0, 0.15);
    > h6:first-child{
      display: flex;
      font-size: 14px;
      align-items:  center;
      font-weight: normal;
      i{
        width: 8px;
        height: 8px;
        border-radius: 50%;

      }
      span{
        text-indent: 8px;
      }
    }
  }
}
</style>



// app.vue
/*
<template>
  <component :is="mode" ></component>
</template>
<script setup>
import { ref, toRefs, markRaw, unref, watch } from 'vue';
defineOptions({
  inheritAttrs: false,
});
const props = defineProps({
  url:{
    type: String,
    default: '/CommonComponentFront/satisfaction_remote_component_table_selection_transfer.0.0.1.umd.js?name=__SATISFACTION_REMOTE_COMPONENT_TABLE_SELECTION_TRANSFER'
  }
});

const { url } = toRefs(props);
const mode = ref(null);

// 特殊字符转义
const strReplaceXss = (str) => {
  const specailToStr = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#x27',
    ',': '&#44',
    ';': '&#59',
    '(': '&#40',
    ')': '&#41',
  };
  return (str || '').replace(/[&<>"',;()]/g, (val) => specailToStr[val]);
};

// 加载远程js
const loadScript = (url) => {
  const str = unref(url);
  // url编码后参数name提取
  const params = new URLSearchParams(`?${encodeURI(strReplaceXss(str).split('.js?')[1] || '')}`);
  const name = params.get('name');
  if (!name) {
    return;
  }
  if (window[name]) {
    mode.value = markRaw(window[name]);
  } else {
    fetch(str, {
      method: 'GET',
      mode: 'cors',
    }).then((res) => {
      if (res.status === 200) {
        res.text().then((code) => {
          const script = document.createElement('script');
          script.text = code;
          document.body.appendChild(script);
          mode.value = markRaw(window[name]);
        });
      }
    });
  }
  return;
};

watch(
  () => url,
  (val) => {
    loadScript(val);
  },
  { deep: true, immediate: true }
);
</script> 

<style>
*{
  padding: 0;
  margin: 0;
  list-style: none;
}
#app,html,body{
  width: 100%;
  height: 100%;
}
</style>
*/











// index.js  全量导入
const files = import.meta.globEager('./*.vue');
const modules = {}
for (const key in files) {
  if (Object.prototype.hasOwnProperty.call(files, key)) {
    modules[files[key].default.type] = files[key].default
  }
}
export default modules
console 命令行工具 X clear

                    
>
console