gaoluyang
3 天以前 8ac303fadd4da94ae3fe34b16dcfb0e462be3dc7
Merge remote-tracking branch 'origin/dev_New' into dev_New
已添加1个文件
已修改6个文件
361 ■■■■■ 文件已修改
src/components/Echarts/echarts.vue 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/useChartBackground.js 133 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/left-bottom.vue 34 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/left-top.vue 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/left-top.vue 35 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/financialAnalysis/components/left-bottom.vue 34 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/left-top.vue 34 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Echarts/echarts.vue
@@ -6,8 +6,10 @@
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, watchEffect } from 'vue'
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import * as echarts from 'echarts'
const emit = defineEmits(['finished'])
// Props
const props = defineProps({
@@ -91,6 +93,47 @@
// Refs
const chartRef = ref(null)
let chartInstance = null
let finishedHandler = null
let initTimer = null
let initAttempts = 0
function clearInitTimer() {
  if (initTimer) {
    clearTimeout(initTimer)
    initTimer = null
  }
}
function isContainerReady() {
  const el = chartRef.value
  if (!el) return false
  // offsetWidth/offsetHeight æ›´è´´è¿‘真实布局(为 0 å¾€å¾€ä»£è¡¨è¿˜æ²¡å¸ƒå±€/不可见)
  return el.offsetWidth > 0 && el.offsetHeight > 0
}
function initChartWhenReady() {
  clearInitTimer()
  initAttempts += 1
  if (!isContainerReady()) {
    // ç­‰å®¹å™¨çœŸæ­£æœ‰å°ºå¯¸ï¼ˆé¿å…é¦–屏初始化偏移/空白,热更新后才正常的情况)
    // æœ€å¤šé‡è¯•约 3 ç§’,避免无限循环
    if (initAttempts < 60) {
      initTimer = setTimeout(initChartWhenReady, 50)
    }
    return
  }
  if (chartInstance) return
  chartInstance = echarts.init(chartRef.value)
  finishedHandler = () => emit('finished')
  chartInstance.on('finished', finishedHandler)
  renderChart()
  // setOption åŽè¡¥ä¸€æ¬¡ resize,确保首屏尺寸正确
  nextTick(() => {
    if (chartInstance) chartInstance.resize()
  })
}
// Methods
function generateChart(option) {
@@ -139,26 +182,38 @@
// Lifecycle hooks
onMounted(() => {
  chartInstance = echarts.init(chartRef.value)
  renderChart()
  initAttempts = 0
  initChartWhenReady()
  window.addEventListener('resize', windowResizeListener)
})
onBeforeUnmount(() => {
  if (chartInstance) {
    window.removeEventListener('resize', windowResizeListener)
    if (finishedHandler) {
      chartInstance.off('finished', finishedHandler)
      finishedHandler = null
    }
    chartInstance.dispose()
    chartInstance = null
  }
  clearInitTimer()
})
// Watch all reactive props that affect the chart
watch(
    () => [props.xAxis, props.yAxis, props.series, props.legend, props.tooltip, props.visualMap],
    () => {
      if (chartInstance) {
        renderChart()
      // å¦‚果首屏还没初始化成功,等待容器 ready åŽå†æ¸²æŸ“
      if (!chartInstance) {
        initChartWhenReady()
        return
      }
      renderChart()
      // æ•°æ®å˜åŒ–后补一次 resize,避免布局变化导致的偏移
      nextTick(() => {
        if (chartInstance) chartInstance.resize()
      })
    },
    { deep: true, immediate: true }
)
src/hooks/useChartBackground.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,133 @@
import { ref, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'
/**
 * å›¾è¡¨èƒŒæ™¯ä½ç½®è°ƒæ•´ composable
 * @param {Object} options é…ç½®é€‰é¡¹
 * @param {Ref} options.wrapperRef - å›¾è¡¨å®¹å™¨çš„ ref
 * @param {Ref} options.backgroundRef - èƒŒæ™¯å…ƒç´ çš„ ref
 * @param {String} options.left - èƒŒæ™¯ left ä½ç½®ï¼Œå¦‚ '25%' æˆ– '50%',默认 '50%'
 * @param {String} options.top - èƒŒæ™¯ top ä½ç½®ï¼Œå¦‚ '50%',默认 '50%'
 * @param {String} options.offsetX - X è½´åç§»å€¼ï¼Œå¦‚ '-51.5%' æˆ– '-50%',默认 '-50%'
 * @param {String} options.offsetY - Y è½´åç§»å€¼ï¼Œå¦‚ '-39%' æˆ– '-50%',默认 '-50%'
 * @param {Ref} options.watchData - å¯é€‰ï¼Œç›‘听的数据变化,数据变化时重新调整位置
 * @returns {Function} adjustBackgroundPosition - æ‰‹åŠ¨è°ƒæ•´èƒŒæ™¯ä½ç½®çš„æ–¹æ³•
 */
export function useChartBackground(options = {}) {
  const {
    wrapperRef,
    backgroundRef,
    left = '50%',
    top = '50%',
    offsetX = '-50%',
    offsetY = '-50%',
    watchData = null
  } = options
  let resizeObserver = null
  let intersectionObserver = null
  let retryTimers = []
  const clearRetryTimers = () => {
    if (!retryTimers.length) return
    retryTimers.forEach((t) => clearTimeout(t))
    retryTimers = []
  }
  // è°ƒæ•´èƒŒæ™¯ä½ç½®
  const adjustBackgroundPosition = () => {
    nextTick(() => {
      if (!wrapperRef?.value || !backgroundRef?.value) {
        return
      }
      // åˆå§‹åŒ–阶段经常出现:容器尚未可见/尺寸为 0(非全屏、tab、动画等)
      // è¿™ç§æƒ…况下先不对齐,等 ResizeObserver / IntersectionObserver å†è§¦å‘
      const rect = wrapperRef.value.getBoundingClientRect()
      if (!rect.width || !rect.height) return
      const background = backgroundRef.value
      // ä½¿ç”¨ç™¾åˆ†æ¯”定位 + transform å¾®è°ƒï¼ˆè¿™æ˜¯æœ€å¯é çš„æ–¹å¼ï¼‰
      background.style.left = left
      background.style.top = top
      background.style.transform = `translate(${offsetX}, ${offsetY})`
    })
  }
  // åˆå§‹åŒ–阶段多次“补偿对齐”,覆盖 Echarts é¦–次渲染/动画造成的延迟布局
  const scheduleKickAlign = () => {
    clearRetryTimers()
    ;[0, 60, 180, 360, 800].forEach((ms) => {
      retryTimers.push(
        setTimeout(() => {
          adjustBackgroundPosition()
        }, ms)
      )
    })
  }
  // çª—口 resize å¤„理
  const resizeHandler = () => {
    adjustBackgroundPosition()
  }
  // å¦‚果提供了 watchData,监听数据变化(需要在 setup é˜¶æ®µåˆ›å»ºï¼‰
  if (watchData) {
    watch(watchData, () => {
      adjustBackgroundPosition()
    }, { deep: true })
  }
  // åˆå§‹åŒ–
  const init = () => {
    // ç›‘听窗口 resize
    window.addEventListener('resize', resizeHandler)
    // ä½¿ç”¨ ResizeObserver ç›‘听容器尺寸变化
    nextTick(() => {
      if (wrapperRef?.value && window.ResizeObserver) {
        resizeObserver = new ResizeObserver(() => {
          adjustBackgroundPosition()
        })
        resizeObserver.observe(wrapperRef.value)
      }
      // ç›‘听“从不可见到可见”,解决初始化时未对齐但热更新又正常的问题
      if (wrapperRef?.value && window.IntersectionObserver) {
        intersectionObserver = new IntersectionObserver(
          (entries) => {
            const entry = entries?.[0]
            if (entry?.isIntersecting) {
              scheduleKickAlign()
            }
          },
          { threshold: 0.01 }
        )
        intersectionObserver.observe(wrapperRef.value)
      }
      // åˆå§‹åŒ–多次补偿对齐,确保图表渲染完成
      scheduleKickAlign()
    })
  }
  // æ¸…理
  const cleanup = () => {
    window.removeEventListener('resize', resizeHandler)
    clearRetryTimers()
    if (resizeObserver) {
      resizeObserver.disconnect()
      resizeObserver = null
    }
    if (intersectionObserver) {
      intersectionObserver.disconnect()
      intersectionObserver = null
    }
  }
  return {
    adjustBackgroundPosition,
    init,
    cleanup
  }
}
src/views/reportAnalysis/PSIDataAnalysis/components/left-bottom.vue
@@ -3,8 +3,8 @@
    <PanelHeader title="采购品分布" />
    <div class="main-panel panel-item-customers">
      <CarouselCards :items="cardItems" :visible-count="3" />
      <div class="pie-chart-wrapper">
        <div class="pie-background"></div>
      <div class="pie-chart-wrapper" ref="pieWrapperRef">
        <div class="pie-background" ref="pieBackgroundRef"></div>
        <Echarts
          ref="chart"
          :chartStyle="chartStyle"
@@ -22,11 +22,15 @@
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from './PanelHeader.vue'
import CarouselCards from './CarouselCards.vue'
import { rawMaterialPurchaseAmountRatio } from '@/api/viewIndex.js'
import { useChartBackground } from '@/hooks/useChartBackground.js'
const pieWrapperRef = ref(null)
const pieBackgroundRef = ref(null)
/**
 * @introduction æŠŠæ•°ç»„中key值相同的那一项提取出来,组成一个对象
@@ -164,6 +168,18 @@
  textStyle: { color: '#B8C8E0' },
}
// ä½¿ç”¨å°è£…的背景位置调整方法
// å›¾è¡¨ä¸­å¿ƒæ˜¯ ['25%', '50%'],背景需要对齐到这个位置
const { init: initBackground, cleanup: cleanupBackground } = useChartBackground({
  wrapperRef: pieWrapperRef,
  backgroundRef: pieBackgroundRef,
  left: '25%',       // å›¾è¡¨ä¸­å¿ƒ X æ˜¯ 25%
  top: '50%',        // å›¾è¡¨ä¸­å¿ƒ Y æ˜¯ 50%
  offsetX: '-51.5%', // X è½´åç§»
  offsetY: '-50%',   // Y è½´åç§»
  watchData: dataList // ç›‘听数据变化,自动调整位置
})
const fetchData = () => {
  rawMaterialPurchaseAmountRatio()
    .then((res) => {
@@ -191,6 +207,11 @@
onMounted(() => {
  fetchData()
  initBackground()
})
onBeforeUnmount(() => {
  cleanupBackground()
})
</script>
@@ -218,9 +239,6 @@
.pie-background {
  position: absolute;
  left: 25%;
  top: 50%;
  transform: translate(-51.5%, -50%);
  width: 310px;
  height: 310px;
  background-image: url('@/assets/BI/玫瑰图边框.png');
@@ -229,5 +247,9 @@
  background-repeat: no-repeat;
  z-index: 1;
  pointer-events: none;
  /* ä½ç½®ç”± JS åŠ¨æ€è®¾ç½®ï¼Œé»˜è®¤å±…ä¸­ */
  left: 25%;
  top: 50%;
  transform: translate(-51.5%, -50%);
}
</style>
src/views/reportAnalysis/PSIDataAnalysis/components/left-top.vue
@@ -3,8 +3,8 @@
    <PanelHeader title="销售品分布" />
    <div class="main-panel panel-item-customers">
      <CarouselCards :items="cardItems" :visible-count="3" />
      <div class="pie-chart-wrapper">
        <div class="pie-background"></div>
      <div class="pie-chart-wrapper" ref="pieWrapperRef">
        <div class="pie-background" ref="pieBackgroundRef"></div>
        <Echarts
          ref="echartsRef"
          :chartStyle="chartStyle"
@@ -21,11 +21,15 @@
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import { productSalesAnalysis } from '@/api/viewIndex.js'
import PanelHeader from './PanelHeader.vue'
import CarouselCards from './CarouselCards.vue'
import Echarts from '@/components/Echarts/echarts.vue'
import { useChartBackground } from '@/hooks/useChartBackground.js'
const pieWrapperRef = ref(null)
const pieBackgroundRef = ref(null)
/**
 * @introduction æŠŠæ•°ç»„中key值相同的那一项提取出来,组成一个对象
@@ -137,6 +141,17 @@
const cardItems = ref([])
// ä½¿ç”¨å°è£…的背景位置调整方法(与其他文件保持一致)
const { init: initBackground, cleanup: cleanupBackground } = useChartBackground({
  wrapperRef: pieWrapperRef,
  backgroundRef: pieBackgroundRef,
  left: '25%',       // å›¾è¡¨ä¸­å¿ƒ X æ˜¯ 25%
  top: '50%',        // å›¾è¡¨ä¸­å¿ƒ Y æ˜¯ 50%
  offsetX: '-51.5%', // X è½´åç§»
  offsetY: '-50%',   // Y è½´åç§»
  watchData: pieDatas // ç›‘听数据变化,自动调整位置
})
const fetchData = () => {
  productSalesAnalysis()
    .then((res) => {
@@ -162,6 +177,11 @@
onMounted(() => {
  fetchData()
  initBackground()
})
onBeforeUnmount(() => {
  cleanupBackground()
})
</script>
src/views/reportAnalysis/dataDashboard/components/basic/left-top.vue
@@ -2,8 +2,8 @@
  <div>
    <PanelHeader title="产品大类" />
    <div class="panel-item-customers">
      <div class="pie-chart-wrapper">
        <div class="pie-background"></div>
      <div class="pie-chart-wrapper" ref="pieWrapperRef">
        <div class="pie-background" ref="pieBackgroundRef"></div>
        <Echarts
          ref="chart"
          :chartStyle="chartStyle"
@@ -21,10 +21,15 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, onBeforeUnmount } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from '../PanelHeader.vue'
import { productCategoryDistribution } from '@/api/viewIndex.js'
import { useChartBackground } from '@/hooks/useChartBackground.js'
const pieWrapperRef = ref(null)
const pieBackgroundRef = ref(null)
const chart = ref(null)
// æ•°æ®åˆ—表(来自接口)
const dataList = ref([])
@@ -170,6 +175,15 @@
  textStyle: { color: '#B8C8E0' },
}
// ä½¿ç”¨å°è£…的背景位置调整方法,可自定义偏移值
const { adjustBackgroundPosition, init: initBackground, cleanup: cleanupBackground } = useChartBackground({
  wrapperRef: pieWrapperRef,
  backgroundRef: pieBackgroundRef,
  offsetX: '-51.5%', // X è½´åç§»ï¼Œå¯åŠ¨æ€è°ƒæ•´
  offsetY: '-39%',   // Y è½´åç§»ï¼Œå¯åŠ¨æ€è°ƒæ•´
  watchData: dataList // ç›‘听数据变化,自动调整位置
})
const loadData = async () => {
  try {
    const res = await productCategoryDistribution()
@@ -182,6 +196,8 @@
    }))
    landLegend.data = dataList.value.map((d) => d.name)
    landSeries.value[0].data = dataList.value
    // æ•°æ®åŠ è½½å®ŒæˆåŽè°ƒæ•´èƒŒæ™¯ä½ç½®
    adjustBackgroundPosition()
  } catch (e) {
    console.error('获取产品大类分布失败:', e)
    dataList.value = []
@@ -190,8 +206,14 @@
  }
}
onMounted(() => {
  loadData()
  initBackground()
})
onBeforeUnmount(() => {
  cleanupBackground()
})
</script>
@@ -212,9 +234,6 @@
.pie-background {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-51.5%, -39%);
  width: 360px;
  height: 360px;
  background-image: url('@/assets/BI/玫瑰图边框.png');
@@ -223,5 +242,9 @@
  background-repeat: no-repeat;
  z-index: 1;
  pointer-events: none;
  /* é»˜è®¤å±…中,会在 JS ä¸­åŠ¨æ€è°ƒæ•´ */
  left: 50%;
  top: 50%;
  transform: translate(-51.5%, -39%);
}
</style>
src/views/reportAnalysis/financialAnalysis/components/left-bottom.vue
@@ -10,8 +10,8 @@
        />
      </div>
      <!-- <CarouselCards :items="cardItems" :visible-count="3" /> -->
      <div class="pie-chart-wrapper">
        <div class="pie-background"></div>
      <div class="pie-chart-wrapper" ref="pieWrapperRef">
        <div class="pie-background" ref="pieBackgroundRef"></div>
        <Echarts
          ref="chart"
          :chartStyle="chartStyle"
@@ -29,11 +29,15 @@
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from './PanelHeader.vue'
import ProductTypeSwitch from './ProductTypeSwitch.vue'
import { expenseCompositionAnalysis } from '@/api/viewIndex.js'
import { useChartBackground } from '@/hooks/useChartBackground.js'
const pieWrapperRef = ref(null)
const pieBackgroundRef = ref(null)
/**
 * @introduction æŠŠæ•°ç»„中key值相同的那一项提取出来,组成一个对象
@@ -185,6 +189,18 @@
  textStyle: { color: '#B8C8E0' },
}
// ä½¿ç”¨å°è£…的背景位置调整方法
// å›¾è¡¨ä¸­å¿ƒæ˜¯ ['25%', '50%'],背景需要对齐到这个位置
const { init: initBackground, cleanup: cleanupBackground } = useChartBackground({
  wrapperRef: pieWrapperRef,
  backgroundRef: pieBackgroundRef,
  left: '25%',       // å›¾è¡¨ä¸­å¿ƒ X æ˜¯ 25%
  top: '50%',        // å›¾è¡¨ä¸­å¿ƒ Y æ˜¯ 50%
  offsetX: '-51.5%', // X è½´åç§»
  offsetY: '-50%',   // Y è½´åç§»
  watchData: dataList // ç›‘听数据变化,自动调整位置
})
const fetchData = () => {
  expenseCompositionAnalysis({ type: amountType.value })
    .then((res) => {
@@ -216,6 +232,11 @@
onMounted(() => {
  fetchData()
  initBackground()
})
onBeforeUnmount(() => {
  cleanupBackground()
})
</script>
@@ -251,9 +272,6 @@
.pie-background {
  position: absolute;
  left: 25%;
  top: 50%;
  transform: translate(-51.5%, -50%);
  width: 310px;
  height: 310px;
  background-image: url('@/assets/BI/玫瑰图边框.png');
@@ -262,5 +280,9 @@
  background-repeat: no-repeat;
  z-index: 1;
  pointer-events: none;
  /* ä½ç½®ç”± JS åŠ¨æ€è®¾ç½®ï¼Œé»˜è®¤å±…ä¸­ */
  left: 25%;
  top: 50%;
  transform: translate(-51.5%, -50%);
}
</style>
src/views/reportAnalysis/productionAnalysis/components/left-top.vue
@@ -5,8 +5,8 @@
      <div class="filters-row">
        <DateTypeSwitch v-model="dateType" @change="handleDateTypeChange" />
      </div>
      <div class="pie-chart-wrapper">
        <div class="pie-background"></div>
      <div class="pie-chart-wrapper" ref="pieWrapperRef">
        <div class="pie-background" ref="pieBackgroundRef"></div>
        <Echarts
          ref="echartsRef"
          :chartStyle="chartStyle"
@@ -23,11 +23,15 @@
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import { productSalesAnalysis } from '@/api/viewIndex.js'
import PanelHeader from './PanelHeader.vue'
import Echarts from '@/components/Echarts/echarts.vue'
import DateTypeSwitch from '@/views/reportAnalysis/financialAnalysis/components/DateTypeSwitch.vue'
import { useChartBackground } from '@/hooks/useChartBackground.js'
const pieWrapperRef = ref(null)
const pieBackgroundRef = ref(null)
const dateType = ref(1) // 1=周 2=月 3=季度
@@ -133,6 +137,18 @@
  textStyle: { color: '#B8C8E0' },
}
// ä½¿ç”¨å°è£…的背景位置调整方法
// å›¾è¡¨ä¸­å¿ƒæ˜¯ ['25%', '50%'],背景需要对齐到这个位置
const { init: initBackground, cleanup: cleanupBackground } = useChartBackground({
  wrapperRef: pieWrapperRef,
  backgroundRef: pieBackgroundRef,
  left: '25%',       // å›¾è¡¨ä¸­å¿ƒ X æ˜¯ 25%
  top: '50%',        // å›¾è¡¨ä¸­å¿ƒ Y æ˜¯ 50%
  offsetX: '-51.5%', // X è½´åç§»
  offsetY: '-50%',   // Y è½´åç§»
  watchData: pieDatas // ç›‘听数据变化,自动调整位置
})
const fetchData = () => {
  productSalesAnalysis()
    .then((res) => {
@@ -156,6 +172,11 @@
onMounted(() => {
  fetchData()
  initBackground()
})
onBeforeUnmount(() => {
  cleanupBackground()
})
</script>
@@ -190,9 +211,6 @@
.pie-background {
  position: absolute;
  left: 25%;
  top: 50%;
  transform: translate(-51.5%, -50%);
  width: 310px;
  height: 310px;
  background-image: url('@/assets/BI/玫瑰图边框.png');
@@ -201,5 +219,9 @@
  background-repeat: no-repeat;
  z-index: 1;
  pointer-events: none;
  /* ä½ç½®ç”± JS åŠ¨æ€è®¾ç½®ï¼Œé»˜è®¤å±…ä¸­ */
  left: 25%;
  top: 50%;
  transform: translate(-51.5%, -50%);
}
</style>