From 494dde52763e61f914ff4eab5f44ef119ddcbbf3 Mon Sep 17 00:00:00 2001 From: Lei OT Date: Tue, 7 Nov 2023 17:16:49 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E9=80=8F=E8=A7=86=E5=9B=BE?= =?UTF-8?q?=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 透视表字段操作 --- package-lock.json | 361 +++++++++++++++++++- package.json | 4 + src/App.jsx | 13 +- src/components/DateGroupRadio/date.js | 2 +- src/components/search/SearchForm.jsx | 6 +- src/libs/ht.js | 88 ++++- src/stores/Distribution.js | 2 +- src/utils/commons.js | 14 + src/views/OrdersPivot.jsx | 471 ++++++++++++++++++++++++++ 9 files changed, 941 insertions(+), 20 deletions(-) create mode 100644 src/views/OrdersPivot.jsx diff --git a/package-lock.json b/package-lock.json index 9735eef..802efcc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,10 +12,14 @@ "@ant-design/pro-components": "^2.6.16", "antd": "^4.22.6", "dingtalk-jsapi": "^3.0.9", + "insert-css": "^2.0.0", "mobx": "^6.6.1", "mobx-react": "^7.5.2", "react": "^18.2.0", + "react-beautiful-dnd": "^13.1.1", + "react-dnd": "^16.0.1", "react-dom": "^18.2.0", + "react-draggable": "^4.4.6", "react-router-dom": "^6.3.0", "react-scripts": "^5.0.1", "web-vitals": "^2.1.4", @@ -4826,6 +4830,21 @@ "react-dom": ">=16.9.0" } }, + "node_modules/@react-dnd/asap": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", + "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==" + }, + "node_modules/@react-dnd/invariant": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz", + "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==" + }, + "node_modules/@react-dnd/shallowequal": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", + "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==" + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -5395,6 +5414,15 @@ "@types/node": "*" } }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", + "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -5475,8 +5503,7 @@ "node_modules/@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "dev": true + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, "node_modules/@types/q": { "version": "1.5.5", @@ -5497,7 +5524,6 @@ "version": "18.2.20", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.20.tgz", "integrity": "sha512-WKNtmsLWJM/3D5mG4U84cysVY31ivmyw85dE84fOCk5Hx78wezB/XEjVPWl2JTZ5FkEeaTJf+VgUAUn3PE7Isw==", - "dev": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -5513,6 +5539,17 @@ "@types/react": "*" } }, + "node_modules/@types/react-redux": { + "version": "7.1.30", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.30.tgz", + "integrity": "sha512-i2kqM6YaUwFKduamV6QM/uHbb0eCP8f8ZQ/0yWf+BsAVVsZPRYJ9eeGWZ3uxLfWwwA0SrPRMTPTqsPFkY3HZdA==", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, "node_modules/@types/resize-observer-browser": { "version": "0.1.7", "resolved": "https://registry.npmmirror.com/@types/resize-observer-browser/-/resize-observer-browser-0.1.7.tgz", @@ -5534,8 +5571,7 @@ "node_modules/@types/scheduler": { "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", - "dev": true + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" }, "node_modules/@types/semver": { "version": "7.3.13", @@ -7441,6 +7477,14 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -7775,6 +7819,14 @@ "postcss": "^8.4" } }, + "node_modules/css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "dependencies": { + "tiny-invariant": "^1.0.6" + } + }, "node_modules/css-declaration-sorter": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz", @@ -8599,6 +8651,16 @@ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, + "node_modules/dnd-core": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz", + "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==", + "dependencies": { + "@react-dnd/asap": "^5.0.1", + "@react-dnd/invariant": "^4.0.1", + "redux": "^4.2.0" + } + }, "node_modules/dns-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", @@ -11156,6 +11218,14 @@ "@babel/runtime": "^7.7.6" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/hoopy": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", @@ -11538,7 +11608,7 @@ }, "node_modules/insert-css": { "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/insert-css/-/insert-css-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/insert-css/-/insert-css-2.0.0.tgz", "integrity": "sha512-xGq5ISgcUP5cvGkS2MMFLtPDBtrtQPSFfC6gA6U8wHKqfjTIMZLZNxOItQnoSjdOzlXOLU/yD32RKC4SvjNbtA==" }, "node_modules/internal-slot": { @@ -14565,6 +14635,11 @@ "node": ">= 4.0.0" } }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -17049,6 +17124,11 @@ "performance-now": "^2.1.0" } }, + "node_modules/raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -17703,6 +17783,24 @@ "node": ">=14" } }, + "node_modules/react-beautiful-dnd": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz", + "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==", + "dependencies": { + "@babel/runtime": "^7.9.2", + "css-box-model": "^1.2.0", + "memoize-one": "^5.1.1", + "raf-schd": "^4.0.2", + "react-redux": "^7.2.0", + "redux": "^4.0.4", + "use-memo-one": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.5 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-color": { "version": "2.17.3", "resolved": "https://registry.npmmirror.com/react-color/-/react-color-2.17.3.tgz", @@ -17844,6 +17942,35 @@ "node": ">=8" } }, + "node_modules/react-dnd": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", + "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==", + "dependencies": { + "@react-dnd/invariant": "^4.0.1", + "@react-dnd/shallowequal": "^4.0.1", + "dnd-core": "^16.0.1", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" + }, + "peerDependencies": { + "@types/hoist-non-react-statics": ">= 3.3.1", + "@types/node": ">= 12", + "@types/react": ">= 16", + "react": ">= 16.14" + }, + "peerDependenciesMeta": { + "@types/hoist-non-react-statics": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-18.2.0.tgz", @@ -17856,6 +17983,19 @@ "react": "^18.2.0" } }, + "node_modules/react-draggable": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", + "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", + "dependencies": { + "clsx": "^1.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, "node_modules/react-error-overlay": { "version": "6.0.11", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", @@ -17871,6 +18011,35 @@ "resolved": "https://registry.npmmirror.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "node_modules/react-redux": { + "version": "7.2.9", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", + "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", + "dependencies": { + "@babel/runtime": "^7.15.4", + "@types/react-redux": "^7.1.20", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + }, + "peerDependencies": { + "react": "^16.8.3 || ^17 || ^18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-redux/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -18109,6 +18278,14 @@ "node": ">=6.0.0" } }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, "node_modules/reflect-metadata": { "version": "0.1.13", "resolved": "https://registry.npmmirror.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz", @@ -19746,6 +19923,11 @@ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" }, + "node_modules/tiny-invariant": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", + "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==" + }, "node_modules/tinycolor2": { "version": "1.4.2", "resolved": "https://registry.npmmirror.com/tinycolor2/-/tinycolor2-1.4.2.tgz", @@ -20231,6 +20413,14 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-memo-one": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", + "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", @@ -24912,6 +25102,21 @@ "rc-util": "^5.24.4" } }, + "@react-dnd/asap": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", + "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==" + }, + "@react-dnd/invariant": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz", + "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==" + }, + "@react-dnd/shallowequal": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", + "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==" + }, "@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -25327,6 +25532,15 @@ "@types/node": "*" } }, + "@types/hoist-non-react-statics": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", + "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -25407,8 +25621,7 @@ "@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "dev": true + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, "@types/q": { "version": "1.5.5", @@ -25429,7 +25642,6 @@ "version": "18.2.20", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.20.tgz", "integrity": "sha512-WKNtmsLWJM/3D5mG4U84cysVY31ivmyw85dE84fOCk5Hx78wezB/XEjVPWl2JTZ5FkEeaTJf+VgUAUn3PE7Isw==", - "dev": true, "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -25445,6 +25657,17 @@ "@types/react": "*" } }, + "@types/react-redux": { + "version": "7.1.30", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.30.tgz", + "integrity": "sha512-i2kqM6YaUwFKduamV6QM/uHbb0eCP8f8ZQ/0yWf+BsAVVsZPRYJ9eeGWZ3uxLfWwwA0SrPRMTPTqsPFkY3HZdA==", + "requires": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, "@types/resize-observer-browser": { "version": "0.1.7", "resolved": "https://registry.npmmirror.com/@types/resize-observer-browser/-/resize-observer-browser-0.1.7.tgz", @@ -25466,8 +25689,7 @@ "@types/scheduler": { "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", - "dev": true + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" }, "@types/semver": { "version": "7.3.13", @@ -26924,6 +27146,11 @@ "wrap-ansi": "^7.0.0" } }, + "clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -27185,6 +27412,14 @@ "postcss-selector-parser": "^6.0.9" } }, + "css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "requires": { + "tiny-invariant": "^1.0.6" + } + }, "css-declaration-sorter": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz", @@ -27796,6 +28031,16 @@ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, + "dnd-core": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz", + "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==", + "requires": { + "@react-dnd/asap": "^5.0.1", + "@react-dnd/invariant": "^4.0.1", + "redux": "^4.2.0" + } + }, "dns-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", @@ -29682,6 +29927,14 @@ "@babel/runtime": "^7.7.6" } }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "^16.7.0" + } + }, "hoopy": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", @@ -29963,7 +30216,7 @@ }, "insert-css": { "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/insert-css/-/insert-css-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/insert-css/-/insert-css-2.0.0.tgz", "integrity": "sha512-xGq5ISgcUP5cvGkS2MMFLtPDBtrtQPSFfC6gA6U8wHKqfjTIMZLZNxOItQnoSjdOzlXOLU/yD32RKC4SvjNbtA==" }, "internal-slot": { @@ -32217,6 +32470,11 @@ "fs-monkey": "^1.0.3" } }, + "memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -33847,6 +34105,11 @@ "performance-now": "^2.1.0" } }, + "raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -34308,6 +34571,20 @@ "whatwg-fetch": "^3.6.2" } }, + "react-beautiful-dnd": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz", + "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==", + "requires": { + "@babel/runtime": "^7.9.2", + "css-box-model": "^1.2.0", + "memoize-one": "^5.1.1", + "raf-schd": "^4.0.2", + "react-redux": "^7.2.0", + "redux": "^4.0.4", + "use-memo-one": "^1.1.1" + } + }, "react-color": { "version": "2.17.3", "resolved": "https://registry.npmmirror.com/react-color/-/react-color-2.17.3.tgz", @@ -34413,6 +34690,18 @@ } } }, + "react-dnd": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", + "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==", + "requires": { + "@react-dnd/invariant": "^4.0.1", + "@react-dnd/shallowequal": "^4.0.1", + "dnd-core": "^16.0.1", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" + } + }, "react-dom": { "version": "18.2.0", "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-18.2.0.tgz", @@ -34422,6 +34711,15 @@ "scheduler": "^0.23.0" } }, + "react-draggable": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", + "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", + "requires": { + "clsx": "^1.1.1", + "prop-types": "^15.8.1" + } + }, "react-error-overlay": { "version": "6.0.11", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", @@ -34437,6 +34735,26 @@ "resolved": "https://registry.npmmirror.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "react-redux": { + "version": "7.2.9", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", + "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", + "requires": { + "@babel/runtime": "^7.15.4", + "@types/react-redux": "^7.1.20", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + }, + "dependencies": { + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + } + } + }, "react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -34623,6 +34941,14 @@ "minimatch": "^3.0.5" } }, + "redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "requires": { + "@babel/runtime": "^7.9.2" + } + }, "reflect-metadata": { "version": "0.1.13", "resolved": "https://registry.npmmirror.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz", @@ -35860,6 +36186,11 @@ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" }, + "tiny-invariant": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", + "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==" + }, "tinycolor2": { "version": "1.4.2", "resolved": "https://registry.npmmirror.com/tinycolor2/-/tinycolor2-1.4.2.tgz", @@ -36208,6 +36539,12 @@ "requires-port": "^1.0.0" } }, + "use-memo-one": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", + "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", + "requires": {} + }, "use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", diff --git a/package.json b/package.json index 332a4f8..5e8f739 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,14 @@ "@ant-design/pro-components": "^2.6.16", "antd": "^4.22.6", "dingtalk-jsapi": "^3.0.9", + "insert-css": "^2.0.0", "mobx": "^6.6.1", "mobx-react": "^7.5.2", "react": "^18.2.0", + "react-beautiful-dnd": "^13.1.1", + "react-dnd": "^16.0.1", "react-dom": "^18.2.0", + "react-draggable": "^4.4.6", "react-router-dom": "^6.3.0", "react-scripts": "^5.0.1", "web-vitals": "^2.1.4", diff --git a/src/App.jsx b/src/App.jsx index cf57d31..df96778 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -42,6 +42,7 @@ import ExchangeRate from './charts/ExchangeRate'; import KPI from './views/KPI'; import Distribution from './views/Distribution'; import Detail from './views/Detail'; +import OrderPivot from './views/OrdersPivot'; import Welcome from './views/Welcome'; import { stores_Context, APP_VERSION } from './config'; import { WaterMark } from '@ant-design/pro-components'; @@ -52,7 +53,7 @@ const App = () => { const menu_items = [ { key: 1, label: 欢迎, icon: }, - { key: 'annual', label: 综合看板, icon: }, + { key: 'annual', label: 综合看板, icon: }, { key: 2, label: '市场', @@ -61,12 +62,17 @@ const App = () => { { key: 21, label: 订单数据, - icon: , + // icon: , }, { key: 22, label: 仪表盘, - icon: , + // icon: , + }, + { + key: 'orders-pivot', + label: 数据透视, + // icon: , }, ], }, @@ -190,6 +196,7 @@ const App = () => { } /> } /> } /> + } /> }> } /> diff --git a/src/components/DateGroupRadio/date.js b/src/components/DateGroupRadio/date.js index 8b249a1..b413cd2 100644 --- a/src/components/DateGroupRadio/date.js +++ b/src/components/DateGroupRadio/date.js @@ -153,6 +153,6 @@ export const resultDataCb = (dataRaw, dateGroup, { data1, data2 }, fieldMapper, })); const retData = [].concat(parseData1, reindexData2 ).map(ele => ({...ele, [fieldMapper.dateKey]: data1KeyMappedStr[ele[fieldMapper.dateKey]] || data2KeyMappedStr[ele[fieldMapper.dateKey]]})); const avg1 = parse1.avgVal; - // console.log('callback', dateGroup, retData, data1KeyMappedStr, data2KeyMappedStr); + // console.log('callback', dateGroup, retData, avg1, parse2.avgVal, data1KeyMappedStr, data2KeyMappedStr); cb(dateGroup, retData, avg1, parse2.avgVal); }; diff --git a/src/components/search/SearchForm.jsx b/src/components/search/SearchForm.jsx index ccd4d02..d86390e 100644 --- a/src/components/search/SearchForm.jsx +++ b/src/components/search/SearchForm.jsx @@ -312,14 +312,16 @@ function getFields(props) { rules={[{ required: true, message: '选择小组' }]} > - , fieldProps?.DepartmentList?.col + , + fieldProps?.DepartmentList?.col ), item( 'WebCode', 99, - + , + fieldProps?.WebCode?.col ), item( 'IncludeTickets', diff --git a/src/libs/ht.js b/src/libs/ht.js index 0704ebe..a423627 100644 --- a/src/libs/ht.js +++ b/src/libs/ht.js @@ -1,4 +1,4 @@ -import { fixTo4Decimals, fixTo1Decimals } from "../utils/commons"; +import { fixTo4Decimals, fixTo1Decimals, fixToInt, groupBy, sortBy, cloneDeep, pick, unique, flush } from "../utils/commons"; /** * 事业部 @@ -173,3 +173,89 @@ export const KPISubjects = [ // { key: 'reply_eff_wa', value: 'reply_eff_wa', label: 'WA回复效率'}, // { key: 'sum_person_num', value: 'sum_person_num', label: '人数' }, ]; + + +/** + * 数据透视计算 + * @param {object[]} data + * @param {any[]} groupby + * @param {object[]} keys + * @param {string} value + * @returns + */ +export const pivotBy = (data, [rows, columns, date], keys, value) => { + const groupbyKeys = flush([].concat(rows, columns, [date])); + console.log('pivotBy', [rows, columns, date], groupbyKeys ); + const uniqueKeys = groupbyKeys.map(keyField => { + const keyu = [...new Set(data.map(f => f[keyField]))]; + return keyu; + }); + const pivotResult = []; // new Array(uniqueKeys.reduce((r, v) => r * v.length, 1)); + const groupData = groupBy(data, row => groupbyKeys.map(kk => `${row[kk]}`).join('=@=')); + + Object.keys(groupData).map((group_str) => { + const _rowKey = groupData[group_str].map((v) => v.key).join('_'); + const _len = groupData[group_str].length; + const _row = { + ...pick(groupData[group_str][0], groupbyKeys), + ...(groupbyKeys.length < 2 ? { rowKey: '总' } : { rowKey: cloneDeep(groupbyKeys).slice(0, -1).map(_k => groupData[group_str][0][_k]).join("»") }), + key: _rowKey, + SumOrder: _len, + SumPersonNum: groupData[group_str].reduce((r, v) => r + v.personNum, 0), + ConfirmOrder: groupData[group_str].reduce((r, v) => r + (Number(v.orderState) === 1 ? 1 : 0), 0), + transactions: groupData[group_str].reduce((r, v) => r + v.transactions, 0), + SumML: groupData[group_str].reduce((r, v) => r + v.ML, 0), + quotePrice: groupData[group_str].reduce((r, v) => r + v.quotePrice, 0), // todo: quotePrice + tourdays: Math.ceil(groupData[group_str].reduce((r, v) => r + v.tourdays, 0) / _len), + applyDays: Math.ceil(groupData[group_str].reduce((r, v) => r + v.applyDays, 0) / _len), + confirmDays: Math.ceil(groupData[group_str].reduce((r, v) => r + v.confirmDays, 0) / _len), + }; + pivotResult.push({ + ..._row, + ConfirmRates: _row.ConfirmOrder ? fixTo4Decimals(_row.ConfirmOrder / _row.SumOrder) : 0, + OrderValue: _row.SumOrder ? fixToInt(_row.SumML / _row.SumOrder) : 0, + }); + return group_str; + }); + // 列转置 + const rowsData = groupBy(data, row => rows.map(kk => `${row[kk]}`).join('=@=')); + const rowsWithColumns = Object.keys(rowsData).map(rowKey => { + const _colData = groupBy(rowsData[rowKey], crow => columns.map(kk => `${crow[kk]}`).join('=@=')); + const _topRowKey = []; + return Object.keys(_colData).reduce((r, colKey) => { + const _len = _colData[colKey].length; + const _rowKey = _colData[colKey].map((v) => v.key).join('_'); + _topRowKey.push(_rowKey); + const _row = { + key: _rowKey, + SumOrder: _len, + SumPersonNum: _colData[colKey].reduce((r, v) => r + v.personNum, 0), + ConfirmOrder: _colData[colKey].reduce((r, v) => r + (Number(v.orderState) === 1 ? 1 : 0), 0), + transactions: _colData[colKey].reduce((r, v) => r + v.transactions, 0), + SumML: _colData[colKey].reduce((r, v) => r + v.ML, 0), + quotePrice: _colData[colKey].reduce((r, v) => r + v.quotePrice, 0), // todo: quotePrice + tourdays: Math.ceil(_colData[colKey].reduce((r, v) => r + v.tourdays, 0) / _len), + applyDays: Math.ceil(_colData[colKey].reduce((r, v) => r + v.applyDays, 0) / _len), + confirmDays: Math.ceil(_colData[colKey].reduce((r, v) => r + v.confirmDays, 0) / _len), + }; + const _rowCalc = { + ConfirmRates: _row.ConfirmOrder ? fixTo4Decimals(_row.ConfirmOrder / _row.SumOrder) : 0, + OrderValue: _row.SumOrder ? fixToInt(_row.SumML / _row.SumOrder) : 0, + }; + Object.assign(r.columns, {[colKey]: {..._row, ..._rowCalc}}); + // (r.columns || (r.columns = {})).push({[colKey]: {..._row, ..._rowCalc}}); + return {...r, key: _topRowKey.join('_')}; + }, {...pick(rowsData[rowKey][0], rows), columns: {}}); + }).map(everyR => { + const allColumns = Object.values(everyR.columns).reduce((r, c) => r.concat([c]), []); + return ['ConfirmOrder', 'SumOrder', 'SumML', 'transactions', 'SumPersonNum'].reduce( + (r, skey) => ({ ...r, + [skey]: allColumns.reduce((a, c) => a + c[skey], 0), + }), + everyR + ); // todo: 其他数据列计算 + }); + + // console.log('pivot res', uniqueKeys, rowsWithColumns, pivotResult); + return { data: pivotResult, columnValues: uniqueKeys, summary: rowsWithColumns }; +}; diff --git a/src/stores/Distribution.js b/src/stores/Distribution.js index 0e92697..f21a92b 100644 --- a/src/stores/Distribution.js +++ b/src/stores/Distribution.js @@ -69,7 +69,7 @@ class Distribution { this.scatterDays = daysData; }); } - return this.detailData; + return json.result; }; resetData = () => { diff --git a/src/utils/commons.js b/src/utils/commons.js index c65f069..be8b8b8 100644 --- a/src/utils/commons.js +++ b/src/utils/commons.js @@ -353,6 +353,20 @@ export function pick(object, keys) { }, {}); } +/** + * 返回对象的副本,经过筛选以省略指定的键。 + * @param {*} object + * @param {string[]} keysToOmit + * @returns + */ +export function omit(object, keysToOmit) { + return Object.fromEntries( + Object.entries(object).filter( + ([key]) => !keysToOmit.includes(key) + ) + ); +} + /** * 深拷贝 */ diff --git a/src/views/OrdersPivot.jsx b/src/views/OrdersPivot.jsx new file mode 100644 index 0000000..8fe250e --- /dev/null +++ b/src/views/OrdersPivot.jsx @@ -0,0 +1,471 @@ +import { useContext, useEffect, useState } from 'react'; +import { observer } from 'mobx-react'; +import { useParams } from 'react-router-dom'; +import { stores_Context } from '../config'; +import { Row, Col, Spin, Table, Button, Select, Typography, Card } from 'antd'; +import { cloneDeep, groupBy, isEmpty, omit, sortBy } from '../utils/commons'; +import { dataFieldAlias, pivotBy } from '../libs/ht'; +import SearchForm from '../components/search/SearchForm'; +import { Line } from '@ant-design/plots'; +import DateGroupRadio from '../components/DateGroupRadio'; + +const { Text } = Typography; + +const filterFields = [ + { key: 'country', label: '国籍' }, + { key: 'SourceType', label: '来源类型' }, + { key: 'productType', label: '产品类型' }, + { key: 'guestGroupType', label: '客群类别' }, + { key: 'travelMotivation', label: '出行动机' }, + { key: 'operatorName', label: '顾问' }, + // todo: 目的地, 目的地国家, 页面类型[PPC, NL...] +]; +const filterFieldsMapped = filterFields.reduce((r, v) => ({ ...r, [v.key]: v }), {}); + +const combineArrays = (arr, sep = '_') => { + // if (arr.length === 0) return []; + if (arr.length === 1) return arr[0].map((item) => item); + + const output = []; + const head = arr[0]; + const tail = arr.slice(1); + + head.forEach((item) => { + const suffixes = combineArrays(tail, sep); + suffixes.forEach((suffix) => { + output.push(`${item}${sep}${suffix}`); + }); + }); + + return output; +}; + +// 注意TdCell要提到DataTable作用域外声明 +const TdCell = (tdprops) => { + // onMouseEnter, onMouseLeave在数据量多的时候,会严重阻塞表格单元格渲染,严重影响性能 + const { onMouseEnter, onMouseLeave, ...restProps } = tdprops; + return ; +}; + +export default observer((props) => { + const { field } = useParams(); + const { date_picker_store: searchFormStore, orders_store, DistributionStore } = useContext(stores_Context); + const { formValues, formValuesToSub } = searchFormStore; + // const { curTab, scatterDays, detailData } = DistributionStore; + + const [loading, setLoading] = useState(false); + const [rawData, setRawData] = useState([]); + const [dataBeforePick, setDataBeforePick] = useState([]); + const [dataBeforeXChange, setDataBeforeXChange] = useState([]); + const [dataSource, setDataSource] = useState([]); + const [dataSourceMapped, setDataSourceMapped] = useState({}); + + const [pivotTableDataSource, setPivotTableDataSource] = useState([]); + + const [pivotColumns, setPivotColumns] = useState([]); + const [pivotDateColumns, setPivotDateColumns] = useState([[], []]); + const [pivotDateColumnsValues, setPivotDateColumnsValues] = useState([]); + + useEffect(() => { + calcDataByDate(); + resetX(); + resetItemFilter(); + + return () => {}; + }, [pivotDateColumns]); + + useEffect(() => { + if (lineChartX === 'day') { + setDataBeforeXChange(dataSource); + } + + return () => {}; + }, [dataSource]); + + const detailRefresh = async (obj) => { + setLoading(true); + DistributionStore.getDetailData({ + ...(obj || formValuesToSub), + }).then((resData) => { + setLoading(false); + setRawData(resData); + // const { data, columnValues } = pivotBy(resData, ['country', 'guestGroupType', 'applyDate'], 'personNum'); + // setPivotDateColumns(['applyDate']); + calcDataByDate(resData); + // setLineChartX('day'); + resetX(); + resetItemFilter(); + }); + }; + + /** + * 走势的数据 + * 汇总 + */ + const calcDataByDate = (_rawData) => { + // console.log(';;;;;', pivotDateColumns); + const { data, columnValues, summary } = pivotBy(_rawData || rawData, [].concat(pivotDateColumns, ['applyDate'])); + setPivotDateColumnsValues(cloneDeep(columnValues).slice(0, -1)); + setDataBeforePick(data.sort(sortBy('applyDate'))); + setDataSource(data.sort(sortBy('applyDate'))); + + setPivotTableDataSource(summary.sort(sortBy('SumOrder')).reverse()); + }; + + const line_config = { + // data: dataSource, + padding: 'auto', + xField: 'applyDate', + yField: 'SumOrder', + seriesField: 'rowKey', + // xAxis: { + // type: 'timeCat', + // }, + yAxis: { + min: 0, + maxTickInterval: 5, + }, + // smooth: true, + label: {}, // 显示标签 + legend: { + position: 'right-top', + // title: { + // text: '总合计 ' + dataSource.reduce((a, b) => a + b.SumOrder, 0), + // }, + itemMarginBottom: 12, // 垂直间距 + }, + }; + + const [lineConfig, setLineConfig] = useState(cloneDeep(line_config)); + + // 透视配置:行列选项 + const [leftFields, setLeftFields] = useState(filterFields); + const [rightFields, setRightFields] = useState(filterFields); + const [rowFields, setRowFields] = useState([]); + const [columnFields, setColumnFields] = useState([]); + + const handleRowsPick = (v) => { + const pickKeys = v.map((ele) => ele.key); + setRowFields(pickKeys); + // const leftFieldsMapped = leftFields.reduce((r, v) => ({ ...r, [v.key]: v }), {}); + const _left = omit(filterFieldsMapped, pickKeys); + setRightFields(isEmpty(v) ? filterFields : Object.values(_left)); + // setPivotDateColumns([].concat(pickKeys, columnFields)); + setPivotDateColumns([pickKeys, columnFields]); + resetItemFilter(); + }; + + const handleColsPick = (val) => { + const pickKeys = isEmpty(val) ? [] : Array.isArray(val) ? val.map((ele) => ele.key) : [val.key]; + setColumnFields(pickKeys); + const afterLeft = Object.values(omit(filterFieldsMapped, rowFields)); + setRightFields(afterLeft); // 单选 + // const rightFieldsMapped = rightFields.reduce((r, v) => ({ ...r, [v.key]: v }), {}); + // const _left = omit(rightFieldsMapped, pickKeys); + // setRightFields(isEmpty(val) ? afterLeft : Object.values(_left)); // 多选 + // setPivotDateColumns([].concat(rowFields, pickKeys)); + setPivotDateColumns([rowFields, pickKeys]); + resetItemFilter(); + }; + + // 行列的值选项 + const [rowsItemValues, setRowsItemValues] = useState(); + const [columnsItemValues, setColumnsItemValues] = useState(); + const [rowsFilter, setRowsFilter] = useState(); + const [columnsFilter, setColumnsFilter] = useState(); + const resetItemFilter = () => { + setRowsItemValues(null); + setColumnsItemValues(null); + setRowsFilter(null); + setColumnsFilter(null); + }; + const handleFieldsItemPick = (v, columnsIndex, columnsName, actionSeries) => { + const _curFilter = { [columnsName]: actionSeries === 'row' ? v : [v] }; + // console.log('handleFieldsItemPick', v, columnsIndex, columnsName, actionSeries, _curFilter); + const _rowsF = actionSeries === 'row' ? { ...rowsFilter, ..._curFilter } : rowsFilter; + const _columnsF = actionSeries === 'col' ? { ...columnsFilter, ..._curFilter } : columnsFilter; + actionSeries === 'row' ? setRowsFilter(_rowsF) : setColumnsFilter(_columnsF); + const currentFilterMerge = { + rows: _rowsF || {}, + columns: _columnsF || {}, + }; + setRowsItemValues(currentFilterMerge.rows); + setColumnsItemValues(currentFilterMerge.columns); + + const rowsFilterFields = Object.keys(currentFilterMerge.rows).filter((ele) => currentFilterMerge.rows[ele].length); + const dataMappedByRows = groupBy(dataBeforePick, (row) => rowsFilterFields.map((kk) => `${row[kk]}`).join('=@=')); + const rowsFilterKey = isEmpty(rowsFilterFields) + ? [] + : combineArrays( + Object.values(currentFilterMerge.rows) + .map((kv) => kv.map((kf) => kf.key)) + .filter((s) => s.length), + '=@=' + ); + const afterRowsFilter = isEmpty(rowsFilterFields) ? dataBeforePick : rowsFilterKey.reduce((r, _key) => r.concat(dataMappedByRows[_key]), []); + // console.log('afterRowsFilter', rowsFilterFields, afterRowsFilter); + + const columnsFilterFields = Object.keys(currentFilterMerge.columns).filter((ele) => currentFilterMerge.rows[ele].length); + const allFilterValues = [].concat( + rowsFilterFields.reduce((r, v) => r.concat(currentFilterMerge.rows[v]), []), + columnsFilterFields.reduce((r, v) => r.concat(currentFilterMerge.columns[v]), []) + ); + + const dataMapped = groupBy(afterRowsFilter, (row) => row[columnsName]); + const pickData = isEmpty(v) ? afterRowsFilter : Array.isArray(v) ? v.reduce((r, v) => r.concat(dataMapped[v.value]), []) : dataMapped[v.value]; + + setDataSource(allFilterValues.length === 0 ? dataBeforePick : pickData.sort(sortBy('applyDate'))); + resetX(); + }; + + // 日月年切换 debug: raw data action + const [lineChartX, setLineChartX] = useState('day'); + const [avgLine1, setAvgLine1] = useState(0); + const orderCountDataMapper = { data1: 'data1', data2: undefined }; + const orderCountDataFieldMapper = { 'dateKey': 'applyDate', 'valueKey': 'SumOrder', 'seriesKey': 'rowKey', _f: 'sum' }; + const resetX = () => { + setLineChartX('day'); + setAvgLine1(0); + }; + + const onChangeXDateFieldGroup = (value, data, avg1) => { + const { xField, yField, seriesField } = lineConfig; + const groupByDate = data.reduce((r, v) => { + (r[v[xField]] || (r[v[xField]] = [])).push(v); + return r; + }, {}); + const _data = Object.keys(groupByDate).reduce((r, _d) => { + const xAxisGroup = groupByDate[_d].reduce((a, v) => { + (a[v[seriesField]] || (a[v[seriesField]] = [])).push(v); + return a; + }, {}); + Object.keys(xAxisGroup).map((_group) => { + const summaryVal = xAxisGroup[_group].reduce((rows, row) => rows + row[yField], 0); + r.push({ ...xAxisGroup[_group][0], [yField]: summaryVal }); + return _group; + }); + return r; + }, []); + // .map((row) => ({ [xField]: row[xField], [yField]: row[yField], [seriesField]: row[seriesField], rowX: row.dateRange[0] })); + + setLineChartX(value); + setDataSource(_data); + setAvgLine1(avg1); + }; + + const targetTableProps = { + loading: false, + // sticky: true, + scroll: { x: 1000, y: 400 }, + pagination: false, + columns: [ + ...pivotDateColumns[0].map((ele) => ({ key: ele, title: filterFieldsMapped[ele].label, dataIndex: ele, width: '6em', fixed: 'left' })), + // { key: 'groupsLabel', title: '', dataIndex: 'groupsLabel' }, + { key: 'SumOrder', title: '订单数', dataIndex: 'SumOrder', width: '5em' }, + ...pivotDateColumns[1].map((ele) => ({ + key: ele, + title: filterFieldsMapped[ele].label, + align: 'left', + children: cloneDeep(pivotDateColumnsValues) + .slice(-1)[0] + .sort() + .map((col) => ({ key: col, title: col || '(空)', dataIndex: col, width: '6em', render: (_, r) => r.columns[col]?.SumOrder || '' })), + })), + ], + }; + + return ( + <> + + + { + detailRefresh(obj); + }} + /> + + + + {/* extra={} */} + + + + {/* todo: 拖拽的操作 */} + {/*
+ {filterFields.map((tag) => ( + -1} onChange={(checked) => handleChange(tag.key, checked)} color={'orange'}> + {tag.label} + + ))} +
*/} + + + + + 行: + + + + + + {rowFields.length > 0 + ? cloneDeep(pivotDateColumnsValues) + .slice(0, rowFields.length) + .map((_colArr, _colIndex) => ( + + + {filterFieldsMapped[pivotDateColumns[0][_colIndex]].label}: + + + + + + )) + : null} + + + + + + + 列: + + + + + + {/* {columnFields.length > 0 + ? cloneDeep(pivotDateColumnsValues) + .slice(rowFields.length) + .map((_colArr, _colIndex) => ( + <> + + + {filterFieldsMapped[pivotDateColumns[_colIndex + rowFields.length]]?.label || _colIndex + rowFields.length}: + + + + + + + )) + : null} */} + + + +
+
+ +
+ + +

+ 走势: {dataFieldAlias[lineConfig.yField].label} +

+ + + + +
+ + + +
+ +
+ +

+ 透视汇总表: {dataFieldAlias[lineConfig.yField].label} +

+ + + + + ); +}); From cfeecf4af58d22a3bcb0e75e71a61f37ae83ddfe Mon Sep 17 00:00:00 2001 From: Lei OT Date: Mon, 13 Nov 2023 11:30:34 +0800 Subject: [PATCH 2/2] =?UTF-8?q?perf:=20=E9=80=8F=E8=A7=86=E8=AE=A1?= =?UTF-8?q?=E7=AE=97=E9=80=9F=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.css | 2 +- src/App.jsx | 6 +- src/libs/ht.js | 315 ++++++++++++++----- src/stores/DataPivot.js | 49 +++ src/stores/Index.js | 2 + src/views/{OrdersPivot.jsx => DataPivot.jsx} | 141 +++++---- 6 files changed, 378 insertions(+), 137 deletions(-) create mode 100644 src/stores/DataPivot.js rename src/views/{OrdersPivot.jsx => DataPivot.jsx} (78%) diff --git a/src/App.css b/src/App.css index 1acdfbf..02207ca 100644 --- a/src/App.css +++ b/src/App.css @@ -32,7 +32,7 @@ padding: 0; } .p-s1{ - padding: .5em; + padding: .5rem!important; } .sticky-top{ diff --git a/src/App.jsx b/src/App.jsx index df96778..f76d996 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -42,7 +42,7 @@ import ExchangeRate from './charts/ExchangeRate'; import KPI from './views/KPI'; import Distribution from './views/Distribution'; import Detail from './views/Detail'; -import OrderPivot from './views/OrdersPivot'; +import DataPivot from './views/DataPivot'; import Welcome from './views/Welcome'; import { stores_Context, APP_VERSION } from './config'; import { WaterMark } from '@ant-design/pro-components'; @@ -71,7 +71,7 @@ const App = () => { }, { key: 'orders-pivot', - label: 数据透视, + label: 数据透视, // icon: , }, ], @@ -196,7 +196,7 @@ const App = () => { } /> } /> } /> - } /> + } /> }> } /> diff --git a/src/libs/ht.js b/src/libs/ht.js index a423627..bcf85dc 100644 --- a/src/libs/ht.js +++ b/src/libs/ht.js @@ -1,4 +1,4 @@ -import { fixTo4Decimals, fixTo1Decimals, fixToInt, groupBy, sortBy, cloneDeep, pick, unique, flush } from "../utils/commons"; +import { fixTo4Decimals, fixTo1Decimals, fixToInt, groupBy, sortBy, cloneDeep, pick, unique, flush } from '../utils/commons'; /** * 事业部 @@ -88,7 +88,7 @@ export const sites = [ { value: '30', key: '30', label: 'TP', code: 'trippest' }, { value: '31', key: '31', label: '花梨鹰', code: 'HLY' }, ]; -export const sitesMappedByCode = sites.reduce((a, c) => ({ ...a, [String(c.code)]: {...c, key: c.code, value: c.code } }), {}); +export const sitesMappedByCode = sites.reduce((a, c) => ({ ...a, [String(c.code)]: { ...c, key: c.code, value: c.code } }), {}); export const dateTypes = [ { key: 'applyDate', value: 'applyDate', label: '提交日期' }, { key: 'ConfirmDate', value: 'ConfirmDate', label: '确认日期' }, @@ -123,11 +123,7 @@ export const dataFieldAlias = dataFieldOptions.reduce( * KPI对象 */ export const KPIObjects = [ - { key: 'overview', value: 'overview', label: '海纳', data: [ - { key: 'ALL', value: 'ALL', label: '海纳' }, - ...overviewGroup - ] - }, + { key: 'overview', value: 'overview', label: '海纳', data: [{ key: 'ALL', value: 'ALL', label: '海纳' }, ...overviewGroup] }, { key: 'bizarea', value: 'bizarea', @@ -174,88 +170,251 @@ export const KPISubjects = [ // { key: 'sum_person_num', value: 'sum_person_num', label: '人数' }, ]; - /** * 数据透视计算 * @param {object[]} data - * @param {any[]} groupby - * @param {object[]} keys - * @param {string} value + * @param {any[]} groupbyKeys * @returns */ -export const pivotBy = (data, [rows, columns, date], keys, value) => { +export const pivotBy = (data, [rows, columns, date]) => { + // console.time('pivot----'); + console.log('pivotBy', [rows, columns, date]); const groupbyKeys = flush([].concat(rows, columns, [date])); - console.log('pivotBy', [rows, columns, date], groupbyKeys ); - const uniqueKeys = groupbyKeys.map(keyField => { - const keyu = [...new Set(data.map(f => f[keyField]))]; - return keyu; - }); - const pivotResult = []; // new Array(uniqueKeys.reduce((r, v) => r * v.length, 1)); - const groupData = groupBy(data, row => groupbyKeys.map(kk => `${row[kk]}`).join('=@=')); - - Object.keys(groupData).map((group_str) => { - const _rowKey = groupData[group_str].map((v) => v.key).join('_'); - const _len = groupData[group_str].length; - const _row = { - ...pick(groupData[group_str][0], groupbyKeys), - ...(groupbyKeys.length < 2 ? { rowKey: '总' } : { rowKey: cloneDeep(groupbyKeys).slice(0, -1).map(_k => groupData[group_str][0][_k]).join("»") }), - key: _rowKey, - SumOrder: _len, - SumPersonNum: groupData[group_str].reduce((r, v) => r + v.personNum, 0), - ConfirmOrder: groupData[group_str].reduce((r, v) => r + (Number(v.orderState) === 1 ? 1 : 0), 0), - transactions: groupData[group_str].reduce((r, v) => r + v.transactions, 0), - SumML: groupData[group_str].reduce((r, v) => r + v.ML, 0), - quotePrice: groupData[group_str].reduce((r, v) => r + v.quotePrice, 0), // todo: quotePrice - tourdays: Math.ceil(groupData[group_str].reduce((r, v) => r + v.tourdays, 0) / _len), - applyDays: Math.ceil(groupData[group_str].reduce((r, v) => r + v.applyDays, 0) / _len), - confirmDays: Math.ceil(groupData[group_str].reduce((r, v) => r + v.confirmDays, 0) / _len), - }; - pivotResult.push({ - ..._row, - ConfirmRates: _row.ConfirmOrder ? fixTo4Decimals(_row.ConfirmOrder / _row.SumOrder) : 0, - OrderValue: _row.SumOrder ? fixToInt(_row.SumML / _row.SumOrder) : 0, - }); - return group_str; - }); - // 列转置 - const rowsData = groupBy(data, row => rows.map(kk => `${row[kk]}`).join('=@=')); - const rowsWithColumns = Object.keys(rowsData).map(rowKey => { - const _colData = groupBy(rowsData[rowKey], crow => columns.map(kk => `${crow[kk]}`).join('=@=')); - const _topRowKey = []; - return Object.keys(_colData).reduce((r, colKey) => { - const _len = _colData[colKey].length; - const _rowKey = _colData[colKey].map((v) => v.key).join('_'); - _topRowKey.push(_rowKey); - const _row = { + const getKeys = (keys) => keys.map((keyField) => [...new Set(data.map((f) => f[keyField]))]); + const [rowsKeys, columnsKeys, dateKeys] = [getKeys(rows), getKeys(columns), getKeys([date])]; + + const calcTradeFields = (dataObj, keepKeys = [], seriesKey = '') => { + const outerKeys = []; + const _keepKeys = [...keepKeys, seriesKey]; + const DataGroupByKeys = {}; + + Object.keys(dataObj).forEach((colKey) => { + const _len = dataObj[colKey].length; + const _rowKey = dataObj[colKey].map((v) => v.key).join('_'); + outerKeys.push(_rowKey); + + const initialData = { + ...pick(dataObj[colKey][0], _keepKeys), + ...(keepKeys.length === 0 + ? { rowLabel: '总' } + : { + rowLabel: cloneDeep(keepKeys) + // .slice(0, -1) + .map((_k) => dataObj[colKey][0][_k]) + .join('»'), + }), + _label: colKey || '(空)', key: _rowKey, + SumOrder: _len, - SumPersonNum: _colData[colKey].reduce((r, v) => r + v.personNum, 0), - ConfirmOrder: _colData[colKey].reduce((r, v) => r + (Number(v.orderState) === 1 ? 1 : 0), 0), - transactions: _colData[colKey].reduce((r, v) => r + v.transactions, 0), - SumML: _colData[colKey].reduce((r, v) => r + v.ML, 0), - quotePrice: _colData[colKey].reduce((r, v) => r + v.quotePrice, 0), // todo: quotePrice - tourdays: Math.ceil(_colData[colKey].reduce((r, v) => r + v.tourdays, 0) / _len), - applyDays: Math.ceil(_colData[colKey].reduce((r, v) => r + v.applyDays, 0) / _len), - confirmDays: Math.ceil(_colData[colKey].reduce((r, v) => r + v.confirmDays, 0) / _len), + + SumPersonNum: 0, + ConfirmOrder: 0, + transactions: 0, + SumML: 0, + quotePrice: 0, + tourdays: 0, + applyDays: 0, + confirmDays: 0, }; + + const calculatedData = dataObj[colKey].reduce((r, v) => { + r.SumPersonNum += v.personNum; + r.ConfirmOrder += Number(v.orderState) === 1 ? 1 : 0; + r.transactions += v.transactions; + r.SumML += v.ML; + r.quotePrice += v.quotePrice; + r.tourdays += v.tourdays; + r.applyDays += v.applyDays; + r.confirmDays += v.confirmDays; + return r; + }, initialData); + + // Calculations + calculatedData.tourdays = Math.ceil(calculatedData.tourdays / _len); + calculatedData.applyDays = Math.ceil(calculatedData.applyDays / _len); + calculatedData.confirmDays = Math.ceil(calculatedData.confirmDays / _len); const _rowCalc = { - ConfirmRates: _row.ConfirmOrder ? fixTo4Decimals(_row.ConfirmOrder / _row.SumOrder) : 0, - OrderValue: _row.SumOrder ? fixToInt(_row.SumML / _row.SumOrder) : 0, + ConfirmRates: calculatedData.ConfirmOrder ? fixTo4Decimals(calculatedData.ConfirmOrder / calculatedData.SumOrder) : 0, + OrderValue: calculatedData.SumOrder ? fixToInt(calculatedData.SumML / calculatedData.SumOrder) : 0, + }; + DataGroupByKeys[colKey] = { ...calculatedData, ..._rowCalc }; + }); + + return { groupByKeys: DataGroupByKeys, key: outerKeys.join('_') }; + }; + + const groupData = groupBy(data, (row) => groupbyKeys.map((kk) => `${row[kk]}`).join('=@=')); + const rowsNcolumnsItems = calcTradeFields(groupData, [...rows, ...columns], date); + const pivotResult = Object.values(rowsNcolumnsItems.groupByKeys); + + const transposeData = (keys, dataProp, [dataKey, colKeys]=[]) => + Object.keys(dataProp) + .map((rowKey) => { + const _colKey = dataKey || 'dataKey'; + const _colData = groupBy(dataProp[rowKey], (crow) => (colKeys || keys).map((kk) => `${crow[kk]}`).join('=@=')); + const _columnsObj = calcTradeFields(_colData); + return { ...pick(dataProp[rowKey][0], keys), [_colKey]: _columnsObj.groupByKeys, key: _columnsObj.key }; + }) + .map((everyR) => { + const _colKey = dataKey || 'dataKey'; + const allColumns = Object.values(everyR[_colKey]).reduce((r, c) => r.concat([c]), []); + const summaryCalc = ['ConfirmOrder', 'SumOrder', 'SumML', 'transactions', 'SumPersonNum', 'quotePrice', 'tourdays', 'applyDays', 'confirmDays'].reduce( + (r, skey) => ({ ...r, [skey]: allColumns.reduce((a, c) => a + c[skey], 0) }), + everyR + ); + summaryCalc.tourdays = Math.ceil(summaryCalc.tourdays / allColumns.length); + summaryCalc.applyDays = Math.ceil(summaryCalc.applyDays / allColumns.length); + summaryCalc.confirmDays = Math.ceil(summaryCalc.confirmDays / allColumns.length); + summaryCalc.ConfirmRates = summaryCalc.ConfirmOrder ? fixTo4Decimals(summaryCalc.ConfirmOrder / summaryCalc.SumOrder) : 0; + summaryCalc.OrderValue = summaryCalc.SumOrder ? fixToInt(summaryCalc.SumML / summaryCalc.SumOrder) : 0; + + return { ...everyR, ...summaryCalc }; + }); + + const rowsData = groupBy(data, (row) => rows.map((kk) => `${row[kk]}`).join('=@=')); + const summaryRows = transposeData(rows, rowsData, ['columns', columns]); + + const columnsData = groupBy(data, (row) => columns.map((kk) => `${row[kk]}`).join('=@=')); + const summaryColumns = transposeData(columns, columnsData, ['rows', rows]); + + // console.timeEnd('pivot----'); + return { data: pivotResult, columnValues: [rowsKeys, columnsKeys, dateKeys], summaryRows, summaryColumns }; +}; + +// todo: 优化 pivotBy 速度 +export const pivotBy3 = (data, [rows, columns, date]) => { + console.log('pivotBy', [rows, columns, date]); + console.time('pivot2'); + // const rowKeys = new Set(data.map(row => row[rows[0]])); + const rowKeys = rows.map((keyField) => { + const keyu = new Set(data.map((f) => f[keyField])); + return keyu; + }); + const colKeys = new Set(data.map(row => row[columns[0]])); + const dateKeys = new Set(data.map(row => row[date])); + + const aggregatedData = {}; + + data.forEach(row => { + const rowKey = row[rows[0]] ?? '__total'; + const colKey = row[columns[0]] ?? '__total'; + const dateKey = row[date]; + + if (!aggregatedData[rowKey]) { + aggregatedData[rowKey] = {}; + } + + // if (!aggregatedData[rowKey][colKey]) { + // aggregatedData[rowKey][colKey] = {}; + // } + + if (!aggregatedData[rowKey][colKey]) { + aggregatedData[rowKey][colKey] = { + SumOrder: 0, + // other aggregated fields + SumPersonNum: 0, + ConfirmOrder: 0, + transactions: 0, + SumML: 0, + // ... + quotePrice: 0, + tourdays: 0, + applyDays: 0, + confirmDays: 0, }; - Object.assign(r.columns, {[colKey]: {..._row, ..._rowCalc}}); - // (r.columns || (r.columns = {})).push({[colKey]: {..._row, ..._rowCalc}}); - return {...r, key: _topRowKey.join('_')}; - }, {...pick(rowsData[rowKey][0], rows), columns: {}}); - }).map(everyR => { - const allColumns = Object.values(everyR.columns).reduce((r, c) => r.concat([c]), []); - return ['ConfirmOrder', 'SumOrder', 'SumML', 'transactions', 'SumPersonNum'].reduce( - (r, skey) => ({ ...r, - [skey]: allColumns.reduce((a, c) => a + c[skey], 0), - }), - everyR - ); // todo: 其他数据列计算 + + } + + aggregatedData[rowKey][colKey].SumOrder++; + aggregatedData[rowKey][colKey].SumPersonNum += row.personNum; + aggregatedData[rowKey][colKey].ConfirmOrder += Number(row.orderState === 1); + aggregatedData[rowKey][colKey].transactions += row.transactions; + aggregatedData[rowKey][colKey].SumML += row.ML; + // aggregate other fields + }); - // console.log('pivot res', uniqueKeys, rowsWithColumns, pivotResult); - return { data: pivotResult, columnValues: uniqueKeys, summary: rowsWithColumns }; + const summarizedData = []; + + // Generate summary rows + for (const rowKey of rowKeys) { + + const rowAggregations = { + SumOrder: 0, + // other aggregated fields + SumPersonNum: 0, + ConfirmOrder: 0, + transactions: 0, + SumML: 0, + // ... + quotePrice: 0, + tourdays: 0, + applyDays: 0, + confirmDays: 0, + }; + + // Calculate aggregates over colKey + for (const colKey in aggregatedData[rowKey]) { + rowAggregations.SumOrder += aggregatedData[rowKey][colKey].SumOrder; + rowAggregations.SumPersonNum += aggregatedData[rowKey][colKey].SumPersonNum; + rowAggregations.ConfirmOrder += aggregatedData[rowKey][colKey].ConfirmOrder; + rowAggregations.transactions += aggregatedData[rowKey][colKey].transactions; + rowAggregations.SumML += aggregatedData[rowKey][colKey].SumML; + // ...aggregate all other fields + } + + const row = { + [rows[0]]: rowKey, + ...rowAggregations + }; + + summarizedData.push(row); + } + + // Generate summary columns + for (const colKey of colKeys) { + const colAggregations = { + SumOrder: 0, + // other aggregated fields + SumPersonNum: 0, + ConfirmOrder: 0, + transactions: 0, + SumML: 0, + // ... + quotePrice: 0, + tourdays: 0, + applyDays: 0, + confirmDays: 0, + }; + + // Calculate aggregates over rowKey + for (const rowKey in aggregatedData) { + if (aggregatedData[rowKey][colKey]) { + colAggregations.SumOrder += aggregatedData[rowKey][colKey].SumOrder; + colAggregations.SumPersonNum += aggregatedData[rowKey][colKey].SumPersonNum; + colAggregations.ConfirmOrder += aggregatedData[rowKey][colKey].ConfirmOrder; + colAggregations.transactions += aggregatedData[rowKey][colKey].transactions; + colAggregations.SumML += aggregatedData[rowKey][colKey].SumML; + // ...aggregate all other fields + } + } + + const col = { + [columns[0]]: colKey, + ...colAggregations + }; + + summarizedData.push(col); + } + + console.timeEnd('pivot2'); + console.log('pivot2 ddd', aggregatedData); + return { + data: [], // aggregatedData, + columnValues: [rowKeys, colKeys, dateKeys], + summaryRows: summarizedData.filter(r => r[rows[0]]), + summaryColumns: summarizedData.filter(c => c[columns[0]]) + }; + }; diff --git a/src/stores/DataPivot.js b/src/stores/DataPivot.js new file mode 100644 index 0000000..ece3763 --- /dev/null +++ b/src/stores/DataPivot.js @@ -0,0 +1,49 @@ +import { makeAutoObservable, runInAction, toJS } from 'mobx'; +import { fetchJSON } from '../utils/request'; +import { isEmpty, sortBy, pick, merge, fixTo2Decimals, groupBy, sortKeys, fixToInt, cloneDeep } from '../utils/commons'; +import { dataFieldAlias } from './../libs/ht'; + +class Trade { + constructor(rootStore) { + this.rootStore = rootStore; + makeAutoObservable(this); + } + + /** + * 明细 + */ + getDetailData = async (param, page) => { + this.detailData[page] = { loading: true, dataSource: [], originData: [] }; + const json = await fetchJSON('/service-Analyse2/GetTradeApartDetail', param); + if (json.errcode === 0) { + runInAction(() => { + this.detailData[page].loading = false; + this.detailData[page].dataSource = json.result; + this.detailData[page].originData = json.result; + }); + } + return json.result; + }; + + setSearchValues(body) { + this.searchValues = body; + } + + timeLineKey = 'week'; + setTimeLineKey(v) { + this.timeLineKey = v; + } + + resetData = () => { + this.detailData = { + orders: { loading: false, dataSource: [], originData: [] }, + }; + }; + + searchValues = {}; + detailData = { + orders: { loading: false, dataSource: [], originData: [] }, + }; +} + +export default Trade; diff --git a/src/stores/Index.js b/src/stores/Index.js index 48dfba1..947763c 100644 --- a/src/stores/Index.js +++ b/src/stores/Index.js @@ -14,6 +14,7 @@ import TradeStore from "./Trade"; import KPI from "./KPI"; import DictData from "./DictData"; import Distribution from "./Distribution"; +import DataPivot from './DataPivot'; class Index { constructor() { this.dashboard_store = new DashboardStore(this); @@ -31,6 +32,7 @@ class Index { this.KPIStore = new KPI(this); this.DictDataStore = new DictData(this); this.DistributionStore = new Distribution(this); + this.DataPivotStore = new DataPivot(this); makeAutoObservable(this); } diff --git a/src/views/OrdersPivot.jsx b/src/views/DataPivot.jsx similarity index 78% rename from src/views/OrdersPivot.jsx rename to src/views/DataPivot.jsx index 8fe250e..cba838c 100644 --- a/src/views/OrdersPivot.jsx +++ b/src/views/DataPivot.jsx @@ -2,8 +2,8 @@ import { useContext, useEffect, useState } from 'react'; import { observer } from 'mobx-react'; import { useParams } from 'react-router-dom'; import { stores_Context } from '../config'; -import { Row, Col, Spin, Table, Button, Select, Typography, Card } from 'antd'; -import { cloneDeep, groupBy, isEmpty, omit, sortBy } from '../utils/commons'; +import { Row, Col, Spin, Table, Select, Typography, Card, Button } from 'antd'; +import { cloneDeep, groupBy, isEmpty, omit, pick, sortBy, unique } from '../utils/commons'; import { dataFieldAlias, pivotBy } from '../libs/ht'; import SearchForm from '../components/search/SearchForm'; import { Line } from '@ant-design/plots'; @@ -18,26 +18,27 @@ const filterFields = [ { key: 'guestGroupType', label: '客群类别' }, { key: 'travelMotivation', label: '出行动机' }, { key: 'operatorName', label: '顾问' }, + { key: 'WebCode', label: '来源站点' }, // todo: 目的地, 目的地国家, 页面类型[PPC, NL...] ]; const filterFieldsMapped = filterFields.reduce((r, v) => ({ ...r, [v.key]: v }), {}); +const quickOptions = [ + { label: '国籍×产品', fields: [['country'], ['productType']] }, + { label: '产品×客群', fields: [['productType'], ['guestGroupType']] }, +]; -const combineArrays = (arr, sep = '_') => { - // if (arr.length === 0) return []; - if (arr.length === 1) return arr[0].map((item) => item); - - const output = []; - const head = arr[0]; - const tail = arr.slice(1); - - head.forEach((item) => { - const suffixes = combineArrays(tail, sep); - suffixes.forEach((suffix) => { - output.push(`${item}${sep}${suffix}`); - }); +/** + * 计算笛卡尔积 + */ +const cartesianProductArray = (arr, sep = '_', index = 0, prefix = '') => { + let result = []; + if(index === arr.length){ + return [prefix]; + } + arr[index].forEach(item => { + result = result.concat(cartesianProductArray(arr, sep, index+1, prefix ? `${prefix}${sep}${item}` : `${item}`)); }); - - return output; + return result; }; // 注意TdCell要提到DataTable作用域外声明 @@ -48,23 +49,23 @@ const TdCell = (tdprops) => { }; export default observer((props) => { - const { field } = useParams(); - const { date_picker_store: searchFormStore, orders_store, DistributionStore } = useContext(stores_Context); + const { page } = useParams(); + const { date_picker_store: searchFormStore, orders_store, DistributionStore, DataPivotStore } = useContext(stores_Context); const { formValues, formValuesToSub } = searchFormStore; - // const { curTab, scatterDays, detailData } = DistributionStore; + const { originData } = DataPivotStore.detailData[page]; const [loading, setLoading] = useState(false); - const [rawData, setRawData] = useState([]); + const [rawData, setRawData] = useState(originData || []); const [dataBeforePick, setDataBeforePick] = useState([]); const [dataBeforeXChange, setDataBeforeXChange] = useState([]); const [dataSource, setDataSource] = useState([]); - const [dataSourceMapped, setDataSourceMapped] = useState({}); + // const [dataSourceMapped, setDataSourceMapped] = useState({}); const [pivotTableDataSource, setPivotTableDataSource] = useState([]); + const [pivotTableColumnSummary, setPivotTableColumnSummary] = useState({}); // 列单选, 只有一组结果 - const [pivotColumns, setPivotColumns] = useState([]); const [pivotDateColumns, setPivotDateColumns] = useState([[], []]); - const [pivotDateColumnsValues, setPivotDateColumnsValues] = useState([]); + const [pivotDateColumnsValues, setPivotDateColumnsValues] = useState([[], []]); useEffect(() => { calcDataByDate(); @@ -84,40 +85,50 @@ export default observer((props) => { const detailRefresh = async (obj) => { setLoading(true); - DistributionStore.getDetailData({ + DataPivotStore.getDetailData({ ...(obj || formValuesToSub), - }).then((resData) => { + }, page).then((resData) => { setLoading(false); setRawData(resData); - // const { data, columnValues } = pivotBy(resData, ['country', 'guestGroupType', 'applyDate'], 'personNum'); - // setPivotDateColumns(['applyDate']); calcDataByDate(resData); - // setLineChartX('day'); resetX(); resetItemFilter(); }); }; + const valKey = 'SumOrder'; + const timesKey = 'applyDate'; /** * 走势的数据 * 汇总 */ const calcDataByDate = (_rawData) => { // console.log(';;;;;', pivotDateColumns); - const { data, columnValues, summary } = pivotBy(_rawData || rawData, [].concat(pivotDateColumns, ['applyDate'])); - setPivotDateColumnsValues(cloneDeep(columnValues).slice(0, -1)); - setDataBeforePick(data.sort(sortBy('applyDate'))); - setDataSource(data.sort(sortBy('applyDate'))); - - setPivotTableDataSource(summary.sort(sortBy('SumOrder')).reverse()); + const { data, columnValues, summaryRows, summaryColumns } = pivotBy(_rawData || rawData, [].concat(pivotDateColumns, [timesKey])); + // console.log('data====', data, '\ncolumnValues', columnValues, '\nsummaryRows', summaryRows, '\nsummaryColumns', summaryColumns); + setDataBeforePick(data.sort(sortBy(timesKey))); + // 折线图数据 + setDataSource(data.sort(sortBy(timesKey))); + // 表格数据 + const sortRowData = cloneDeep(summaryRows).sort(sortBy(valKey)).reverse(); + setPivotTableDataSource(sortRowData); + // 列汇总 + const sortColData = summaryColumns.sort(sortBy(valKey)).reverse(); + const colDataMapped = isEmpty(pivotDateColumns[1]) ? sortColData[0] : sortColData.reduce((r, v) => ({...r, [v[pivotDateColumns[1][0]]]: v}), {}); + setPivotTableColumnSummary(colDataMapped); + // 行列的选项值 + const _r = (pivotDateColumns[0].map(eleR => unique(sortRowData.map(ele => ele[eleR])))); + const _c = (pivotDateColumns[1].map(eleC => unique(sortColData.map(ele => ele[eleC])))); + // console.log('_r', _r, '_c', _c); + setPivotDateColumnsValues([_r, _c, columnValues[2]]); }; const line_config = { // data: dataSource, padding: 'auto', - xField: 'applyDate', - yField: 'SumOrder', - seriesField: 'rowKey', + xField: timesKey, + yField: valKey, + seriesField: 'rowLabel', // xAxis: { // type: 'timeCat', // }, @@ -139,11 +150,21 @@ export default observer((props) => { const [lineConfig, setLineConfig] = useState(cloneDeep(line_config)); // 透视配置:行列选项 - const [leftFields, setLeftFields] = useState(filterFields); + // const [leftFields, setLeftFields] = useState(filterFields); const [rightFields, setRightFields] = useState(filterFields); const [rowFields, setRowFields] = useState([]); const [columnFields, setColumnFields] = useState([]); + const [rowSelection, setRowSelection] = useState(); + const [columnSelection, setColumnSelection] = useState(); + const quickOpt = (i) => { + const { fields: pivotFields } = quickOptions[i]; + const [row, col] = pivotFields; + setRowSelection(Object.values(pick(filterFieldsMapped, row))); + setColumnSelection(filterFieldsMapped[col[0]]); + setPivotDateColumns(pivotFields); + }; + const handleRowsPick = (v) => { const pickKeys = v.map((ele) => ele.key); setRowFields(pickKeys); @@ -196,14 +217,13 @@ export default observer((props) => { const dataMappedByRows = groupBy(dataBeforePick, (row) => rowsFilterFields.map((kk) => `${row[kk]}`).join('=@=')); const rowsFilterKey = isEmpty(rowsFilterFields) ? [] - : combineArrays( + : cartesianProductArray( Object.values(currentFilterMerge.rows) .map((kv) => kv.map((kf) => kf.key)) .filter((s) => s.length), '=@=' ); const afterRowsFilter = isEmpty(rowsFilterFields) ? dataBeforePick : rowsFilterKey.reduce((r, _key) => r.concat(dataMappedByRows[_key]), []); - // console.log('afterRowsFilter', rowsFilterFields, afterRowsFilter); const columnsFilterFields = Object.keys(currentFilterMerge.columns).filter((ele) => currentFilterMerge.rows[ele].length); const allFilterValues = [].concat( @@ -214,15 +234,15 @@ export default observer((props) => { const dataMapped = groupBy(afterRowsFilter, (row) => row[columnsName]); const pickData = isEmpty(v) ? afterRowsFilter : Array.isArray(v) ? v.reduce((r, v) => r.concat(dataMapped[v.value]), []) : dataMapped[v.value]; - setDataSource(allFilterValues.length === 0 ? dataBeforePick : pickData.sort(sortBy('applyDate'))); + setDataSource(allFilterValues.length === 0 ? dataBeforePick : pickData.sort(sortBy(timesKey))); resetX(); }; - // 日月年切换 debug: raw data action + // 日月年切换 const [lineChartX, setLineChartX] = useState('day'); const [avgLine1, setAvgLine1] = useState(0); const orderCountDataMapper = { data1: 'data1', data2: undefined }; - const orderCountDataFieldMapper = { 'dateKey': 'applyDate', 'valueKey': 'SumOrder', 'seriesKey': 'rowKey', _f: 'sum' }; + const orderCountDataFieldMapper = { 'dateKey': timesKey, 'valueKey': valKey, 'seriesKey': 'rowLabel', _f: 'sum' }; const resetX = () => { setLineChartX('day'); setAvgLine1(0); @@ -260,16 +280,17 @@ export default observer((props) => { pagination: false, columns: [ ...pivotDateColumns[0].map((ele) => ({ key: ele, title: filterFieldsMapped[ele].label, dataIndex: ele, width: '6em', fixed: 'left' })), - // { key: 'groupsLabel', title: '', dataIndex: 'groupsLabel' }, { key: 'SumOrder', title: '订单数', dataIndex: 'SumOrder', width: '5em' }, ...pivotDateColumns[1].map((ele) => ({ key: ele, title: filterFieldsMapped[ele].label, - align: 'left', - children: cloneDeep(pivotDateColumnsValues) - .slice(-1)[0] - .sort() - .map((col) => ({ key: col, title: col || '(空)', dataIndex: col, width: '6em', render: (_, r) => r.columns[col]?.SumOrder || '' })), + align: 'left', className: 'p-s1', + children: cloneDeep(pivotDateColumnsValues[1][0] || []).map((col) => ({ + key: col, + title: `${col || '(空)'}: ${pivotTableColumnSummary[col]?.[valKey]}`, + dataIndex: ['columns', col, valKey], + width: '6em', + })), })), ], }; @@ -299,7 +320,15 @@ export default observer((props) => { {/* extra={} */} - + ( + // + // ))} + > {/* todo: 拖拽的操作 */} @@ -324,6 +353,7 @@ export default observer((props) => { placeholder={`选择`} onChange={(v) => handleRowsPick(v)} // value={sale_store.salesTrade.pickSales} + // value={rowSelection} maxTagCount={2} maxTagPlaceholder={(omittedValues) => ` + ${omittedValues.length} 更多...`} allowClear={true} @@ -337,12 +367,12 @@ export default observer((props) => { {rowFields.length > 0 - ? cloneDeep(pivotDateColumnsValues) - .slice(0, rowFields.length) + ? cloneDeep(pivotDateColumnsValues)[0] + // .slice(0, rowFields.length) .map((_colArr, _colIndex) => ( - {filterFieldsMapped[pivotDateColumns[0][_colIndex]].label}: + {filterFieldsMapped[pivotDateColumns[0][_colIndex]]?.label}: