From 7f69902d2f2359d34ef935a34abb426a4ca7f608 Mon Sep 17 00:00:00 2001 From: Lei OT Date: Fri, 28 Nov 2025 14:43:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=B8=95=E7=B4=AF=E6=89=98=E5=88=86?= =?UTF-8?q?=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Pareto.jsx | 125 ++++++++++++++++++++++++++++++++++++++ src/views/Orders.jsx | 7 ++- 2 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 src/components/Pareto.jsx diff --git a/src/components/Pareto.jsx b/src/components/Pareto.jsx new file mode 100644 index 0000000..0545e6f --- /dev/null +++ b/src/components/Pareto.jsx @@ -0,0 +1,125 @@ +import React from 'react'; +import { DualAxes } from '@ant-design/charts'; +import { fixTo2Decimals, fixTo4Decimals, fixTo1Decimals, groupBy } from '../utils/commons'; + +const ParetoChart = ({ data, xField, yField, thresholds = { A: 80, B: 90 }, title = '帕累托分析', showCategory = true, showThresholds = true, yFieldAlias }) => { + // 1. 数据清洗 + const cleanedData = data + .filter((d) => d[yField]) + .sort((a, b) => b[yField] - a[yField]); + + // 2. 累计占比 & 分类 + const total = cleanedData.reduce((sum, d) => sum + d[yField], 0); + let cumulative = 0; + const barData = cleanedData.map((d) => { + cumulative += d[yField]; + const ratio = (cumulative / total) * 100; + let category = 'C'; + if (ratio <= thresholds.A) category = 'A'; + else if (ratio <= thresholds.B) category = 'B'; + return { [xField]: d[xField], [yField]: d[yField], Category: category }; + }); + + // Fixed the groupBy usage and barColor assignment + const barColor = (() => { + const grouped = groupBy(barData, 'Category'); + const ret = new Map(); + for (const [category, items] of Object.entries(grouped)) { + ret.set( + category, + items.map((e) => e[xField]) + ); + } + return ret; + })(); + + const lineData = barData.map((d, idx) => { + const cumulativeValue = barData.slice(0, idx + 1).reduce((sum, x) => sum + x[yField], 0); + return { [xField]: d[xField], Cumulative: fixTo2Decimals((cumulativeValue / total) * 100 )}; + }); + + // 3. DualAxes 配置 + const config = { + data: [barData, lineData], + xField, + yField: [yField, 'Cumulative'], + meta: { + [yField]: { + alias: yFieldAlias || yField, + formatter: (v) => v > 1_0000 ? `${fixTo1Decimals((v || 0) / 10000)} 万` : v, + }, + Cumulative: { + alias: '累计百分比', + formatter: (val) => `${val}%`, + }, + }, + yAxis: { + // 格式化左坐标轴 + Cumulative: { + // min: 0, + label: { + formatter: (val) => `${val}%`, + }, + }, + }, + geometryOptions: [ + { + geometry: 'column', + colorField: 'Category', + color: showCategory + ? (x) => { + const xText = x[xField]; + let thisC = null; + for (const [Category, Xs] of barColor) { + if (Xs.includes(xText)) { + thisC = Category; + } + } + if (thisC === 'A') return '#52c41a'; + if (thisC === 'B') return '#faad14'; + return '#5B8FF9'; + } + : '#5B8FF9', + }, + { + geometry: 'line', + color: '#ff4d4f', + // smooth: true, + lineStyle: { + lineWidth: 2, + }, + }, + ], + annotations: showThresholds + ? { + Cumulative: [ + { + type: 'line', + start: ['min', thresholds.A], + end: ['max', thresholds.A], + style: { stroke: '#52c41a', lineDash: [4, 4], lineWidth: 2 }, + // text: { content: `${thresholds.A}% 阈值`, position: 'end' }, + }, + { + type: 'line', + start: ['min', thresholds.B], + end: ['max', thresholds.B], + style: { stroke: '#faad14', lineDash: [4, 4], lineWidth: 2 }, + // text: { content: `${thresholds.B}% 阈值`, position: 'end' }, + }, + ], + } + : {}, + tooltip: { shared: true }, + title: { visible: true, text: title }, + }; + + return ( + <> +

{title}

+ + + ); +}; + +export default ParetoChart; diff --git a/src/views/Orders.jsx b/src/views/Orders.jsx index e80e4f6..3e4bac1 100644 --- a/src/views/Orders.jsx +++ b/src/views/Orders.jsx @@ -2,7 +2,7 @@ import React, { Component } from "react"; import { Row, Col, Tabs, Table, Divider, Spin } from "antd"; import { ContainerOutlined, BlockOutlined, SmileOutlined, TagsOutlined, GlobalOutlined, FullscreenOutlined, DingtalkOutlined, CarryOutOutlined, CoffeeOutlined, ClockCircleOutlined, HeartOutlined, IdcardOutlined, ContactsOutlined, GoogleOutlined, GooglePlusOutlined } from "@ant-design/icons"; import { stores_Context } from "../config"; -import { Line, Pie } from "@ant-design/charts"; +import { Line, Pie, } from "@ant-design/charts"; import { observer } from "mobx-react"; import * as config from "../config"; import { NavLink } from "react-router-dom"; @@ -11,6 +11,7 @@ import { utils, writeFileXLSX } from "xlsx"; import DateGroupRadio from '../components/DateGroupRadio'; import SearchForm from './../components/search/SearchForm'; import { TableExportBtn } from './../components/Data'; +import ParetoChart from "../components/Pareto"; class Orders extends Component { static contextType = stores_Context; @@ -349,7 +350,7 @@ class Orders extends Component { dataSource: table_data.dataSource, columns: table_data.columns, size: 'small', - pagination: false, + // pagination: false, scroll: { x: (100*(table_data.columns.length)) }, loading: orders_store.loading, }; @@ -503,10 +504,12 @@ class Orders extends Component { + {['Form', 'Product', 'Country', 'line'].includes(orders_store.active_tab_key) && } + {['Form', 'Product', 'Country', 'line'].includes(orders_store.active_tab_key) && }