huminmin
3 天以前 b0c3dbefea78b106b7c680597361ea37930eaa0d
Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-management into dev_New
已添加1个文件
已修改11个文件
560 ■■■■ 文件已修改
src/components/Echarts/echarts.vue 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/useChartBackground.js 133 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/measurementEquipment/components/calibrationDia.vue 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/measurementEquipment/components/formDia.vue 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/measurementEquipment/index.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/spareParts/index.vue 48 ●●●●● 补丁 | 查看 | 原始文档 | 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/views/safeProduction/safetyTrainingAssessment/index.vue 26 ●●●●● 补丁 | 查看 | 原始文档 | 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/equipmentManagement/measurementEquipment/components/calibrationDia.vue
@@ -51,11 +51,14 @@
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="有效期:" prop="valid">
                        <el-form-item label="有效日期(天):" prop="valid">
                            <el-input
                                v-model="form.valid"
                                placeholder="请输入"
                                type="number"
                                placeholder="请输入有效期天数"
                                clearable
                                :min="1"
                                @input="handleValidInput"
                            >
                                <template #append>日</template>
                            </el-input>
@@ -152,7 +155,32 @@
    rules: {
        code: [{required: true, message: "请输入", trigger: "blur"}],
        name: [{required: true, message: "请输入", trigger: "blur"}],
        valid: [{required: true, message: "请输入", trigger: "blur"}],
        valid: [
            {required: true, message: "请输入", trigger: "blur"},
            {
                validator: (rule, value, callback) => {
                    if (value === '' || value === null || value === undefined) {
                        callback();
                        return;
                    }
                    const numValue = Number(value);
                    if (isNaN(numValue)) {
                        callback(new Error('请输入有效的数字'));
                        return;
                    }
                    if (numValue <= 0) {
                        callback(new Error('只能输入正数'));
                        return;
                    }
                    if (!Number.isInteger(numValue)) {
                        callback(new Error('请输入整数'));
                        return;
                    }
                    callback();
                },
                trigger: 'blur'
            }
        ],
        recordDate: [{required: true, message: "请选择", trigger: "change"}],
        userId: [{required: true, message: "请选择", trigger: "change"}],
        entryDate: [{required: true, message: "请选择", trigger: "change"}],
@@ -233,6 +261,27 @@
    }
}
// å¤„理有效日期输入,只允许正整数
const handleValidInput = (value) => {
    if (value === '' || value === null || value === undefined) {
        form.value.valid = '';
        return;
    }
    // è½¬æ¢ä¸ºå­—符串并移除所有非数字字符(包括负号、小数点等)
    const numStr = String(value).replace(/[^0-9]/g, '');
    if (numStr === '') {
        form.value.valid = '';
        return;
    }
    const numValue = parseInt(numStr, 10);
    // ç¡®ä¿æ˜¯æ­£æ•´æ•°ï¼ˆå¤§äºŽ0)
    if (numValue > 0 && !isNaN(numValue)) {
        form.value.valid = numValue;
    } else {
        form.value.valid = '';
    }
}
const submitForm = () => {
    proxy.$refs["formRef"].validate(valid => {
        if (valid) {
src/views/equipmentManagement/measurementEquipment/components/formDia.vue
@@ -15,11 +15,20 @@
                ref="formRef"
            >
                <el-row :gutter="30">
                    <el-col :span="24">
                    <el-col :span="12">
                        <el-form-item label="出厂编号:" prop="code">
                            <el-input
                                v-model="form.code"
                                placeholder="请输入"
                                clearable
                            />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="计量器具名称:" prop="name">
                            <el-input
                                v-model="form.name"
                                placeholder="请输入计量器具名称"
                                clearable
                            />
                        </el-form-item>
@@ -74,8 +83,11 @@
            <el-form-item label="有效日期(天):" prop="valid">
              <el-input
                  v-model="form.valid"
                  type="number"
                  placeholder="请输入有效期天数"
                  clearable
                  :min="1"
                  @input="handleValidInput"
              >
              <template #append>日</template>
              </el-input>
@@ -171,6 +183,7 @@
const data = reactive({
    form: {
        code: "",
    name: "",
    instationLocation: "",
    mostDate:"",
        model: "",
@@ -184,6 +197,7 @@
    },
    rules: {
        code: [{required: true, message: "请输入", trigger: "blur"}],
    name: [{ required: true, message: "请输入", trigger: "blur" }],
        model: [{required: true, message: "请输入", trigger: "blur"}],
        validDate: [{required: true, message: "请输入", trigger: "blur"}],
        nextDate: [{required: true, message: "请选择", trigger: "change"}],
@@ -192,7 +206,32 @@
    instationLocation: [{required: true, message: "请输入", trigger: "blur"}],
    mostDate: [{required: true, message: "请选择", trigger: "change"}],
    cycle: [{required: true, message: "请选择", trigger: "blur"}],
    valid: [{required: true, message: "请输入", trigger: "blur"}],
    valid: [
      {required: true, message: "请输入", trigger: "blur"},
      {
        validator: (rule, value, callback) => {
          if (value === '' || value === null || value === undefined) {
            callback();
            return;
          }
          const numValue = Number(value);
          if (isNaN(numValue)) {
            callback(new Error('请输入有效的数字'));
            return;
          }
          if (numValue <= 0) {
            callback(new Error('只能输入正数'));
            return;
          }
          if (!Number.isInteger(numValue)) {
            callback(new Error('请输入整数'));
            return;
          }
          callback();
        },
        trigger: 'blur'
      }
    ],
    unit: [{required: true, message: "请输入", trigger: "blur"}],
    }
})
@@ -254,6 +293,27 @@
    }
}
// å¤„理有效日期输入,只允许正整数
const handleValidInput = (value) => {
    if (value === '' || value === null || value === undefined) {
        form.value.valid = '';
        return;
    }
    // è½¬æ¢ä¸ºå­—符串并移除所有非数字字符(包括负号、小数点等)
    const numStr = String(value).replace(/[^0-9]/g, '');
    if (numStr === '') {
        form.value.valid = '';
        return;
    }
    const numValue = parseInt(numStr, 10);
    // ç¡®ä¿æ˜¯æ­£æ•´æ•°ï¼ˆå¤§äºŽ0)
    if (numValue > 0 && !isNaN(numValue)) {
        form.value.valid = numValue;
    } else {
        form.value.valid = '';
    }
}
const submitForm = () => {
    proxy.$refs["formRef"].validate(valid => {
        if (valid) {
src/views/equipmentManagement/measurementEquipment/index.vue
@@ -82,6 +82,12 @@
    minWidth:150,
    align:"center"
    },
  {
    label: "计量器具名称",
    prop: "name",
    width: '160px',
    align: "center",
  },
    {
        label: "安装位置",
        prop: "instationLocation",
src/views/equipmentManagement/spareParts/index.vue
@@ -38,7 +38,7 @@
        </el-table-column>
        <el-table-column prop="price" label="ä»·æ ¼" width="140"></el-table-column>
        <el-table-column prop="quantity" label="数量" width="140"></el-table-column>
        <el-table-column prop="description" label="描述" width="150"></el-table-column>
        <el-table-column prop="description" label="描述"></el-table-column>
        <el-table-column label="操作" width="150" fixed="right" align="center">
          <template #default="{ row }">
            <el-button
@@ -60,6 +60,18 @@
          </template>
        </el-table-column>
      </el-table>
      <!-- åˆ†é¡µç»„ä»¶ -->
      <div class="pagination-container">
        <el-pagination
          v-model:current-page="pagination.current"
          v-model:page-size="pagination.size"
          :page-sizes="[10, 20, 50, 100]"
          :total="pagination.total"
          layout="total, sizes, prev, pager, next, jumper"
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
        />
      </div>
    </div>
    <el-dialog title="分类管理" v-model="dialogVisible" width="60%">
      <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
@@ -147,6 +159,12 @@
const queryParams = reactive({
  name: ''
});
// åˆ†é¡µå‚æ•°
const pagination = reactive({
  current: 1,
  size: 10,
  total: 0
});
// è¡¨å•数据
const form = reactive({
  id:'',
@@ -215,7 +233,10 @@
const fetchListData = async () => {
  loading.value = true;
  try {
    const params = {};
    const params = {
      current: pagination.current,
      size: pagination.size
    };
    if (queryParams.name) {
      params.name = queryParams.name;
    }
@@ -223,6 +244,7 @@
    if (res.code === 200) {
      renderTableData.value = res.data.records || [];
      categories.value = res.data.records || [];
      pagination.total = res.data.total || 0;
    }
  } catch (error) {
        loading.value = false;
@@ -233,12 +255,27 @@
// æŸ¥è¯¢
const handleQuery = () => {
  pagination.current = 1;
  fetchListData();
}
// é‡ç½®æŸ¥è¯¢
const resetQuery = () => {
  queryParams.name = '';
  pagination.current = 1;
  fetchListData();
}
// åˆ†é¡µå¤§å°æ”¹å˜
const handleSizeChange = (size) => {
  pagination.size = size;
  pagination.current = 1;
  fetchListData();
}
// å½“前页改变
const handleCurrentChange = (current) => {
  pagination.current = current;
  fetchListData();
}
@@ -373,6 +410,13 @@
  margin-top: unset;
}
.pagination-container {
  margin-top: 20px;
  display: flex;
  justify-content: flex-end;
  padding: 16px 0;
}
.el-table__header-wrapper th {
  background-color: #f5f7fa;
  font-weight: 600;
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>
src/views/safeProduction/safetyTrainingAssessment/index.vue
@@ -2,23 +2,14 @@
  <div class="app-container">
    <div class="search_form">
      <div>
        <span class="search_title">课程编号:</span>
        <el-input v-model="searchForm.courseCode"
                  style="width: 240px"
                  placeholder="请输入培训编号搜索"
        <span class="search_title">培训日期:</span>
        <el-date-picker v-model="searchForm.trainingDate"
                        value-format="YYYY-MM-DD"
                        format="YYYY-MM-DD"
                  @change="handleQuery"
                  clearable
                  :prefix-icon="Search" />
        <span class="search_title ml10">培训方式:</span>
        <el-select v-model="searchForm.trainingMode"
                   clearable
                   @change="handleQuery"
                   style="width: 240px">
          <el-option v-for="item in trainingModeOptions"
                     :key="item.value"
                     :label="item.label"
                     :value="item.value" />
        </el-select>
                        type="date"
                        placeholder="请选择"
                        clearable />
        <el-button type="primary"
                   @click="handleQuery"
                   style="margin-left: 10px">
@@ -426,8 +417,7 @@
  // å“åº”式数据
  const data = reactive({
    searchForm: {
      courseCode: "",
      trainingMode: "",
      trainingDate: "",
      state: 0,
    },
    tableLoading: false,