QualityGoal.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. <template>
  2. <div class="sliding-chart-container">
  3. <ScreenComHeader :module-id="moduleId" title="质量目标" />
  4. <div ref="chartRef" style="width: 100%; height: 100%"></div>
  5. </div>
  6. </template>
  7. <script setup>
  8. import * as echarts from "echarts";
  9. import ScreenComHeader from "@/views/screens/configs/screenComHeader.vue";
  10. import { qualityReport } from "@/api/screens";
  11. const props = defineProps({
  12. moduleId: {
  13. type: String,
  14. required: true,
  15. },
  16. });
  17. const chartRef = ref(null);
  18. let chartInstance = null;
  19. let timer = null;
  20. const currentIndex = ref(0);
  21. // 初始化图表
  22. const initChart = () => {
  23. if (!chartRef.value) return;
  24. chartInstance = echarts.init(chartRef.value);
  25. updateChart();
  26. window.addEventListener("resize", handleResize);
  27. };
  28. // 数据转换函数
  29. const transformData = (rawData) => {
  30. return rawData.map((item) => {
  31. // 处理工序名称列表
  32. const operationNames = item.operationNameList.filter(
  33. (name) => name !== "成品率"
  34. );
  35. // 处理工序数值列表 - 每3个一组:[目标值, 实际值, 差值]
  36. const operationNumbers = item.operationNumberList;
  37. const targetValues = [];
  38. const actualValues = [];
  39. for (let i = 0; i < operationNumbers.length; i += 3) {
  40. // 跳过成品率数据(通常是最后3个)
  41. if (i / 3 >= operationNames.length) break;
  42. targetValues.push(parseFloat(operationNumbers[i]));
  43. actualValues.push(parseFloat(operationNumbers[i + 1]));
  44. }
  45. // 获取成品率数据(最后3个值)
  46. const yieldIndex = operationNumbers.length - 3;
  47. const productTarget = parseFloat(operationNumbers[yieldIndex]);
  48. const productActual = parseFloat(operationNumbers[yieldIndex + 1]);
  49. return {
  50. title:
  51. `${item.materialModel}工序成品率对比` +
  52. (item.materialCategory ? `(${item.materialCategory})` : ""),
  53. xAxis: operationNames,
  54. productTarget: productTarget,
  55. productActual: productActual,
  56. series: [
  57. {
  58. name: "工序目标值",
  59. data: targetValues,
  60. color: "#5470C6",
  61. type: "line",
  62. symbol: "diamond",
  63. symbolSize: 10,
  64. },
  65. {
  66. name: "工序实际值",
  67. data: actualValues,
  68. color: "#91CC75",
  69. type: "line",
  70. symbol: "circle",
  71. symbolSize: 8,
  72. },
  73. ],
  74. };
  75. });
  76. };
  77. // 更新图表数据
  78. const updateChart = () => {
  79. if (!chartInstance || !chartData.value.length) return;
  80. const currentData = chartData.value[currentIndex.value];
  81. // 计算工序差值
  82. const processDiff = currentData.series[0].data.map((target, index) => {
  83. const diff = currentData.series[1].data[index] - target;
  84. return parseFloat(diff.toFixed(1));
  85. });
  86. const option = {
  87. backgroundColor: "transparent",
  88. title: {
  89. text: currentData.title,
  90. left: "center",
  91. textStyle: {
  92. fontSize: 14,
  93. color: "#fff",
  94. },
  95. },
  96. tooltip: {
  97. trigger: "axis",
  98. axisPointer: {
  99. type: "cross",
  100. label: {
  101. backgroundColor: "#6a7985",
  102. },
  103. },
  104. backgroundColor: "rgba(0,0,0,0.7)",
  105. textStyle: {
  106. color: "#fff",
  107. },
  108. formatter: (params) => {
  109. const processIndex = params[0].dataIndex;
  110. const diffValue = processDiff[processIndex];
  111. const diffColor = diffValue >= 0 ? "#91CC75" : "#EE6666";
  112. const diffSymbol = diffValue >= 0 ? "+" : "";
  113. let result = `<div style="font-weight:bold;margin-bottom:5px">${params[0].axisValue}</div>`;
  114. // 工序数据
  115. result += `
  116. <div style="display:flex;align-items:center;margin:3px 0">
  117. <span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#5470C6;margin-right:5px"></span>
  118. <span style="flex:1">工序目标值:</span>
  119. <span style="font-weight:bold">${params[0].value}%</span>
  120. </div>
  121. <div style="display:flex;align-items:center;margin:3px 0">
  122. <span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#91CC75;margin-right:5px"></span>
  123. <span style="flex:1">工序实际值:</span>
  124. <span style="font-weight:bold">${params[1].value}%</span>
  125. </div>
  126. <div style="display:flex;align-items:center;margin:3px 0">
  127. <span style="display:inline-block;width:10px;height:10px;background:#EE6666;margin-right:5px"></span>
  128. <span style="flex:1">工序差值:</span>
  129. <span style="font-weight:bold;color:${diffColor}">${diffSymbol}${diffValue}%</span>
  130. </div>`;
  131. // 产品数据
  132. result += `
  133. <div style="border-top:1px solid rgba(255,255,255,0.2);margin:5px 0"></div>
  134. <div style="display:flex;align-items:center;margin:3px 0">
  135. <span style="display:inline-block;width:10px;height:2px;background:#5470C6;margin-right:5px"></span>
  136. <span style="flex:1">产品目标值:</span>
  137. <span style="font-weight:bold">${currentData.productTarget}%</span>
  138. </div>
  139. <div style="display:flex;align-items:center;margin:3px 0">
  140. <span style="display:inline-block;width:10px;height:2px;background:#91CC75;margin-right:5px"></span>
  141. <span style="flex:1">产品实际值:</span>
  142. <span style="font-weight:bold">${currentData.productActual}%</span>
  143. </div>`;
  144. return result;
  145. },
  146. },
  147. legend: {
  148. data: [
  149. ...currentData.series.map((item) => item.name),
  150. "产品目标值",
  151. "产品实际值",
  152. ],
  153. bottom: 10,
  154. textStyle: {
  155. color: "#fff",
  156. },
  157. },
  158. grid: {
  159. top: "10%",
  160. right: "12%",
  161. bottom: "15%", // 增加底部空间
  162. left: "8%",
  163. },
  164. xAxis: {
  165. type: "category",
  166. boundaryGap: false,
  167. data: currentData.xAxis,
  168. axisLine: {
  169. lineStyle: {
  170. color: "#fff",
  171. },
  172. },
  173. axisLabel: {
  174. color: "#fff",
  175. interval: 0,
  176. rotate: currentData.xAxis.some((name) => name.length > 4) ? 30 : 0,
  177. },
  178. },
  179. yAxis: {
  180. type: "value",
  181. name: "成品率(%)",
  182. nameTextStyle: {
  183. color: "#fff",
  184. padding: [0, 0, 0, 40],
  185. },
  186. min: 60, // 固定最小值,确保产品线可见
  187. max: 120,
  188. axisLine: {
  189. lineStyle: {
  190. color: "#fff",
  191. },
  192. },
  193. axisLabel: {
  194. color: "#fff",
  195. formatter: "{value}%",
  196. },
  197. splitLine: {
  198. lineStyle: {
  199. color: "rgba(255,255,255,0.1)",
  200. },
  201. },
  202. },
  203. series: [
  204. // 工序目标值(实线)
  205. {
  206. ...currentData.series[0],
  207. lineStyle: {
  208. color: "#5470C6",
  209. width: 3,
  210. },
  211. itemStyle: {
  212. color: "#5470C6",
  213. borderColor: "#fff",
  214. borderWidth: 1,
  215. },
  216. label: {
  217. show: true,
  218. position: "top",
  219. formatter: (params) => {
  220. const diff = processDiff[params.dataIndex];
  221. const diffColor = diff >= 0 ? "#91CC75" : "#EE6666";
  222. const diffSymbol = diff >= 0 ? "+" : "";
  223. return `${params.value}% (${diffSymbol}${diff}%)`;
  224. },
  225. color: "#fff",
  226. rich: {
  227. diff: {
  228. color: "#EE6666",
  229. padding: [0, 0, 0, 5],
  230. },
  231. },
  232. },
  233. },
  234. // 工序实际值(实线)
  235. {
  236. ...currentData.series[1],
  237. lineStyle: {
  238. color: "#91CC75",
  239. width: 3,
  240. },
  241. itemStyle: {
  242. color: "#91CC75",
  243. borderColor: "#fff",
  244. borderWidth: 1,
  245. },
  246. label: {
  247. show: false, // 只在目标值上显示标签
  248. },
  249. },
  250. // 产品目标值横线(虚线)
  251. {
  252. name: "产品目标值",
  253. type: "line",
  254. data: currentData.xAxis.map(() => currentData.productTarget),
  255. symbol: "none",
  256. lineStyle: {
  257. color: "#5470C6",
  258. width: 2,
  259. type: "dashed",
  260. },
  261. markLine: {
  262. silent: true,
  263. symbol: "none",
  264. lineStyle: {
  265. color: "#5470C6",
  266. width: 2,
  267. type: "dashed",
  268. },
  269. label: {
  270. show: true,
  271. position: "end",
  272. formatter: "产品目标: {c}%",
  273. color: "#5470C6",
  274. },
  275. data: [
  276. {
  277. yAxis: currentData.productTarget,
  278. },
  279. ],
  280. },
  281. },
  282. // 产品实际值横线(虚线)
  283. {
  284. name: "产品实际值",
  285. type: "line",
  286. data: currentData.xAxis.map(() => currentData.productActual),
  287. symbol: "none",
  288. lineStyle: {
  289. color: "#91CC75",
  290. width: 2,
  291. type: "dashed",
  292. },
  293. markLine: {
  294. silent: true,
  295. symbol: "none",
  296. lineStyle: {
  297. color: "#91CC75",
  298. width: 2,
  299. type: "dashed",
  300. },
  301. label: {
  302. show: true,
  303. position: "end",
  304. formatter: "产品实际: {c}%",
  305. color: "#91CC75",
  306. },
  307. data: [
  308. {
  309. yAxis: currentData.productActual,
  310. },
  311. ],
  312. },
  313. },
  314. ],
  315. animationDuration: 500,
  316. };
  317. chartInstance.setOption(option, true);
  318. };
  319. const nextChart = () => {
  320. currentIndex.value = (currentIndex.value + 1) % chartData.value.length;
  321. updateChart();
  322. resetTimer();
  323. };
  324. const resetTimer = () => {
  325. if (timer) clearInterval(timer);
  326. startTimer();
  327. };
  328. const startTimer = () => {
  329. timer = setInterval(nextChart, 3000);
  330. };
  331. const handleResize = () => {
  332. chartInstance?.resize();
  333. };
  334. const chartData = ref([]);
  335. const loadData = async () => {
  336. const rowData = await qualityReport();
  337. chartData.value = transformData(rowData.data);
  338. };
  339. onMounted(() => {
  340. loadData();
  341. initChart();
  342. startTimer();
  343. });
  344. onBeforeUnmount(() => {
  345. if (chartInstance) {
  346. chartInstance.dispose();
  347. window.removeEventListener("resize", handleResize);
  348. }
  349. if (timer) clearInterval(timer);
  350. });
  351. </script>
  352. <style scoped>
  353. /* 样式保持不变 */
  354. .sliding-chart-container {
  355. width: 100%;
  356. height: 100%;
  357. position: relative;
  358. background-color: transparent;
  359. border-radius: 8px;
  360. padding: 10px;
  361. box-sizing: border-box;
  362. }
  363. </style>