From b0c3dbefea78b106b7c680597361ea37930eaa0d Mon Sep 17 00:00:00 2001
From: huminmin <mac@MacBook-Pro.local>
Date: 星期五, 30 一月 2026 16:12:27 +0800
Subject: [PATCH] Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-management into dev_New

---
 src/views/reportAnalysis/PSIDataAnalysis/components/left-bottom.vue              |   34 ++
 src/views/equipmentManagement/measurementEquipment/index.vue                     |    6 
 src/views/equipmentManagement/spareParts/index.vue                               |   48 ++++
 src/views/reportAnalysis/PSIDataAnalysis/components/left-top.vue                 |   26 ++
 src/views/reportAnalysis/financialAnalysis/components/left-bottom.vue            |   34 ++
 src/components/Echarts/echarts.vue                                               |   65 ++++++
 src/views/equipmentManagement/measurementEquipment/components/formDia.vue        |   64 ++++++
 src/hooks/useChartBackground.js                                                  |  133 +++++++++++++
 src/views/reportAnalysis/productionAnalysis/components/left-top.vue              |   34 ++
 src/views/safeProduction/safetyTrainingAssessment/index.vue                      |   28 -
 src/views/equipmentManagement/measurementEquipment/components/calibrationDia.vue |   55 +++++
 src/views/reportAnalysis/dataDashboard/components/basic/left-top.vue             |   35 ++
 12 files changed, 504 insertions(+), 58 deletions(-)

diff --git a/src/components/Echarts/echarts.vue b/src/components/Echarts/echarts.vue
index a386bb7..0e07163 100644
--- a/src/components/Echarts/echarts.vue
+++ b/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 寰�寰�浠h〃杩樻病甯冨眬/涓嶅彲瑙侊級
+  return el.offsetWidth > 0 && el.offsetHeight > 0
+}
+
+function initChartWhenReady() {
+  clearInitTimer()
+  initAttempts += 1
+
+  if (!isContainerReady()) {
+    // 绛夊鍣ㄧ湡姝f湁灏哄锛堥伩鍏嶉灞忓垵濮嬪寲鍋忕Щ/绌虹櫧锛岀儹鏇存柊鍚庢墠姝e父鐨勬儏鍐碉級
+    // 鏈�澶氶噸璇曠害 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 }
 )
diff --git a/src/hooks/useChartBackground.js b/src/hooks/useChartBackground.js
new file mode 100644
index 0000000..d69a1fb
--- /dev/null
+++ b/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锛堥潪鍏ㄥ睆銆乼ab銆佸姩鐢荤瓑锛�
+      // 杩欑鎯呭喌涓嬪厛涓嶅榻愶紝绛� 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)
+      }
+
+      // 鐩戝惉鈥滀粠涓嶅彲瑙佸埌鍙鈥濓紝瑙e喅鍒濆鍖栨椂鏈榻愪絾鐑洿鏂板張姝e父鐨勯棶棰�
+      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
+  }
+}
diff --git a/src/views/equipmentManagement/measurementEquipment/components/calibrationDia.vue b/src/views/equipmentManagement/measurementEquipment/components/calibrationDia.vue
index b5c1ea1..b7fa07e 100644
--- a/src/views/equipmentManagement/measurementEquipment/components/calibrationDia.vue
+++ b/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('鍙兘杈撳叆姝f暟'));
+						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 @@
 	}
 }
 
+// 澶勭悊鏈夋晥鏃ユ湡杈撳叆锛屽彧鍏佽姝f暣鏁�
+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) {
diff --git a/src/views/equipmentManagement/measurementEquipment/components/formDia.vue b/src/views/equipmentManagement/measurementEquipment/components/formDia.vue
index d2a1969..6b7feec 100644
--- a/src/views/equipmentManagement/measurementEquipment/components/formDia.vue
+++ b/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('鍙兘杈撳叆姝f暟'));
+            return;
+          }
+          if (!Number.isInteger(numValue)) {
+            callback(new Error('璇疯緭鍏ユ暣鏁�'));
+            return;
+          }
+          callback();
+        },
+        trigger: 'blur'
+      }
+    ],
     unit: [{required: true, message: "璇疯緭鍏�", trigger: "blur"}],
 	}
 })
@@ -254,6 +293,27 @@
 	}
 }
 
+// 澶勭悊鏈夋晥鏃ユ湡杈撳叆锛屽彧鍏佽姝f暣鏁�
+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) {
diff --git a/src/views/equipmentManagement/measurementEquipment/index.vue b/src/views/equipmentManagement/measurementEquipment/index.vue
index 4572f22..46ca100 100644
--- a/src/views/equipmentManagement/measurementEquipment/index.vue
+++ b/src/views/equipmentManagement/measurementEquipment/index.vue
@@ -82,6 +82,12 @@
     minWidth:150,
     align:"center"
 	},
+  {
+    label: "璁¢噺鍣ㄥ叿鍚嶇О",
+    prop: "name",
+    width: '160px',
+    align: "center",
+  },
 	{
 		label: "瀹夎浣嶇疆",
 		prop: "instationLocation",
diff --git a/src/views/equipmentManagement/spareParts/index.vue b/src/views/equipmentManagement/spareParts/index.vue
index eb0bdd5..0b0dae2 100644
--- a/src/views/equipmentManagement/spareParts/index.vue
+++ b/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;
diff --git a/src/views/reportAnalysis/PSIDataAnalysis/components/left-bottom.vue b/src/views/reportAnalysis/PSIDataAnalysis/components/left-bottom.vue
index 7daf096..669c826 100644
--- a/src/views/reportAnalysis/PSIDataAnalysis/components/left-bottom.vue
+++ b/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鍊肩浉鍚岀殑閭d竴椤规彁鍙栧嚭鏉ワ紝缁勬垚涓�涓璞�
@@ -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>
diff --git a/src/views/reportAnalysis/PSIDataAnalysis/components/left-top.vue b/src/views/reportAnalysis/PSIDataAnalysis/components/left-top.vue
index 581020d..8fcaa42 100644
--- a/src/views/reportAnalysis/PSIDataAnalysis/components/left-top.vue
+++ b/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鍊肩浉鍚岀殑閭d竴椤规彁鍙栧嚭鏉ワ紝缁勬垚涓�涓璞�
@@ -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>
 
diff --git a/src/views/reportAnalysis/dataDashboard/components/basic/left-top.vue b/src/views/reportAnalysis/dataDashboard/components/basic/left-top.vue
index c787fce..5b7e29e 100644
--- a/src/views/reportAnalysis/dataDashboard/components/basic/left-top.vue
+++ b/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)
 
 // 鏁版嵁鍒楄〃锛堟潵鑷帴鍙o級
 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>
diff --git a/src/views/reportAnalysis/financialAnalysis/components/left-bottom.vue b/src/views/reportAnalysis/financialAnalysis/components/left-bottom.vue
index 4a20578..3fe95d6 100644
--- a/src/views/reportAnalysis/financialAnalysis/components/left-bottom.vue
+++ b/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鍊肩浉鍚岀殑閭d竴椤规彁鍙栧嚭鏉ワ紝缁勬垚涓�涓璞�
@@ -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>
diff --git a/src/views/reportAnalysis/productionAnalysis/components/left-top.vue b/src/views/reportAnalysis/productionAnalysis/components/left-top.vue
index 07dfb54..0cce7d6 100644
--- a/src/views/reportAnalysis/productionAnalysis/components/left-top.vue
+++ b/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=瀛e害
 
@@ -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>
diff --git a/src/views/safeProduction/safetyTrainingAssessment/index.vue b/src/views/safeProduction/safetyTrainingAssessment/index.vue
index 0027c7f..1ab310f 100644
--- a/src/views/safeProduction/safetyTrainingAssessment/index.vue
+++ b/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="璇疯緭鍏ュ煿璁紪鍙锋悳绱�"
-                  @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>
+        <span class="search_title">鍩硅鏃ユ湡锛�</span>
+        <el-date-picker v-model="searchForm.trainingDate"
+                        value-format="YYYY-MM-DD"
+                        format="YYYY-MM-DD"
+                        @change="handleQuery"
+                        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,

--
Gitblit v1.9.3