From 4fa938ca794c559a5beb1ffed245120b744bf874 Mon Sep 17 00:00:00 2001 From: cloud Date: Mon, 20 Jul 2026 17:20:12 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD:=20=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E5=8A=9F=E7=8E=87=E6=9B=B2=E7=BA=BF=E6=8A=A5=E5=91=8A=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E4=B8=8E=E5=9B=BE=E8=A1=A8=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/controllers/WindPowerController.cpp | 11 +- docs/接口文档.md | 10 + frontend/web_app/package-lock.json | 1003 ++++++++++++++++- frontend/web_app/package.json | 2 + frontend/web_app/src/App.css | 4 +- frontend/web_app/src/pages/HomePage.jsx | 558 ++++++++- 6 files changed, 1531 insertions(+), 57 deletions(-) diff --git a/backend/src/controllers/WindPowerController.cpp b/backend/src/controllers/WindPowerController.cpp index 3d15cb2..fb7fcf5 100644 --- a/backend/src/controllers/WindPowerController.cpp +++ b/backend/src/controllers/WindPowerController.cpp @@ -259,7 +259,7 @@ bool ValidateChartOptions(const json& options, std::string& error) { }; if (!validate_number("scatter_size", 1.0, 8.0) || !validate_number("scatter_opacity", 0.0, 100.0) || - !validate_number("text_size", 10.0, 24.0) || + !validate_number("text_scale", 0.75, 1.5) || !validate_number("actual_marker_size", 2.0, 12.0) || !validate_number("design_marker_size", 2.0, 12.0)) { error = "图表数值参数无效"; @@ -311,6 +311,15 @@ std::optional LoadChartOptions() { if (!options.contains("show_filtered")) { options["show_filtered"] = false; } + // Migrate the earlier single pixel-size setting to the shared text multiplier. + if (!options.contains("text_scale")) { + double scale = 1.0; + if (options.contains("text_size") && options["text_size"].is_number()) { + scale = options["text_size"].get() / 18.0; + } + options["text_scale"] = std::clamp(scale, 0.75, 1.5); + } + options.erase("text_size"); std::string error; if (ValidateChartOptions(options, error)) { return std::optional{options}; diff --git a/docs/接口文档.md b/docs/接口文档.md index 9f017ba..ba00be8 100644 --- a/docs/接口文档.md +++ b/docs/接口文档.md @@ -252,3 +252,13 @@ "data": null } ``` + +### GET /api/wind/chart-options + +读取全局图表样式配置。未保存时返回 `configured: false`;已保存时 `options.text_scale` +为文字倍率,取值范围 `0.75~1.50`,其中 `1.00` 为默认倍率。 + +### POST /api/wind/chart-options + +保存全局图表样式配置。请求体包含当前图表全部样式字段,其中 `text_scale` 为必填数字, +取值范围 `0.75~1.50`。旧配置中的 `text_size` 会在读取时自动换算为倍率。 diff --git a/frontend/web_app/package-lock.json b/frontend/web_app/package-lock.json index 1856194..385f37a 100644 --- a/frontend/web_app/package-lock.json +++ b/frontend/web_app/package-lock.json @@ -8,6 +8,8 @@ "name": "wind_power_web", "version": "0.0.0", "dependencies": { + "exceljs": "^4.4.0", + "html2canvas": "^1.4.1", "react": "^19.2.4", "react-dom": "^19.2.4", "react-router-dom": "^7.13.2", @@ -455,6 +457,47 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmmirror.com/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isboolean": "^3.0.3", + "lodash.isequal": "^4.5.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0" + } + }, + "node_modules/@fast-csv/format/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, + "node_modules/@fast-csv/parse": { + "version": "4.3.6", + "resolved": "https://registry.npmmirror.com/@fast-csv/parse/-/parse-4.3.6.tgz", + "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.groupby": "^4.6.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0", + "lodash.isundefined": "^3.0.1", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/@fast-csv/parse/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.2.tgz", @@ -980,6 +1023,75 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "license": "MIT", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", @@ -987,11 +1099,45 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmmirror.com/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, + "license": "MIT" + }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "license": "MIT" }, "node_modules/baseline-browser-mapping": { @@ -1007,11 +1153,49 @@ "node": ">=6.0.0" } }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmmirror.com/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "license": "MIT", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmmirror.com/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.16", "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.16.tgz", "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -1052,6 +1236,56 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmmirror.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "engines": { + "node": ">=0.2.0" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", @@ -1096,6 +1330,18 @@ "node": ">=0.8" } }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "license": "MIT/X11", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", @@ -1142,11 +1388,25 @@ "dev": true, "license": "MIT" }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "node_modules/convert-source-map": { @@ -1169,6 +1429,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmmirror.com/crc-32/-/crc-32-1.2.2.tgz", @@ -1181,6 +1447,19 @@ "node": ">=0.8" } }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1196,6 +1475,21 @@ "node": ">= 8" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", @@ -1231,6 +1525,45 @@ "node": ">=8" } }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.389", "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz", @@ -1238,6 +1571,15 @@ "dev": true, "license": "ISC" }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", @@ -1445,6 +1787,39 @@ "node": ">=0.10.0" } }, + "node_modules/exceljs": { + "version": "4.4.0", + "resolved": "https://registry.npmmirror.com/exceljs/-/exceljs-4.4.0.tgz", + "integrity": "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==", + "license": "MIT", + "dependencies": { + "archiver": "^5.0.0", + "dayjs": "^1.8.34", + "fast-csv": "^4.3.1", + "jszip": "^3.10.1", + "readable-stream": "^3.6.0", + "saxes": "^5.0.1", + "tmp": "^0.2.0", + "unzipper": "^0.10.11", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/fast-csv": { + "version": "4.3.6", + "resolved": "https://registry.npmmirror.com/fast-csv/-/fast-csv-4.3.6.tgz", + "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", + "license": "MIT", + "dependencies": { + "@fast-csv/format": "4.3.5", + "@fast-csv/parse": "4.3.6" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1544,6 +1919,18 @@ "node": ">=0.8" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", @@ -1559,6 +1946,22 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmmirror.com/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -1569,6 +1972,27 @@ "node": ">=6.9.0" } }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", @@ -1595,6 +2019,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", @@ -1622,6 +2052,39 @@ "hermes-estree": "0.25.1" } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", @@ -1632,6 +2095,12 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz", @@ -1659,6 +2128,23 @@ "node": ">=0.8.19" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1682,6 +2168,12 @@ "node": ">=0.10.0" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", @@ -1766,6 +2258,48 @@ "node": ">=6" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", @@ -1776,6 +2310,48 @@ "json-buffer": "3.0.1" } }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", @@ -1790,6 +2366,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.32.0.tgz", @@ -2051,6 +2636,12 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", + "license": "ISC" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", @@ -2067,6 +2658,73 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "license": "MIT" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmmirror.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "license": "MIT" + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmmirror.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", + "license": "MIT" + }, + "node_modules/lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2074,6 +2732,18 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2088,7 +2758,6 @@ "version": "3.1.5", "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -2097,6 +2766,27 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", @@ -2140,6 +2830,24 @@ "node": ">=18" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", @@ -2190,6 +2898,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", @@ -2213,6 +2927,15 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", @@ -2282,6 +3005,12 @@ "node": ">= 0.8.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", @@ -2351,6 +3080,50 @@ "react-dom": ">=18" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2361,6 +3134,19 @@ "node": ">=4" } }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/rolldown": { "version": "1.1.5", "resolved": "https://registry.npmmirror.com/rolldown/-/rolldown-1.1.5.tgz", @@ -2395,6 +3181,38 @@ "@rolldown/binding-win32-x64-msvc": "1.1.5" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz", @@ -2417,6 +3235,12 @@ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "license": "MIT" }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2462,6 +3286,15 @@ "node": ">=0.8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -2488,6 +3321,31 @@ "node": ">=8" } }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -2505,6 +3363,24 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmmirror.com/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmmirror.com/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", @@ -2526,6 +3402,54 @@ "node": ">= 0.8.0" } }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmmirror.com/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "license": "MIT", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -2573,6 +3497,30 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmmirror.com/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vite": { "version": "8.1.4", "resolved": "https://registry.npmmirror.com/vite/-/vite-8.1.4.tgz", @@ -2695,6 +3643,12 @@ "node": ">=0.10.0" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/xlsx": { "version": "0.18.5", "resolved": "https://registry.npmmirror.com/xlsx/-/xlsx-0.18.5.tgz", @@ -2716,6 +3670,12 @@ "node": ">=0.8" } }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", @@ -2736,6 +3696,41 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "license": "MIT", + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/zod": { "version": "4.4.3", "resolved": "https://registry.npmmirror.com/zod/-/zod-4.4.3.tgz", diff --git a/frontend/web_app/package.json b/frontend/web_app/package.json index 9fd33fe..6fc022e 100644 --- a/frontend/web_app/package.json +++ b/frontend/web_app/package.json @@ -10,6 +10,8 @@ "preview": "vite preview" }, "dependencies": { + "exceljs": "^4.4.0", + "html2canvas": "^1.4.1", "react": "^19.2.4", "react-dom": "^19.2.4", "react-router-dom": "^7.13.2", diff --git a/frontend/web_app/src/App.css b/frontend/web_app/src/App.css index 07d4484..a15ec59 100644 --- a/frontend/web_app/src/App.css +++ b/frontend/web_app/src/App.css @@ -541,7 +541,7 @@ select:focus { .chartFrame { position: relative; - min-height: 540px; + min-height: 580px; } .chartFrameEditing .u-over { @@ -550,7 +550,7 @@ select:focus { .uplotWrap { width: 100%; - min-height: 540px; + min-height: 580px; } .uplot { diff --git a/frontend/web_app/src/pages/HomePage.jsx b/frontend/web_app/src/pages/HomePage.jsx index accf4d0..367b56c 100644 --- a/frontend/web_app/src/pages/HomePage.jsx +++ b/frontend/web_app/src/pages/HomePage.jsx @@ -51,7 +51,7 @@ const DESIGN_FIELD_HINTS = { }; const CHUNK_SIZE = 4000; -const CHART_HEIGHT = 540; +const CHART_HEIGHT = 580; const BRUSH_RADIUS = 10; const EDIT_TOOL_RESTORE = 'restore'; const EDIT_TOOL_ERASE = 'erase'; @@ -90,7 +90,7 @@ const DEFAULT_CHART_OPTIONS = { scatter_color: '#2563eb', scatter_opacity: 42, show_scatter: true, - text_size: 14, + text_scale: 1, text_color: '#334155', actual_color: '#000000', actual_line_style: 'solid', @@ -104,6 +104,37 @@ const DEFAULT_CHART_OPTIONS = { design_marker_size: 5, }; +const TEXT_SCALE_LIMITS = { min: 0.75, max: 1.5 }; +const SCREEN_TYPOGRAPHY = { tick: 14, axis: 20, legend: 18, title: 24 }; +const EXPORT_TYPOGRAPHY = { tick: 22, axis: 28, legend: 24, title: 34 }; + +function getTextScale(value) { + const scale = Number(value); + if (!Number.isFinite(scale)) return 1; + return Math.min(TEXT_SCALE_LIMITS.max, Math.max(TEXT_SCALE_LIMITS.min, scale)); +} + +function normalizeChartOptions(options = {}) { + const { text_size: legacyTextSize, ...currentOptions } = options; + const textScale = Number.isFinite(Number(currentOptions.text_scale)) + ? Number(currentOptions.text_scale) + : Number.isFinite(Number(legacyTextSize)) + ? Number(legacyTextSize) / 18 + : DEFAULT_CHART_OPTIONS.text_scale; + return { + ...DEFAULT_CHART_OPTIONS, + ...currentOptions, + text_scale: getTextScale(textScale), + }; +} + +function getTypography(options, base) { + const scale = getTextScale(options?.text_scale); + return Object.fromEntries( + Object.entries(base).map(([name, size]) => [name, Math.round(size * scale)]), + ); +} + function loadChartState() { try { const state = JSON.parse(sessionStorage.getItem(CHART_STATE_KEY) || 'null'); @@ -265,10 +296,19 @@ function normalizeNumber(value) { function chooseFirstDataSheet(workbook) { for (const sheetName of workbook.SheetNames) { const sheet = workbook.Sheets[sheetName]; - const rows = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: '', raw: true }); - const nonEmptyRows = rows.filter((row) => row.some((cell) => String(cell ?? '').trim() !== '')); - if (nonEmptyRows.length > 1) { - return { sheetName, rows: nonEmptyRows }; + const rawRows = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: '', raw: true }); + const displayRows = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: '', raw: false }); + const nonEmptyIndexes = rawRows + .map((row, index) => ( + row.some((cell) => String(cell ?? '').trim() !== '') ? index : -1 + )) + .filter((index) => index >= 0); + if (nonEmptyIndexes.length > 1) { + return { + sheetName, + rows: nonEmptyIndexes.map((index) => rawRows[index]), + displayRows: nonEmptyIndexes.map((index) => displayRows[index] || []), + }; } } return null; @@ -290,6 +330,10 @@ async function readExcelFile(file) { sheet_name: sheetData.sheetName, headers, rows: dataRows, + raw_headers: (sheetData.displayRows[0] || sheetData.rows[0]) + .map((cell) => String(cell ?? '')), + // 报告中的原始页需要保留数值类型,Excel 的 COUNTIFS/AVERAGEIFS 才能参与计算。 + raw_rows: dataRows.map((row) => [...row]), row_count: dataRows.length, }; } @@ -370,6 +414,53 @@ function buildStandardRows(files, mapping) { return { rows, indexes }; } +function hasMatchingHeaderSequence(files) { + if (!files.length) return false; + const firstHeaders = files[0].headers || []; + return files.every((file) => ( + file.headers.length === firstHeaders.length && + file.headers.every((header, index) => header === firstHeaders[index]) + )); +} + +function findRawSourceFiles(files, mapping, fanId) { + if (!fanId) return []; + return files.filter((file) => { + const fanColumnIndex = file.headers.indexOf(mapping.fan_id); + return fanColumnIndex >= 0 && file.rows.some((row) => ( + String(row[fanColumnIndex] ?? '').trim() === fanId + )); + }); +} + +function buildRawReportData(file, mapping, fanId) { + if (!file || !fanId) return null; + + const fanColumnIndex = file.headers.indexOf(mapping.fan_id); + const windSpeedColumnIndex = file.headers.indexOf(mapping.wind_speed); + if (fanColumnIndex < 0 || windSpeedColumnIndex < 0) return null; + + const rows = file.rows.reduce((result, row, index) => { + if (String(row[fanColumnIndex] ?? '').trim() === fanId) { + const rawRow = [...(file.raw_rows?.[index] || row)]; + const windSpeed = normalizeNumber(rawRow[windSpeedColumnIndex]); + // 部分源 Excel 将风速保存为“数字文本”。保留显示值的同时写成数值单元格, + // 使计算表的 COUNTIFS 能正确统计风频。 + if (Number.isFinite(windSpeed)) { + rawRow[windSpeedColumnIndex] = windSpeed; + } + result.push(rawRow); + } + return result; + }, []); + + return { + headers: [...(file.raw_headers || file.headers)], + rows, + windSpeedColumnIndex, + }; +} + function formatTimestampForFile(date = new Date()) { return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}` + `${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`; @@ -438,6 +529,50 @@ function interpolateDesignPower(points, windSpeed) { return points[points.length - 1].design_power; } +function buildReportDesignCurve(designCurve) { + const points = (designCurve || []) + .filter((point) => Number.isFinite(point?.wind_speed) + && Number.isFinite(point?.design_power) + && point.wind_speed >= 0 + && point.design_power >= 0) + .sort((left, right) => left.wind_speed - right.wind_speed); + if (!points.length || points[0].wind_speed <= 0) return points; + + const firstGap = points.length > 1 ? points[1].wind_speed - points[0].wind_speed : 1; + const step = Number.isFinite(firstGap) && firstGap > 0 ? firstGap : 1; + const lowWindPoints = []; + for (let windSpeed = 0; windSpeed < points[0].wind_speed - 0.000001; windSpeed += step) { + lowWindPoints.push({ + wind_speed: Number(windSpeed.toFixed(6)), + design_power: 0, + }); + } + return [...lowWindPoints, ...points]; +} + +function buildReportCurveRows(scatterPoints, reportDesignCurve) { + return reportDesignCurve.map((designPoint) => { + const powers = (scatterPoints || []) + .filter((point) => { + const windSpeed = Number(point?.wind_speed); + const activePower = Number(point?.active_power); + return Number.isFinite(windSpeed) && Number.isFinite(activePower) + && windSpeed >= designPoint.wind_speed - 0.5 + && windSpeed < designPoint.wind_speed + 0.5; + }) + .map((point) => Number(point.active_power)); + const averagePower = powers.length + ? powers.reduce((sum, power) => sum + power, 0) / powers.length + : null; + return { + wind_speed: designPoint.wind_speed, + design_power: designPoint.design_power, + sample_count: powers.length, + average_power: averagePower, + }; + }).filter((point) => Number.isFinite(point.average_power) && point.average_power > 0); +} + function schemeParamsToState(parameters) { return { grid_connected_speed: String( @@ -667,10 +802,16 @@ function getVisibleActualCurve(actualCurve) { return sorted.slice(validIndexes[0], validIndexes[validIndexes.length - 1] + 1); } -function exportPowerCurvePng({ actualCurve, designCurve, scatterPoints, filteredPoints, showActual, showDesign, showScatter, showFiltered, options, scales }) { +function renderPowerCurvePng({ actualCurve, designCurve, scatterPoints, filteredPoints, showActual, showDesign, showScatter, showFiltered, options, scales }) { const width = 1800; const height = 1160; - const padding = { top: 120, right: 70, bottom: 115, left: 135 }; + const typography = getTypography(options, EXPORT_TYPOGRAPHY); + const padding = { + top: Math.max(120, typography.title * 2 + 40), + right: 70, + bottom: Math.max(135, typography.axis + typography.tick + 82), + left: Math.max(160, typography.axis + typography.tick * 4 + 48), + }; const gridColor = '#d8e0ea'; const plotWidth = width - padding.left - padding.right; const plotHeight = height - padding.top - padding.bottom; @@ -699,10 +840,8 @@ function exportPowerCurvePng({ actualCurve, designCurve, scatterPoints, filtered context.fillStyle = '#ffffff'; context.fillRect(0, 0, width, height); - const textSize = Math.min(24, Math.max(10, Number(options.text_size) || DEFAULT_CHART_OPTIONS.text_size)); const textColor = options.text_color || DEFAULT_CHART_OPTIONS.text_color; - const textScale = textSize / DEFAULT_CHART_OPTIONS.text_size; - context.font = `${Math.round(16 * textScale)}px "Segoe UI", sans-serif`; + context.font = `${typography.tick}px "Segoe UI", sans-serif`; if (options.show_grid) { context.strokeStyle = gridColor; @@ -729,6 +868,7 @@ function exportPowerCurvePng({ actualCurve, designCurve, scatterPoints, filtered context.fillText(formatTick(yValue), padding.left - 14, toY(yValue) + 5); } context.textAlign = 'center'; + context.font = `${typography.axis}px "Segoe UI", sans-serif`; context.fillText(options.x_axis_label || DEFAULT_CHART_OPTIONS.x_axis_label, padding.left + plotWidth / 2, height - 28); context.save(); context.translate(34, padding.top + plotHeight / 2); @@ -753,12 +893,12 @@ function exportPowerCurvePng({ actualCurve, designCurve, scatterPoints, filtered if (showActual) drawSmoothLine(context, actualCurve, toX, toY, actualStyle); context.save(); context.fillStyle = textColor; - context.font = `700 ${Math.round(34 * textScale)}px "Segoe UI", sans-serif`; + context.font = `700 ${typography.title}px "Segoe UI", sans-serif`; context.textAlign = 'center'; context.fillText( options.title || DEFAULT_CHART_OPTIONS.title, padding.left + plotWidth / 2, - padding.top - Math.round(34 * textScale), + padding.top - typography.title, ); context.restore(); @@ -770,9 +910,10 @@ function exportPowerCurvePng({ actualCurve, designCurve, scatterPoints, filtered ...(showFiltered ? [['滤除点', '#94a3b8']] : []), ]; context.textAlign = 'left'; + context.font = `${typography.legend}px "Segoe UI", sans-serif`; const legendLeft = padding.left + 22; - const legendTop = padding.top + Math.max(30, Math.round(34 * textScale)); - const legendLineHeight = Math.max(30, Math.round(28 * textScale)); + const legendTop = padding.top + Math.max(30, typography.legend + 10); + const legendLineHeight = Math.max(34, typography.legend + 12); entries.forEach(([label, type], index) => { const top = legendTop + index * legendLineHeight; const isScatter = type === 'scatter'; @@ -793,13 +934,229 @@ function exportPowerCurvePng({ actualCurve, designCurve, scatterPoints, filtered context.fillText(label, legendLeft + 30, top + 7); }); } + return canvas.toDataURL('image/png'); +} + +async function exportPowerCurvePng(args) { + const imageData = args.imageData || renderPowerCurvePng(args); + if (!imageData) return false; const link = document.createElement('a'); - link.href = canvas.toDataURL('image/png'); - link.download = `${String(options.title || '风速-功率曲线').replace(/[\\/:*?"<>|]/g, '_')}.png`; + link.href = imageData; + link.download = `${String(args.options.title || '风速-功率曲线').replace(/[\\/:*?"<>|]/g, '_')}.png`; link.click(); return true; } +async function captureChartFrame(frame) { + if (!frame) return null; + const { default: html2canvas } = await import('html2canvas'); + const frameRect = frame.getBoundingClientRect(); + const plotCanvas = frame.querySelector('.uplot canvas'); + const plotRect = plotCanvas?.getBoundingClientRect(); + const captureHeight = plotRect + ? Math.max(1, Math.ceil(plotRect.bottom - frameRect.top)) + : undefined; + const canvas = await html2canvas(frame, { + backgroundColor: '#ffffff', + height: captureHeight, + logging: false, + onclone: (documentClone) => { + documentClone.querySelectorAll('.uplot .u-legend').forEach((legend) => legend.remove()); + }, + scale: 2, + }); + return { + imageData: canvas.toDataURL('image/png'), + imageWidth: canvas.width, + imageHeight: canvas.height, + }; +} + +const REPORT_DETAIL_HEADERS = [ + '风机编号', + '采样时间', + '平均功率', + '平均转速', + '平均风速', + '3个叶片变桨角平均值', +]; + +const REPORT_BORDER = { + top: { style: 'thin', color: { argb: 'FF475569' } }, + left: { style: 'thin', color: { argb: 'FF475569' } }, + bottom: { style: 'thin', color: { argb: 'FF475569' } }, + right: { style: 'thin', color: { argb: 'FF475569' } }, +}; + +function reportNumber(value) { + if (value === null || value === undefined || String(value).trim() === '') return ''; + const number = Number(value); + return Number.isFinite(number) ? number : ''; +} + +function reportPitchAverage(row) { + const values = [row.blade_pitch_1, row.blade_pitch_2, row.blade_pitch_3] + .map(reportNumber); + return values.every((value) => value !== '') ? values.reduce((sum, value) => sum + value, 0) / values.length : ''; +} + +function reportDetailValues(row, fanId) { + const savedPitchAverage = reportNumber(row.pitch_angle_average); + return [ + row.fan_id || fanId || '', + row.time || '', + reportNumber(row.active_power), + reportNumber(row.generator_speed), + reportNumber(row.wind_speed), + savedPitchAverage !== '' ? savedPitchAverage : reportPitchAverage(row), + ]; +} + +function styleReportHeader(row) { + row.height = 23; + row.eachCell((cell) => { + cell.font = { bold: true, color: { argb: 'FF0F172A' } }; + cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE2E8F0' } }; + cell.alignment = { horizontal: 'center', vertical: 'middle' }; + cell.border = REPORT_BORDER; + }); +} + +function styleReportBody(sheet, startRow, endRow, numericColumns) { + for (let rowIndex = startRow; rowIndex <= endRow; rowIndex += 1) { + const row = sheet.getRow(rowIndex); + row.eachCell({ includeEmpty: true }, (cell, columnNumber) => { + cell.border = REPORT_BORDER; + cell.alignment = { vertical: 'middle' }; + if (numericColumns.includes(columnNumber)) { + cell.numFmt = '0.0000'; + } + }); + } +} + +function addReportDetailSheet(workbook, name, rows, fanId) { + const sheet = workbook.addWorksheet(name, { + views: [{ state: 'frozen', ySplit: 1 }], + }); + sheet.columns = [ + { width: 16 }, { width: 22 }, { width: 14 }, { width: 14 }, { width: 14 }, { width: 23 }, + ]; + sheet.addRow(REPORT_DETAIL_HEADERS); + rows.forEach((row) => sheet.addRow(reportDetailValues(row, fanId))); + styleReportHeader(sheet.getRow(1)); + styleReportBody(sheet, 2, Math.max(2, rows.length + 1), [3, 4, 5, 6]); + sheet.autoFilter = `A1:F${Math.max(1, rows.length + 1)}`; + return sheet; +} + +function excelColumnName(index) { + let value = index + 1; + let name = ''; + while (value > 0) { + const remainder = (value - 1) % 26; + name = String.fromCharCode(65 + remainder) + name; + value = Math.floor((value - 1) / 26); + } + return name; +} + +function addRawReportSheet(workbook, { headers, rows }) { + const sheet = workbook.addWorksheet('筛选前的数据', { + views: [{ state: 'frozen', ySplit: 1 }], + }); + sheet.addRow(headers); + rows.forEach((row) => sheet.addRow(row)); + sheet.columns = headers.map((header, index) => { + const maxLength = rows.reduce((max, row) => ( + Math.max(max, String(row[index] ?? '').length) + ), String(header ?? '').length); + return { width: Math.min(32, Math.max(12, maxLength + 2)) }; + }); + styleReportHeader(sheet.getRow(1)); + styleReportBody(sheet, 2, Math.max(2, rows.length + 1), []); + sheet.autoFilter = `A1:${excelColumnName(Math.max(0, headers.length - 1))}${Math.max(1, rows.length + 1)}`; + return sheet; +} + +async function exportPowerCurveReport({ + fanId, + rawReportData, + effectiveRows, + reportRows, + chartRenderSnapshot, +}) { + const { default: ExcelJS } = await import('exceljs'); + const workbook = new ExcelJS.Workbook(); + workbook.creator = '风电功率计算平台'; + workbook.created = new Date(); + workbook.calcProperties.fullCalcOnLoad = true; + + addReportDetailSheet(workbook, '筛选后的数据', effectiveRows, fanId); + addRawReportSheet(workbook, rawReportData); + const rawWindColumn = excelColumnName(rawReportData.windSpeedColumnIndex); + + const curveSheet = workbook.addWorksheet('功率曲线计算表', { + views: [{ state: 'frozen', ySplit: 1 }], + }); + curveSheet.columns = [ + { width: 9 }, { width: 14 }, { width: 16 }, { width: 17 }, { width: 17 }, { width: 19 }, { width: 19 }, { width: 4 }, { width: 16 }, + ]; + curveSheet.addRow([ + '序号', '风速 (m/s)', '风频时间 (h)', '计算功率 (kW)', '保证功率 (kW)', '计算发电量 (kWh)', '理论发电量 (kWh)', '', 'K值', + ]); + styleReportHeader(curveSheet.getRow(1)); + + reportRows.forEach((point, index) => { + const rowNumber = index + 2; + const row = curveSheet.getRow(rowNumber); + row.getCell(1).value = index + 1; + row.getCell(2).value = reportNumber(point.wind_speed); + row.getCell(3).value = { + formula: `(COUNTIFS('筛选前的数据'!${rawWindColumn}:${rawWindColumn},">="&B${rowNumber}-0.5,'筛选前的数据'!${rawWindColumn}:${rawWindColumn},"<"&B${rowNumber}+0.5)/COUNT('筛选前的数据'!${rawWindColumn}:${rawWindColumn}))*8760`, + }; + row.getCell(4).value = { + formula: `IFERROR(AVERAGEIFS('筛选后的数据'!$C:$C,'筛选后的数据'!$E:$E,">="&B${rowNumber}-0.5,'筛选后的数据'!$E:$E,"<"&B${rowNumber}+0.5),0)`, + }; + row.getCell(5).value = reportNumber(point.design_power); + row.getCell(6).value = { formula: `IFERROR(ROUND(C${rowNumber}*D${rowNumber}/1000,4),0)` }; + row.getCell(7).value = { formula: `ROUND(C${rowNumber}*E${rowNumber}/1000,4)` }; + }); + + const lastRow = reportRows.length + 1; + curveSheet.getCell('I2').value = { + formula: `IFERROR(SUM(F2:F${lastRow})/SUM(G2:G${lastRow}),0)`, + }; + styleReportBody(curveSheet, 2, lastRow, [2, 3, 4, 5, 6, 7, 9]); + curveSheet.getCell('I2').numFmt = '0.0000'; + curveSheet.autoFilter = `A1:G${lastRow}`; + + const reportImage = chartRenderSnapshot?.imageData + || (chartRenderSnapshot ? renderPowerCurvePng(chartRenderSnapshot) : false); + if (reportImage) { + const imageId = workbook.addImage({ base64: reportImage, extension: 'png' }); + const imageWidth = 820; + const imageHeight = chartRenderSnapshot?.imageWidth && chartRenderSnapshot?.imageHeight + ? Math.round(imageWidth * chartRenderSnapshot.imageHeight / chartRenderSnapshot.imageWidth) + : 529; + curveSheet.addImage(imageId, { + tl: { col: 8, row: 3 }, + ext: { width: imageWidth, height: imageHeight }, + }); + } + + const buffer = await workbook.xlsx.writeBuffer(); + const blob = new Blob([buffer], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `完整功率曲线报告_${sanitizeFileName(fanId)}_${formatTimestampForFile()}.xlsx`; + link.click(); + URL.revokeObjectURL(url); +} + function PowerCurveChart({ points, scatterPoints, @@ -809,6 +1166,7 @@ function PowerCurveChart({ designPoints, chartOptions, chartExportRef, + chartSnapshotRef, editMode, editTool, onRestorePoints, @@ -849,23 +1207,39 @@ function PowerCurveChart({ [validDesign, visibleActualCurve], ); const activeChartOptions = useMemo( - () => ({ ...DEFAULT_CHART_OPTIONS, ...chartOptions }), + () => normalizeChartOptions(chartOptions), [chartOptions], ); + const screenTypography = useMemo( + () => getTypography(activeChartOptions, SCREEN_TYPOGRAPHY), + [activeChartOptions], + ); + const xAxisSize = screenTypography.tick + 22; + const yAxisSize = Math.max(64, Math.round(screenTypography.tick * 3.2 + 16)); + const axisLabelSize = screenTypography.axis + 18; const showScatterOnChart = activeChartOptions.show_scatter || editMode; - - chartExportRef.current = () => exportPowerCurvePng({ - actualCurve: visibleActualCurve, - designCurve: visibleDesignCurve, - scatterPoints: validScatter, - filteredPoints: validFiltered, - showActual: plotRef.current?.series[1]?.show !== false, - showDesign: plotRef.current?.series[2]?.show !== false, - showScatter: activeChartOptions.show_scatter, - showFiltered, - options: activeChartOptions, - scales: plotRef.current?.scales, - }); + chartSnapshotRef.current = async () => { + const snapshot = { + actualCurve: visibleActualCurve, + designCurve: visibleDesignCurve, + scatterPoints: validScatter, + filteredPoints: validFiltered, + showActual: plotRef.current?.series[1]?.show !== false, + showDesign: plotRef.current?.series[2]?.show !== false, + showScatter: showScatterOnChart, + showFiltered, + options: activeChartOptions, + scales: plotRef.current?.scales, + }; + try { + const capturedImage = await captureChartFrame(frameRef.current); + return capturedImage ? { ...snapshot, ...capturedImage } : snapshot; + } catch { + // Keep the existing Canvas renderer as a fallback when browser capture is unavailable. + return snapshot; + } + }; + chartExportRef.current = async () => exportPowerCurvePng(await chartSnapshotRef.current()); useEffect(() => { if (!chartRef.current || (!visibleActualCurve.length && !visibleDesignCurve.length)) return undefined; @@ -944,8 +1318,12 @@ function PowerCurveChart({ { label: activeChartOptions.x_axis_label, stroke: activeChartOptions.text_color, - font: `${activeChartOptions.text_size}px "Segoe UI", sans-serif`, - labelFont: `600 ${Math.round(activeChartOptions.text_size * 1.08)}px "Segoe UI", sans-serif`, + size: xAxisSize, + gap: 8, + labelSize: axisLabelSize, + labelGap: 10, + font: `${screenTypography.tick}px "Segoe UI", sans-serif`, + labelFont: `600 ${screenTypography.axis}px "Segoe UI", sans-serif`, splits: (u, axisIdx, min, max) => getAxisTicks(min, max, activeChartOptions.x_tick_interval), values: (u, splits) => splits.map(formatTick), grid: { show: activeChartOptions.show_grid, stroke: '#d8e0ea', width: 1 }, @@ -953,8 +1331,12 @@ function PowerCurveChart({ { label: activeChartOptions.y_axis_label, stroke: activeChartOptions.text_color, - font: `${activeChartOptions.text_size}px "Segoe UI", sans-serif`, - labelFont: `600 ${Math.round(activeChartOptions.text_size * 1.08)}px "Segoe UI", sans-serif`, + size: yAxisSize, + gap: 8, + labelSize: axisLabelSize, + labelGap: 10, + font: `${screenTypography.tick}px "Segoe UI", sans-serif`, + labelFont: `600 ${screenTypography.axis}px "Segoe UI", sans-serif`, splits: (u, axisIdx, min, max) => getAxisTicks(min, max, activeChartOptions.y_tick_interval), values: (u, splits) => splits.map(formatTick), grid: { show: activeChartOptions.show_grid, stroke: '#d8e0ea', width: 1 }, @@ -1049,7 +1431,7 @@ function PowerCurveChart({ const titleElement = chart.root.querySelector('.u-title'); if (titleElement) { titleElement.style.color = activeChartOptions.text_color; - titleElement.style.fontSize = `${Math.round(activeChartOptions.text_size * 1.25)}px`; + titleElement.style.fontSize = `${screenTypography.title}px`; titleElement.style.padding = '8px 0 10px'; } plotRef.current = chart; @@ -1150,6 +1532,7 @@ function PowerCurveChart({ onRestorePoints, onErasePoints, activeChartOptions, + screenTypography, ]); useEffect(() => { @@ -1174,8 +1557,8 @@ function PowerCurveChart({ className="chartLegend" style={{ color: activeChartOptions.text_color, - fontSize: `${activeChartOptions.text_size}px`, - top: `${Math.max(72, Math.round(activeChartOptions.text_size * 5.5))}px`, + fontSize: `${screenTypography.legend}px`, + top: `${Math.max(72, screenTypography.title * 3)}px`, }} > {showScatterOnChart && ( @@ -1228,8 +1611,7 @@ export default function HomePage() { const [readingDesign, setReadingDesign] = useState(false); const [designRows, setDesignRows] = useState(() => createDesignRows(savedSessionState.design_rows)); const [designMeta, setDesignMeta] = useState(null); - const [chartOptions, setChartOptions] = useState(() => ({ - ...DEFAULT_CHART_OPTIONS, + const [chartOptions, setChartOptions] = useState(() => normalizeChartOptions({ ...(savedSessionState.chart_options || {}), // Apply the new black default only to sessions carrying the old built-in orange. actual_color: savedSessionState.chart_options?.actual_color === '#ea580c' @@ -1243,7 +1625,9 @@ export default function HomePage() { const [chartSettingsOpen, setChartSettingsOpen] = useState(false); const [savingChartOptions, setSavingChartOptions] = useState(false); const [chartSettingsMessage, setChartSettingsMessage] = useState(''); + const [exportingReport, setExportingReport] = useState(false); const chartExportRef = useRef(() => false); + const chartSnapshotRef = useRef(() => null); const [schemes, setSchemes] = useState(DEFAULT_SCHEMES); const [selectedSchemeId, setSelectedSchemeId] = useState(DEFAULT_SCHEME_ID); const [schemeDescription, setSchemeDescription] = useState(DEFAULT_SCHEMES[0].description); @@ -1314,6 +1698,18 @@ export default function HomePage() { [effectiveBaseScatter, restoredFilteredPoints], ); const confirmedCurveScatter = confirmedCurveScatterByFan[selectedFan] || effectiveScatter; + const hasConsistentRawHeaders = useMemo( + () => hasMatchingHeaderSequence(files), + [files], + ); + const rawReportSourceFiles = useMemo( + () => findRawSourceFiles(files, mapping, selectedFan), + [files, mapping, selectedFan], + ); + const selectedRawReportData = useMemo(() => { + if (!hasConsistentRawHeaders || rawReportSourceFiles.length !== 1) return null; + return buildRawReportData(rawReportSourceFiles[0], mapping, selectedFan); + }, [hasConsistentRawHeaders, mapping, rawReportSourceFiles, selectedFan]); const selectedEstimatedParams = selectedFan && result?.estimated_params ? result.estimated_params[selectedFan] : null; @@ -1349,7 +1745,7 @@ export default function HomePage() { try { const data = await getWindChartOptions(); if (!canceled && data.configured && data.options) { - setChartOptions({ ...DEFAULT_CHART_OPTIONS, ...data.options }); + setChartOptions(normalizeChartOptions(data.options)); setShowFiltered(Boolean(data.options.show_filtered)); } } catch (err) { @@ -1484,7 +1880,7 @@ export default function HomePage() { setChartSettingsMessage(''); try { const data = await saveWindChartOptions({ ...chartOptions, show_filtered: showFiltered }); - setChartOptions({ ...DEFAULT_CHART_OPTIONS, ...data.options }); + setChartOptions(normalizeChartOptions(data.options)); setShowFiltered(Boolean(data.options.show_filtered)); setChartSettingsMessage('图表参数已保存,将作为全局默认配置。'); } catch (err) { @@ -1520,6 +1916,55 @@ export default function HomePage() { })) .sort((left, right) => left.wind_speed - right.wind_speed); }, [designRows]); + const reportDesignCurve = useMemo( + () => buildReportDesignCurve(designCurve), + [designCurve], + ); + const reportCurveRows = useMemo( + () => buildReportCurveRows(effectiveScatter, reportDesignCurve), + [effectiveScatter, reportDesignCurve], + ); + const reportExportDisabledReason = files.length && !hasConsistentRawHeaders + ? '同一批上传文件的列头和列顺序必须完全一致' + : files.length && rawReportSourceFiles.length !== 1 + ? '完整报告要求当前风机对应唯一一份上传 Excel 文件' + : files.length && (!selectedRawReportData || !selectedRawReportData.rows.length) + ? '当前风机没有可导出的原始数据,或未识别到风速列' + : ''; + const canExportFullReport = Boolean( + selectedFan && selectedRawReportData?.rows.length && reportCurveRows.length && !isEditMode && + !reportExportDisabledReason, + ); + const handleExportFullReport = useCallback(async () => { + if (!canExportFullReport || exportingReport) return; + setExportingReport(true); + setError(''); + try { + const chartRenderSnapshot = await chartSnapshotRef.current?.(); + if (!chartRenderSnapshot) { + throw new Error('图表尚未完成绘制,请稍后重试'); + } + await exportPowerCurveReport({ + fanId: selectedFan, + rawReportData: selectedRawReportData, + effectiveRows: effectiveScatter, + reportRows: reportCurveRows, + chartRenderSnapshot, + }); + } catch (err) { + setError(err.message || '导出完整报告失败'); + } finally { + setExportingReport(false); + } + }, [ + canExportFullReport, + effectiveScatter, + exportingReport, + reportCurveRows, + rawReportSourceFiles, + selectedRawReportData, + selectedFan, + ]); useEffect(() => { let canceled = false; @@ -1629,6 +2074,9 @@ export default function HomePage() { for (const file of selectedFiles) { parsedFiles.push(await readExcelFile(file)); } + if (!hasMatchingHeaderSequence(parsedFiles)) { + throw new Error('同一批上传文件的列头和列顺序必须完全一致'); + } setFiles(parsedFiles); setMapping(inferMapping(parsedFiles[0].headers)); } catch (err) { @@ -2081,7 +2529,7 @@ export default function HomePage() {
-

+

{chartOptions.title || DEFAULT_CHART_OPTIONS.title}

@@ -2142,6 +2590,15 @@ export default function HomePage() { > 导出有效数据 +