From 27733bd91ec5c2b9966185c7125bb2e264bd2f2e Mon Sep 17 00:00:00 2001 From: RUI <298977887@qq.com> Date: Tue, 10 Jun 2025 16:14:49 +0800 Subject: [PATCH] 0610.1 --- package.json | 9 + pnpm-lock.yaml | 1171 ++++++++++++++++ public/md2.md | 414 ++++++ public/test2.md | 59 + src/components/markdown/index.tsx | 33 + src/components/markdown/styles.ts | 161 +++ src/components/markdown2/index.tsx | 30 + src/components/markdown2/styles.ts | 161 +++ src/{utils/ConnectDB.ts => lib/connectDB.ts} | 0 src/models/index.ts | 1 + src/models/types.ts | 1 + src/pages/api/admin/cleanup-indexes.ts | 2 +- src/pages/api/backstage/accounts/[id].ts | 2 +- .../backstage/accounts/accountgrowth/clear.ts | 102 ++ .../backstage/accounts/accountgrowth/index.ts | 2 +- .../accounts/accountgrowth/register copy.ts | 56 - .../accounts/accountgrowth/register.ts | 26 +- src/pages/api/backstage/accounts/index.ts | 2 +- src/pages/api/backstage/balance/[id].ts | 2 +- src/pages/api/backstage/brands/[id].ts | 2 +- src/pages/api/backstage/brands/index.ts | 2 +- src/pages/api/backstage/categories/[id].ts | 2 +- src/pages/api/backstage/categories/index.ts | 2 +- src/pages/api/backstage/coupons/[id].ts | 2 +- .../api/backstage/coupons/assign/[id].ts | 2 +- .../api/backstage/coupons/assign/index.ts | 2 +- src/pages/api/backstage/coupons/index.ts | 2 +- src/pages/api/backstage/customers/[id].ts | 2 +- src/pages/api/backstage/customers/index.ts | 2 +- .../api/backstage/customers/sales/[id].ts | 2 +- src/pages/api/backstage/mine/info/[id].ts | 2 +- src/pages/api/backstage/mine/sales/index.ts | 2 +- .../api/backstage/payment-platforms/[id].ts | 2 +- .../api/backstage/payment-platforms/index.ts | 2 +- src/pages/api/backstage/products/[id].ts | 2 +- src/pages/api/backstage/products/index.ts | 2 +- src/pages/api/backstage/sales/Records/[id].ts | 2 +- .../api/backstage/sales/Records/index.ts | 2 +- src/pages/api/backstage/sales/[id].ts | 2 +- .../api/backstage/sales/aftersale/index.ts | 2 +- .../backstage/sales/aftersale/records/[id].ts | 2 +- .../sales/aftersale/records/index.ts | 2 +- .../sales/aftersale/records/updateProgress.ts | 2 +- .../sales/aftersale/uploadPaymentCode.ts | 2 +- src/pages/api/backstage/sales/index.ts | 2 +- src/pages/api/backstage/sales/updateStatus.ts | 2 +- src/pages/api/backstage/suppliers/[id].ts | 2 +- src/pages/api/backstage/suppliers/index.ts | 2 +- src/pages/api/backstage/transactions/[id].ts | 2 +- src/pages/api/backstage/transactions/index.ts | 2 +- src/pages/api/backstage/users/[id].ts | 2 +- src/pages/api/backstage/users/index.ts | 2 +- src/pages/api/login.ts | 2 +- src/pages/api/logistics/batchStatus.ts | 2 +- src/pages/api/logistics/details.ts | 2 +- src/pages/api/logistics/status.ts | 2 +- src/pages/api/paymentCodeImage/[id].ts | 2 +- src/pages/api/products/batchImages.ts | 2 +- src/pages/api/products/images/[id].ts | 2 +- src/pages/api/roles/[id].ts | 2 +- src/pages/api/roles/index.ts | 2 +- src/pages/api/script-categories/[id].ts | 2 +- src/pages/api/script-categories/index.ts | 2 +- src/pages/api/scripts/[id].ts | 2 +- src/pages/api/scripts/index.ts | 2 +- src/pages/api/team/create.ts | 2 +- src/pages/api/team/index.ts | 2 +- src/pages/api/team/members.ts | 2 +- src/pages/api/test/batch-query-logistics.ts | 2 +- src/pages/api/test/logistic-detail.ts | 2 +- src/pages/api/test/logistics-records.ts | 2 +- src/pages/api/todos/[id].ts | 2 +- src/pages/api/todos/index.ts | 2 +- .../tools/SFExpress/updateLogisticsDetails.ts | 2 +- src/pages/api/tools/logistics/detail/[id].ts | 2 +- src/pages/api/tools/logistics/index.ts | 2 +- src/pages/api/user.ts | 2 +- src/pages/api/users/[id].ts | 2 +- src/pages/api/users/index.ts | 2 +- .../backstage/accounts/account-modal.tsx | 39 +- src/pages/backstage/accounts/index.tsx | 27 +- src/pages/team/AfterSaleRecord/index.tsx | 16 +- .../components/CustomerAnalysisModal.tsx | 766 +++++++++++ .../components/SalesAnalysisModal.tsx | 1077 +++++++++++++++ src/pages/team/SaleRecord/index.tsx | 282 ++-- src/pages/team/SaleRecord/test.tsx | 909 +++++++++++++ src/pages/team/account/index.tsx | 1191 +++++++++++++++++ src/pages/test/markdown/22.tsx | 32 + src/pages/test/markdown/index.tsx | 31 + src/pages/test/userstore-debug.tsx | 257 ++++ src/store/userStore.ts | 6 +- src/utils/highlight.ts | 17 + 92 files changed, 6731 insertions(+), 273 deletions(-) create mode 100644 public/md2.md create mode 100644 public/test2.md create mode 100644 src/components/markdown/index.tsx create mode 100644 src/components/markdown/styles.ts create mode 100644 src/components/markdown2/index.tsx create mode 100644 src/components/markdown2/styles.ts rename src/{utils/ConnectDB.ts => lib/connectDB.ts} (100%) create mode 100644 src/pages/api/backstage/accounts/accountgrowth/clear.ts delete mode 100644 src/pages/api/backstage/accounts/accountgrowth/register copy.ts create mode 100644 src/pages/team/SaleRecord/components/CustomerAnalysisModal.tsx create mode 100644 src/pages/team/SaleRecord/components/SalesAnalysisModal.tsx create mode 100644 src/pages/team/SaleRecord/test.tsx create mode 100644 src/pages/team/account/index.tsx create mode 100644 src/pages/test/markdown/22.tsx create mode 100644 src/pages/test/markdown/index.tsx create mode 100644 src/pages/test/userstore-debug.tsx create mode 100644 src/utils/highlight.ts diff --git a/package.json b/package.json index 7595856..a865abd 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,9 @@ "bcryptjs": "^3.0.2", "color": "^5.0.0", "dayjs": "^1.11.13", + "echarts": "^5.6.0", + "echarts-for-react": "^3.0.2", + "file-saver": "^2.0.5", "geist": "^1.4.2", "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", @@ -31,14 +34,20 @@ "react-dom": "^19.0.0", "react-error-boundary": "^6.0.0", "react-icons": "^5.5.0", + "react-markdown": "^10.1.0", + "rehype-highlight": "^7.0.2", + "rehype-raw": "^7.0.0", + "remark-gfm": "^4.0.1", "socket.io": "^4.8.1", "socket.io-client": "^4.8.1", "styled-components": "^6.0.9", "uuid": "^11.1.0", + "xlsx": "^0.18.5", "zustand": "^5.0.5" }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/file-saver": "^2.0.7", "@types/jsonwebtoken": "^9.0.9", "@types/node": "^20", "@types/ramda": "^0.30.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a14aa4..a5af8d3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,15 @@ importers: dayjs: specifier: ^1.11.13 version: 1.11.13 + echarts: + specifier: ^5.6.0 + version: 5.6.0 + echarts-for-react: + specifier: ^3.0.2 + version: 3.0.2(echarts@5.6.0)(react@19.1.0) + file-saver: + specifier: ^2.0.5 + version: 2.0.5 geist: specifier: ^1.4.2 version: 1.4.2(next@15.3.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) @@ -74,6 +83,18 @@ importers: react-icons: specifier: ^5.5.0 version: 5.5.0(react@19.1.0) + react-markdown: + specifier: ^10.1.0 + version: 10.1.0(@types/react@19.1.6)(react@19.1.0) + rehype-highlight: + specifier: ^7.0.2 + version: 7.0.2 + rehype-raw: + specifier: ^7.0.0 + version: 7.0.0 + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 socket.io: specifier: ^4.8.1 version: 4.8.1 @@ -86,6 +107,9 @@ importers: uuid: specifier: ^11.1.0 version: 11.1.0 + xlsx: + specifier: ^0.18.5 + version: 0.18.5 zustand: specifier: ^5.0.5 version: 5.0.5(@types/react@19.1.6)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)) @@ -93,6 +117,9 @@ importers: '@tailwindcss/postcss': specifier: ^4 version: 4.1.8 + '@types/file-saver': + specifier: ^2.0.7 + version: 2.0.7 '@types/jsonwebtoken': specifier: ^9.0.9 version: 9.0.9 @@ -708,12 +735,30 @@ packages: '@types/cors@2.8.18': resolution: {integrity: sha512-nX3d0sxJW41CqQvfOzVG1NCTXfFDrDWIghCZncpHeWlVFd81zxB/DLhg7avFg6eHLCRX7ckBmoIIcqa++upvJA==} + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/file-saver@2.0.7': + resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/jsonwebtoken@9.0.9': resolution: {integrity: sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==} '@types/lodash@4.17.17': resolution: {integrity: sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNo/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQ==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -738,6 +783,12 @@ packages: '@types/stylis@4.2.5': resolution: {integrity: sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==} + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/uuid@10.0.0': resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} @@ -755,6 +806,9 @@ packages: peerDependencies: react: '*' + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@yr/monotone-cubic-spline@1.0.3': resolution: {integrity: sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==} @@ -765,6 +819,10 @@ packages: add-dom-event-listener@1.1.0: resolution: {integrity: sha512-WCxx1ixHT0GQU9hb0KI/mhgRQhnU+U3GvwY6ZvVjYq8rsihIGoaIOUbY0yMPBxLH5MDtr0kz3fisWGNcbWW7Jw==} + adler-32@1.3.1: + resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==} + engines: {node: '>=0.8'} + antd@5.25.4: resolution: {integrity: sha512-yXdWqq1NJSZnD1HoPZWnWuQJGVYYnB3h0Ufsz4sbt3T0N9SdJ4G9GPpLMk8Gn9zWtwBekfR4THPVZ9uzAyhBHQ==} peerDependencies: @@ -774,6 +832,9 @@ packages: apexcharts@4.7.0: resolution: {integrity: sha512-iZSrrBGvVlL+nt2B1NpqfDuBZ9jX61X9I2+XV0hlYXHtTwhwLTHDKGXjNXAgFBDLuvSYCB/rq2nPWVPRv2DrGA==} + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + base64id@2.0.0: resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} engines: {node: ^4.5.0 || >= 5.9} @@ -799,6 +860,25 @@ packages: caniuse-lite@1.0.30001720: resolution: {integrity: sha512-Ec/2yV2nNPwb4DnTANEV99ZWwm3ZWfdlfkQbWSDDt+PsXEVYwlhPH8tdMaPunYTKKmz7AnHi2oNEi1GcmKCD8g==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + cfb@1.2.2: + resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==} + engines: {node: '>=0.8'} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + chownr@3.0.0: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} @@ -809,6 +889,10 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + codepage@1.15.0: + resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==} + engines: {node: '>=0.8'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -839,6 +923,9 @@ packages: resolution: {integrity: sha512-16BlyiuyLq3MLxpRWyOTiWsO3ii/eLQLJUQXBSNcxMBBSnyt1ee9YUdaozQp03ifwm5woztEZGDbk9RGVuCsdw==} engines: {node: '>=18'} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + compute-scroll-into-view@3.1.1: resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==} @@ -853,6 +940,11 @@ packages: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} engines: {node: '>= 0.10'} + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + css-color-keywords@1.0.0: resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==} engines: {node: '>=4'} @@ -884,6 +976,9 @@ packages: supports-color: optional: true + decode-named-character-reference@1.1.0: + resolution: {integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -892,9 +987,21 @@ packages: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + echarts-for-react@3.0.2: + resolution: {integrity: sha512-DRwIiTzx8JfwPOVgGttDytBqdp5VzCSyMRIxubgU/g2n9y3VLUmF2FK7Icmg/sNVkv4+rktmrLN9w22U2yy3fA==} + peerDependencies: + echarts: ^3.0.0 || ^4.0.0 || ^5.0.0 + react: ^15.0.0 || >=16.0.0 + + echarts@5.6.0: + resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==} + engine.io-client@6.6.3: resolution: {integrity: sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==} @@ -910,6 +1017,30 @@ packages: resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} engines: {node: '>=10.13.0'} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + file-saver@2.0.5: + resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==} + + frac@1.1.2: + resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==} + engines: {node: '>=0.8'} + geist@1.4.2: resolution: {integrity: sha512-OQUga/KUc8ueijck6EbtT07L4tZ5+TZgjw8PyWfxo16sL5FWk7gNViPNU8hgCFjy6bJi9yuTP+CRpywzaGN8zw==} peerDependencies: @@ -918,9 +1049,65 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + hast-util-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} + + hast-util-is-element@3.0.0: + resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} + + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hast-util-raw@9.1.0: + resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-to-parse5@8.0.0: + resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==} + + hast-util-to-text@4.0.2: + resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + + highlight.js@11.11.1: + resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} + engines: {node: '>=12.0.0'} + + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + inline-style-parser@0.2.4: + resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + jiti@2.4.2: resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} hasBin: true @@ -1036,16 +1223,154 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + lowlight@3.3.0: + resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==} + magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + memory-pager@1.5.0: resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -1146,6 +1471,12 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + path-to-regexp@8.2.0: resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} engines: {node: '>=16'} @@ -1171,6 +1502,12 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + property-information@6.5.0: + resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1445,6 +1782,12 @@ packages: react-lifecycles-compat@3.0.4: resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + react@19.1.0: resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} engines: {node: '>=0.10.0'} @@ -1454,6 +1797,24 @@ packages: peerDependencies: react: '*' + rehype-highlight@7.0.2: + resolution: {integrity: sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==} + + rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + resize-observer-polyfill@1.5.1: resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} @@ -1488,6 +1849,9 @@ packages: simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + size-sensor@1.0.2: + resolution: {integrity: sha512-2NCmWxY7A9pYKGXNBfteo4hy14gWu47rg5692peVMst6lQLPKrVjhY+UTEsPI5ceFRJSl3gVgMYaUi/hKuaiKw==} + socket.io-adapter@2.5.5: resolution: {integrity: sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==} @@ -1507,9 +1871,16 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + sparse-bitfield@3.0.3: resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} + ssf@0.11.2: + resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==} + engines: {node: '>=0.8'} + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -1517,6 +1888,15 @@ packages: string-convert@0.2.1: resolution: {integrity: sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + style-to-js@1.1.16: + resolution: {integrity: sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==} + + style-to-object@1.0.8: + resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==} + styled-components@6.1.18: resolution: {integrity: sha512-Mvf3gJFzZCkhjY2Y/Fx9z1m3dxbza0uI9H1CbNZm/jSHCojzJhQ0R7bByrlFJINnMzz/gPulpoFFGymNwrsMcw==} engines: {node: '>= 16'} @@ -1573,9 +1953,18 @@ packages: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-toolbelt@9.6.0: resolution: {integrity: sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==} + tslib@2.3.0: + resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} + tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} @@ -1593,6 +1982,27 @@ packages: undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-find-after@5.0.0: + resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} + + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + use-sync-external-store@1.5.0: resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} peerDependencies: @@ -1606,9 +2016,21 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + + vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + warning@4.0.3: resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} @@ -1617,6 +2039,14 @@ packages: resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} engines: {node: '>=18'} + wmf@1.0.2: + resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==} + engines: {node: '>=0.8'} + + word@0.3.0: + resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==} + engines: {node: '>=0.8'} + ws@8.17.1: resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} engines: {node: '>=10.0.0'} @@ -1629,6 +2059,11 @@ packages: utf-8-validate: optional: true + xlsx@0.18.5: + resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==} + engines: {node: '>=0.8'} + hasBin: true + xmlhttprequest-ssl@2.1.2: resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} engines: {node: '>=0.4.0'} @@ -1637,6 +2072,9 @@ packages: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} + zrender@5.6.1: + resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==} + zustand@5.0.5: resolution: {integrity: sha512-mILtRfKW9xM47hqxGIxCv12gXusoY/xTSHBYApXozR0HmQv299whhBeeAcRy+KrPPybzosvJBCOmVjq6x12fCg==} engines: {node: '>=12.20.0'} @@ -1655,6 +2093,9 @@ packages: use-sync-external-store: optional: true + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -2298,6 +2739,22 @@ snapshots: dependencies: '@types/node': 20.17.57 + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.8 + + '@types/estree@1.0.8': {} + + '@types/file-saver@2.0.7': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/jsonwebtoken@9.0.9': dependencies: '@types/ms': 2.1.0 @@ -2305,6 +2762,10 @@ snapshots: '@types/lodash@4.17.17': {} + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/ms@2.1.0': {} '@types/node@20.17.57': @@ -2333,6 +2794,10 @@ snapshots: '@types/stylis@4.2.5': {} + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + '@types/uuid@10.0.0': {} '@types/webidl-conversions@7.0.3': {} @@ -2347,6 +2812,8 @@ snapshots: dependencies: react: 19.1.0 + '@ungap/structured-clone@1.3.0': {} + '@yr/monotone-cubic-spline@1.0.3': {} accepts@1.3.8: @@ -2358,6 +2825,8 @@ snapshots: dependencies: object-assign: 4.1.1 + adler-32@1.3.1: {} + antd@5.25.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: '@ant-design/colors': 7.2.1 @@ -2425,6 +2894,8 @@ snapshots: '@svgdotjs/svg.select.js': 4.0.3(@svgdotjs/svg.js@3.2.4) '@yr/monotone-cubic-spline': 1.0.3 + bail@2.0.2: {} + base64id@2.0.0: {} bcryptjs@3.0.2: {} @@ -2441,12 +2912,29 @@ snapshots: caniuse-lite@1.0.30001720: {} + ccount@2.0.1: {} + + cfb@1.2.2: + dependencies: + adler-32: 1.3.1 + crc-32: 1.2.2 + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + chownr@3.0.0: {} classnames@2.5.1: {} client-only@0.0.1: {} + codepage@1.15.0: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -2482,6 +2970,8 @@ snapshots: color-convert: 3.1.0 color-string: 2.0.1 + comma-separated-tokens@2.0.3: {} + compute-scroll-into-view@3.1.1: {} cookie@0.7.2: {} @@ -2495,6 +2985,8 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 + crc-32@1.2.2: {} + css-color-keywords@1.0.0: {} css-to-react-native@3.2.0: @@ -2515,14 +3007,34 @@ snapshots: dependencies: ms: 2.1.3 + decode-named-character-reference@1.1.0: + dependencies: + character-entities: 2.0.2 + dequal@2.0.3: {} detect-libc@2.0.4: {} + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer: 5.2.1 + echarts-for-react@3.0.2(echarts@5.6.0)(react@19.1.0): + dependencies: + echarts: 5.6.0 + fast-deep-equal: 3.1.3 + react: 19.1.0 + size-sensor: 1.0.2 + + echarts@5.6.0: + dependencies: + tslib: 2.3.0 + zrender: 5.6.1 + engine.io-client@6.6.3: dependencies: '@socket.io/component-emitter': 3.1.2 @@ -2558,15 +3070,134 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.2.2 + entities@6.0.1: {} + + escape-string-regexp@5.0.0: {} + + estree-util-is-identifier-name@3.0.0: {} + + extend@3.0.2: {} + + fast-deep-equal@3.1.3: {} + + file-saver@2.0.5: {} + + frac@1.1.2: {} + geist@1.4.2(next@15.3.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)): dependencies: next: 15.3.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) graceful-fs@4.2.11: {} + hast-util-from-parse5@8.0.3: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.1 + property-information: 7.1.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + + hast-util-is-element@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-raw@9.1.0: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + '@ungap/structured-clone': 1.3.0 + hast-util-from-parse5: 8.0.3 + hast-util-to-parse5: 8.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + parse5: 7.3.0 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.16 + unist-util-position: 5.0.0 + vfile-message: 4.0.2 + transitivePeerDependencies: + - supports-color + + hast-util-to-parse5@8.0.0: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-text@4.0.2: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + hast-util-is-element: 3.0.0 + unist-util-find-after: 5.0.0 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + + highlight.js@11.11.1: {} + + html-url-attributes@3.0.1: {} + + html-void-elements@3.0.0: {} + + inline-style-parser@0.2.4: {} + + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + is-arrayish@0.3.2: optional: true + is-decimal@2.0.1: {} + + is-hexadecimal@2.0.1: {} + + is-plain-obj@4.1.0: {} + jiti@2.4.2: {} js-tokens@4.0.0: {} @@ -2664,16 +3295,370 @@ snapshots: lodash@4.17.21: {} + longest-streak@3.1.0: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 + lowlight@3.3.0: + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + highlight.js: 11.11.1 + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + markdown-table@3.0.4: {} + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + mdast-util-from-markdown@2.0.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.1.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.0 + + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + memory-pager@1.5.0: {} + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.1.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.1 + decode-named-character-reference: 1.1.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + mime-db@1.52.0: {} mime-types@2.1.35: @@ -2759,6 +3744,20 @@ snapshots: object-assign@4.1.1: {} + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.1.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + path-to-regexp@8.2.0: {} picocolors@1.1.1: {} @@ -2789,6 +3788,10 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + property-information@6.5.0: {} + + property-information@7.1.0: {} + punycode@2.3.1: {} ramda@0.30.1: {} @@ -3155,6 +4158,24 @@ snapshots: react-lifecycles-compat@3.0.4: {} + react-markdown@10.1.0(@types/react@19.1.6)(react@19.1.0): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 19.1.6 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.0 + react: 19.1.0 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + react@19.1.0: {} reactcss@1.2.3(react@19.1.0): @@ -3162,6 +4183,54 @@ snapshots: lodash: 4.17.21 react: 19.1.0 + rehype-highlight@7.0.2: + dependencies: + '@types/hast': 3.0.4 + hast-util-to-text: 4.0.2 + lowlight: 3.3.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + rehype-raw@7.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-raw: 9.1.0 + vfile: 6.0.3 + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.0 + unified: 11.0.5 + vfile: 6.0.3 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + resize-observer-polyfill@1.5.1: {} safe-buffer@5.2.1: {} @@ -3214,6 +4283,8 @@ snapshots: is-arrayish: 0.3.2 optional: true + size-sensor@1.0.2: {} + socket.io-adapter@2.5.5: dependencies: debug: 4.3.7 @@ -3257,14 +4328,33 @@ snapshots: source-map-js@1.2.1: {} + space-separated-tokens@2.0.2: {} + sparse-bitfield@3.0.3: dependencies: memory-pager: 1.5.0 + ssf@0.11.2: + dependencies: + frac: 1.1.2 + streamsearch@1.1.0: {} string-convert@0.2.1: {} + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + style-to-js@1.1.16: + dependencies: + style-to-object: 1.0.8 + + style-to-object@1.0.8: + dependencies: + inline-style-parser: 0.2.4 + styled-components@6.1.18(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: '@emotion/is-prop-valid': 1.2.2 @@ -3317,8 +4407,14 @@ snapshots: dependencies: punycode: 2.3.1 + trim-lines@3.0.1: {} + + trough@2.2.0: {} + ts-toolbelt@9.6.0: {} + tslib@2.3.0: {} + tslib@2.6.2: {} tslib@2.8.1: {} @@ -3331,6 +4427,44 @@ snapshots: undici-types@6.19.8: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-find-after@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + use-sync-external-store@1.5.0(react@19.1.0): dependencies: react: 19.1.0 @@ -3339,10 +4473,27 @@ snapshots: vary@1.1.2: {} + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + + vfile-message@4.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.2 + warning@4.0.3: dependencies: loose-envify: 1.4.0 + web-namespaces@2.0.1: {} + webidl-conversions@7.0.0: {} whatwg-url@14.2.0: @@ -3350,14 +4501,34 @@ snapshots: tr46: 5.1.1 webidl-conversions: 7.0.0 + wmf@1.0.2: {} + + word@0.3.0: {} + ws@8.17.1: {} + xlsx@0.18.5: + dependencies: + adler-32: 1.3.1 + cfb: 1.2.2 + codepage: 1.15.0 + crc-32: 1.2.2 + ssf: 0.11.2 + wmf: 1.0.2 + word: 0.3.0 + xmlhttprequest-ssl@2.1.2: {} yallist@5.0.0: {} + zrender@5.6.1: + dependencies: + tslib: 2.3.0 + zustand@5.0.5(@types/react@19.1.6)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)): optionalDependencies: '@types/react': 19.1.6 react: 19.1.0 use-sync-external-store: 1.5.0(react@19.1.0) + + zwitch@2.0.4: {} diff --git a/public/md2.md b/public/md2.md new file mode 100644 index 0000000..d13edab --- /dev/null +++ b/public/md2.md @@ -0,0 +1,414 @@ + +好,我现在需要帮助用户解决他们在使用Next.js和MongoDB构建客户管理系统时遇到的问题。他们提供了一些代码片段,包括CustomerModal、CustomersPage、以及相关的API处理文件。 + +首先,我需要理解他们的系统结构。看起来他们有一个用于展示客户的页面,一个用于编辑或添加客户的模态窗口,还有一个处理CRUD操作的API端点。用户可能在使用过程中遇到了一些问题,比如数据更新不生效、页面刷新后状态丢 +失、或者错误提示等。 + +接下来,我会详细分析每个文件的功能和潜在的问题: + +1. **CustomerModal**:这是一个用于添加或编辑客户的模态组件。它通过`useContext`获取客户列表上下文,并调用`fetchCustomers`方法来初始化数据。在提交表单时,它会根据是否存在客户ID决定是更新还是创建新客户。 + +2. **CustomersPage**:这个页面负责显示所有客户的数据,并允许用户进行搜索、分页浏览以及添加或编辑客户。使用了`useState`和`useContext`来管理状态和数据 fetching。 + +3. **API处理文件**:这是一个Next.js API路由,用于处理客户的CRUD操作。看起来已经实现了GET(获取单个客户)、PUT(更新客户)和DELETE(删除客户)方法,并且使用了连接数据库的中间件。 + +现在,我需要考虑用户可能遇到的具体问题。假设他们遇到了数据无法正确提交或状态管理的问题,那么我会检查以下几个方面: + +- **上下文的使用**:在`CustomerModal`中,是否正确地传递了客户列表的状态?如果没有正确更新,可能会导致页面显示不正确的数据。 + +- **API请求处理**:在提交表单时,Axios请求是否正确构造?特别是对于PUT请求,是否需要发送完整的地址对象,而不仅仅是部分属性? + +- **错误处理和状态管理**:如果Axios请求失败,是否有适当的错误处理机制?如果没有,可能会导致用户不知道发生了什么问题。 + +- **页面刷新后的数据丢失**:由于`useState`只能在组件内部保持状态,页面刷新会导致状态丢失。这可能意味着用户需要将客户数据存储到后端数据库中,而不是仅仅依赖前端的状态管理。 + +通过分析这些点,我可以推断出用户的问题可能与API请求的处理、上下文更新或错误处理有关。因此,在帮助用户解决问题时,我应该建议他们检查这些方面,并提供相应的解决方案和代码示例。 + + +根据您提供的代码片段,我可以看出您正在使用Next.js和MongoDB构建一个客户管理系统,并且已经实现了基本的CRUD(创建、读取、更新、删除)功能。以下是针对您的问题和可能遇到的挑战的一些建议和解答: + +### 1. 管理客户数据的状态 + +在`CustomersPage`中,您使用了`useState`来管理客户数据的状态。为了确保状态能够正确反映最新的数据,建议将所有对客户的操作(添加、编辑、删除)包装在一个事务中。这样可以防止部分更新导致的数据不一致。 + +**改进代码示例:** + +```typescript +const updateCustomer = async (customer: Customer) => { + try { + // 使用事务包裹CRUD操作,确保数据一致性 + const session = await mongoose.startSession(); + session.startTransaction(); + + // 更新客户数据 + await customer.findByIdAndUpdate( + id, + { $set: customer }, + { session, new: true } + ); + + // 提交事务 + await session.commitTransaction(); + + // 刷新客户列表以显示更新 + refetchCustomers(); + } catch (error) { + console.error('更新客户时出错:', error); + throw error; + } +}; +``` + +### 2. 在`CustomerModal`中正确处理文件上传 + +在您的模态表单中,您已经提到了处理文件上传的需求。以下是使用`react-dropzone`和Axios实现文件上传的示例: + +**改进代码示例:** + +```typescript +import { useDropzone } from 'react-dropzone'; +import axios from 'axios'; + +const CustomerModal = ({ customer }: { customer?: Customer }) => { + const [files, setFiles] = useState([]); + + const onDrop = useCallback((acceptedFiles: File[]) => { + setFiles(acceptedFiles); + }, []); + + const { getRootProps, getInputProps } = useDropzone({ + onDrop, + accept: { + 'image/*': ['.jpeg', '.jpg', '.png'] + } + }); + + const uploadFiles = async () => { + try { + // 假设files是一个包含上传文件的数组 + if (files.length === 0) return; + + // 创建一个 FormData 对象 + const formData = new FormData(); + for (const file of files) { + formData.append('files', file); + } + + // 发送 POST 请求到后端 API + await axios.post('/api/upload', formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }); + + // 上传成功后更新客户数据 + setCustomer(prev => ({ ...prev!, avatar: ' uploaded' })); + + } catch (error) { + console.error('文件上传失败:', error); + throw error; + } + }; + + return ( +
+ + {children} +
+ ); +}; +``` + +### 3. 处理复杂的数据结构 + +在您的`CustomerModal`中,您可能需要处理更复杂的数据结构。例如,如果您有一个嵌套的地址对象或其他关联数据,可以使用`react-hook-form`来管理表单状态,提高表单处理效率。 + +**改进代码示例:** + +```typescript +import { useForm } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +import * as yup from 'yup'; + +interface CustomerSchema { + name: string; + email: string; + address: { + street: string; + city: string; + country: string; + }; +} + +const schema = yup.object().shape({ + name: yup.string().required(), + email: yup.string().email().required(), + address: yup.object().shape({ + street: yup.string().required(), + city: yup.string().required(), + country: yup.string().required() + }) +}); + +export const CustomerModal = ({ customer }: { customer?: Customer }) => { + const { register, handleSubmit, formState } = useForm({ + resolver: yupResolver, + defaultValues: customer || { + name: '', + email: '', + address: { + street: '', + city: '', + country: '' + } + } + }); + + const onSubmit = async (data: CustomerSchema) => { + try { + if (customer?._id) { + // 更新客户 + await axios.put(`/api/customers/${customer._id}`, data); + } else { + // 创建新客户 + await axios.post('/api/customers', data); + } + refetchCustomers(); + onClose(); + } catch (error) { + console.error('表单提交失败:', error); + throw error; + } + }; + + return ( +
+ {/* 表单字段在此 */} +
+ ); +}; +``` + +### 4. 实现分页和排序 + +在`CustomersPage`中,您已经实现了基本的搜索功能。为了进一步提高用户体验,可以添加分页和排序功能。 + +**改进代码示例:** + +```typescript +interface CustomerListState extends CustomersState { + currentPage: number; + itemsPerPage: number; + sortField: keyof Customer; + sortOrder: 'asc' | 'desc'; +} + +const CustomersPage = () => { + const [state, setState] = useState({ + customers: [], + searchTerm: '', + currentPage: 1, + itemsPerPage: 10, + sortField: 'name', + sortOrder: 'asc' + }); + + const handleSortChange = (sortField: keyof Customer) => { + let newSortOrder: 'asc' | 'desc'; + if (state.sortField === sortField && state.sortOrder === 'asc') { + newSortOrder = 'desc'; + } else { + newSortOrder = 'asc'; + } + setState(prev => ({ + ...prev, + sortField, + sortOrder: newSortOrder + })); + }; + + const handlePageChange = (pageNumber: number) => { + setState(prev => ({ ...prev, currentPage: pageNumber })); + }; + + return ( + <> + setState({...state, searchTerm: e.target.value})} + /> + +
+ {Object.keys(Customer).map((field) => ( + + ))} +
+ + + {/* 表格内容 */} +
+ + + + ); +}; +``` + +### 5. 错误处理与日志记录 + +在您的API和前端代码中,错误处理是非常重要的。确保所有网络请求都有适当的错误处理,并将错误信息记录到控制台或集中式日志系统中。 + +**改进代码示例:** + +```typescript +// 在您的 axios 配置中添加拦截器 +axios.interceptors.response.use( + (response) => response, + (error) => { + console.error('请求失败:', error); + // 您可以在这里处理特定错误状态,例如重试逻辑或显示用户友好的错误消息 + return Promise.reject(error); + } +); + +// 在您的组件中,确保您处理了所有可能的网络错误 +const onSubmit = async (data: CustomerSchema) => { + try { + if (customer?._id) { + await axios.put(`/api/customers/${customer._id}`, data) + .catch((error) => { + console.error('更新客户失败:', error); + throw error; + }); + } else { + await axios.post('/api/customers', data) + .catch((error) => { + console.error('创建客户失败:', error); + throw error; + }); + } + refetchCustomers(); + onClose(); + } catch (error) { + // 处理全局错误 + console.error('表单提交失败:', error); + // 您可以在这里显示用户友好的错误消息 + } +}; +``` + +### 6. 性能优化 + +为了提高您的应用性能,可以考虑以下措施: + +- **数据缓存**:使用本地存储或缓存解决方案(如 `redux`、`mobx` 或简单的 `localStorage`) 来减少对后端的请求次数。 + +- **懒加载**:对于表格中的大量数据,可以实现懒加载,只在需要时加载更多数据。 + +- **优化API调用**:确保您的API端点进行了适当的优化,例如分页、过滤和排序都在服务器端处理。 + +### 7. 测试 + +不要忘记测试您的代码!单元测试、集成测试和用户界面测试都是必不可少的。使用工具如 `Jest`、`React Testing Library` 或 `Cypress` 来帮助您进行测试。 + +**测试示例:** + +```typescript +import { render, fireEvent } from '@testing-library/react'; +import CustomersPage from './CustomersPage'; + +describe('CustomersPage', () => { + it('should display customers when search term is empty', () => { + const mockCustomers = [ + { _id: '1', name: 'John Doe', email: 'john@example.com' }, + // 其他客户... + ]; + + render(); + + // 断言表格中显示了所有客户 + // 您可以在此处添加具体的断言 + }); + + it('should filter customers based on search term', () => { + const mockCustomers = [ + { _id: '1', name: 'John Doe', email: 'john@example.com' }, + { _id: '2', name: 'Jane Smith', email: 'jane@example.com' }, + ]; + + const component = render(); + + // 输入搜索词 + fireEvent.change(component.getByPlaceholderText('搜索客户...'), { + target: { value: 'John' } + }); + + // 断言表格中只显示包含 'John' 的客户 + }); +}); +``` + +### 8. 部署与监控 + +最后,确保您的应用可以顺利部署到生产环境,并且有适当的监控和错误报告机制。使用工具如 `Docker` 进行容器化部署,`Prometheus` 进行监控,以及 `Sentry` 或 `New Relic` 进行错误跟踪。 + +### 总结 + +通过以上改进措施,您的React和TypeScript应用将更加完善、高效,并且易于维护。确保您遵循最佳实践,定期审查和优化代码,并根据用户反馈进行调整,以提供最佳的用户体验。 + + +以下是逐步说明如何实现上述技术改进: + +1. **安装必要的依赖** + + 首先,安装所需的库,如 `typescript`、`axios`、`react-hook-form` 和 `yup` 等。 + + ```bash + npm install typescript axios react-hook-form yup @hookform/resolvers/yup + ``` + +2. **设置 TypeScript 项目** + + 创建一个 `tsconfig.json` 文件以配置 TypeScript 设置,确保您的项目使用最新的 ES6+ 特性,并正确处理模块解析。 + +3. **集成 Axios** + + 在项目中引入 `axios` 来管理 HTTP 请求。您可以在项目的入口点或在需要的地方按需导入和使用 `axios`。 + +4. **实现 Form 表单验证** + + 使用 `react-hook-form` 和 `yup` 进行表单数据验证。定义您的模式,并将其与 `useForm` 钩子结合使用,以确保表单输入的正确性。 + +5. **添加分页和排序功能** + + 在表格组件中实现分页和排序逻辑。您可以通过添加下拉列表或按钮来允许用户选择排序方式(如按名称、电子邮件等),然后根据选定的条件调整数据展示。 + +6. **实施数据懒加载** + + 对于大量数据,使用无限滚动或其他技术实现懒加载,确保在需要时才从服务器获取更多数据,提升应用性能。 + +7. **设置错误处理和日志记录** + + 在您的 API 调用中添加错误拦截器,并将错误信息记录到控制台或集中式日志服务中。同时,在组件级别处理错误状态,以提供友好的用户体验。 + +8. **部署应用** + + 使用容器化技术如 Docker 将您的应用打包,并配置持续集成/持续部署(CI/CD)管道,确保代码顺利交付到生产环境。 + +9. **监控和维护** + + 部署监控工具跟踪应用性能和错误。定期审查日志,优化代码,并根据用户反馈进行调整,以保持应用的最佳状态。 + +通过遵循这些步骤,您可以显著提升 React 和 TypeScript 应用的质量和性能,确保其在各种使用场景下的稳定性和高效性。 \ No newline at end of file diff --git a/public/test2.md b/public/test2.md new file mode 100644 index 0000000..373430e --- /dev/null +++ b/public/test2.md @@ -0,0 +1,59 @@ + +# h1 + +
+ +## h2 + +
+ +**Paragraph** Lorem ipsum is placeholder text commonly used in the graphic, print, and publishing industries for previewing layouts and visual mockups. + +
+ +[Link (https://www.google.com/)](https://www.google.com/) + +
+ +###### Lists + +
+ +- [x] Write the press release +- [ ] Update the website +- [ ] Contact the media + +
+ +--- + +
+ +###### A table: + +
+ +| Syntax | Description | Test Text | +| :--- | :----: | ---: | +| Header | Title | Here's this | +| Paragraph | Text | And more | + +
+ +```tsx +import React from 'react'; +import ReactDOM from 'react-dom'; +import ReactMarkdown from 'react-markdown'; +import rehypeHighlight from 'rehype-highlight'; + +ReactDOM.render( + {'# Your markdown here'}, + document.querySelector('#content') +); +``` + +
+ +![aoun](/aoun.png) + +> A block quote with ~~strikethrough~~ and a URL: [https://reactjs.org](https://reactjs.org). \ No newline at end of file diff --git a/src/components/markdown/index.tsx b/src/components/markdown/index.tsx new file mode 100644 index 0000000..0d4da75 --- /dev/null +++ b/src/components/markdown/index.tsx @@ -0,0 +1,33 @@ +//src\components\markdown\index.tsx +import ReactMarkdown from 'react-markdown'; +// markdown plugins +import rehypeHighlight from 'rehype-highlight'; +import rehypeRaw from 'rehype-raw'; +import remarkGfm from 'remark-gfm'; //为删除线、表格、任务列表和 URL 直接添加支持 +import '@/utils/highlight'; + +import StyledMarkdown from './styles'; +import { ComponentProps } from 'react'; +import { useThemeToken } from '@/theme/hooks'; +import { useSettings } from '@/store/settingStore'; + +// 使用 ComponentProps 提取 ReactMarkdown 的 Props 类型 +type Props = ComponentProps; + +export default function Markdown({ children }: Props) { + const token = useThemeToken(); + const { themeMode } = useSettings(); + return ( + + + {children} + + + ); +} diff --git a/src/components/markdown/styles.ts b/src/components/markdown/styles.ts new file mode 100644 index 0000000..b67bdab --- /dev/null +++ b/src/components/markdown/styles.ts @@ -0,0 +1,161 @@ +import { ThemeMode } from '@/types/enum'; +import { GlobalToken } from 'antd'; +import styled from 'styled-components'; + +const StyledMarkdown = styled.div<{ + $token: GlobalToken; + //$thememode: ThemeMode + $thememode: any +}>` + display: grid; + // Text + h1 { + font-size: 1.75rem; + font-weight: 800; + } + h2 { + font-size: 1.35rem; + font-weight: 700; + } + h3 { + font-size: 1.25rem; + font-weight: 600; + } + h4 { + font-size: 1.075rem; + font-weight: 500; + } + h5 { + font-size: 0.95rem; + font-weight: 400; + } + h6 { + font-size: 0.825rem; + font-weight: 300; + } + + a { + color: ${(props) => props.$token.colorPrimary}; + } + img { + border-radius: 4px; + } + br { + display: grid; + content: ''; + margin-top: 0.75em; + } + + // Divider + hr { + margin: 0; + border-width: 1; + border-style: solid; + } + + // List + ul, + ol { + margin: 0; + li { + line-height: 2; + display: flex; + align-items: center; + } + } + + // Blockquote + blockquote { + line-height: 1.5; + font-size: 1.5em; + margin: 40px auto; + position: relative; + padding: 24px 24px 24px 64px; + border-radius: 16px; + background-color: #f4f6f8; + color: #637381; + p, + span { + margin-bottom: 0; + font-size: inherit; + font-family: inherit; + } + &::before { + left: 16px; + top: -8px; + display: block; + font-size: 3em; + position: absolute; + content: '“'; + } + } + + // Code Block + pre, + pre > code { + font-size: 16px; + overflow-x: auto; + white-space: pre; + border-radius: 8px; + } + code { + font-size: 14px; + border-radius: 4px; + white-space: pre; + padding: 0px; + background-color: ${(props) => + props.$thememode === ThemeMode.Light ? '#161c24' : '#919eab29'}; + } + + // Table + table { + width: 100%; + border-collapse: collapse; + border: 1px solid #919eab33; + th, + td { + padding: 8px; + border: 1px solid #919eab33; + } + tbody tr:nth-of-type(odd) { + //background-color: #f4f6f8; //改为根据主题明暗度设置,暗色颜色为#161c24,明色颜色为#f4f6f8 + } + } + + // Checkbox + input { + margin-right: 10px; + &[type='checkbox'] { + position: relative; + cursor: pointer; + &::before { + content: ''; + top: -2px; + left: -2px; + width: 17px; + height: 17px; + border-radius: 3px; + position: absolute; + background-color: #f4f6f8; + } + &:checked { + &::before { + background-color: ${(props) => props.$token.colorPrimary}; + } + &::after { + content: ''; + top: 1px; + left: 5px; + width: 4px; + height: 9px; + position: absolute; + transform: rotate(45deg); + border: solid white; + border-width: 0 2px 2px 0; + } + } + } + } +`; + +export default StyledMarkdown; diff --git a/src/components/markdown2/index.tsx b/src/components/markdown2/index.tsx new file mode 100644 index 0000000..4e12abe --- /dev/null +++ b/src/components/markdown2/index.tsx @@ -0,0 +1,30 @@ +//src\components\markdown\index.tsx +import ReactMarkdown from 'react-markdown'; +// markdown plugins +import rehypeHighlight from 'rehype-highlight'; +import rehypeRaw from 'rehype-raw'; +import remarkGfm from 'remark-gfm'; //为删除线、表格、任务列表和 URL 直接添加支持 +import '@/utils/highlight'; + +//import StyledMarkdown from './styles'; +import { ComponentProps } from 'react'; +// import { useThemeToken } from '@/theme/hooks'; +//import { useSettings } from '@/store/settingStore'; + +// 使用 ComponentProps 提取 ReactMarkdown 的 Props 类型 +type Props = ComponentProps; + +export default function Markdown({ children }: Props) { + //const token = useThemeToken(); + //const { themeMode } = useSettings(); + return ( + + + {children} + + + ); +} diff --git a/src/components/markdown2/styles.ts b/src/components/markdown2/styles.ts new file mode 100644 index 0000000..b67bdab --- /dev/null +++ b/src/components/markdown2/styles.ts @@ -0,0 +1,161 @@ +import { ThemeMode } from '@/types/enum'; +import { GlobalToken } from 'antd'; +import styled from 'styled-components'; + +const StyledMarkdown = styled.div<{ + $token: GlobalToken; + //$thememode: ThemeMode + $thememode: any +}>` + display: grid; + // Text + h1 { + font-size: 1.75rem; + font-weight: 800; + } + h2 { + font-size: 1.35rem; + font-weight: 700; + } + h3 { + font-size: 1.25rem; + font-weight: 600; + } + h4 { + font-size: 1.075rem; + font-weight: 500; + } + h5 { + font-size: 0.95rem; + font-weight: 400; + } + h6 { + font-size: 0.825rem; + font-weight: 300; + } + + a { + color: ${(props) => props.$token.colorPrimary}; + } + img { + border-radius: 4px; + } + br { + display: grid; + content: ''; + margin-top: 0.75em; + } + + // Divider + hr { + margin: 0; + border-width: 1; + border-style: solid; + } + + // List + ul, + ol { + margin: 0; + li { + line-height: 2; + display: flex; + align-items: center; + } + } + + // Blockquote + blockquote { + line-height: 1.5; + font-size: 1.5em; + margin: 40px auto; + position: relative; + padding: 24px 24px 24px 64px; + border-radius: 16px; + background-color: #f4f6f8; + color: #637381; + p, + span { + margin-bottom: 0; + font-size: inherit; + font-family: inherit; + } + &::before { + left: 16px; + top: -8px; + display: block; + font-size: 3em; + position: absolute; + content: '“'; + } + } + + // Code Block + pre, + pre > code { + font-size: 16px; + overflow-x: auto; + white-space: pre; + border-radius: 8px; + } + code { + font-size: 14px; + border-radius: 4px; + white-space: pre; + padding: 0px; + background-color: ${(props) => + props.$thememode === ThemeMode.Light ? '#161c24' : '#919eab29'}; + } + + // Table + table { + width: 100%; + border-collapse: collapse; + border: 1px solid #919eab33; + th, + td { + padding: 8px; + border: 1px solid #919eab33; + } + tbody tr:nth-of-type(odd) { + //background-color: #f4f6f8; //改为根据主题明暗度设置,暗色颜色为#161c24,明色颜色为#f4f6f8 + } + } + + // Checkbox + input { + margin-right: 10px; + &[type='checkbox'] { + position: relative; + cursor: pointer; + &::before { + content: ''; + top: -2px; + left: -2px; + width: 17px; + height: 17px; + border-radius: 3px; + position: absolute; + background-color: #f4f6f8; + } + &:checked { + &::before { + background-color: ${(props) => props.$token.colorPrimary}; + } + &::after { + content: ''; + top: 1px; + left: 5px; + width: 4px; + height: 9px; + position: absolute; + transform: rotate(45deg); + border: solid white; + border-width: 0 2px 2px 0; + } + } + } + } +`; + +export default StyledMarkdown; diff --git a/src/utils/ConnectDB.ts b/src/lib/connectDB.ts similarity index 100% rename from src/utils/ConnectDB.ts rename to src/lib/connectDB.ts diff --git a/src/models/index.ts b/src/models/index.ts index 0081b25..36d1abe 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -387,6 +387,7 @@ LogisticsRecordSchema.index({ 关联记录: 1 }); // 对关联记录字段建 const DailyGrowthSchema = new Schema({ 日期: { type: Date, required: true }, 总人数: { type: Number, required: true }, + 消耗: { type: Number, required: true }, 扣除人数: { type: Number, required: true }, 日增长人数: { type: Number, required: true }, // 可在前端计算 }); diff --git a/src/models/types.ts b/src/models/types.ts index c55ed9d..23e9363 100644 --- a/src/models/types.ts +++ b/src/models/types.ts @@ -231,6 +231,7 @@ export interface IAccount { // 定义日增长数据接口类型 export interface IDailyGrowth { 日期: Date; + 消耗: number; // 推广引流消费 总人数: number; 扣除人数: number; 日增长人数?: number; diff --git a/src/pages/api/admin/cleanup-indexes.ts b/src/pages/api/admin/cleanup-indexes.ts index 7ecf63e..8c12e8b 100644 --- a/src/pages/api/admin/cleanup-indexes.ts +++ b/src/pages/api/admin/cleanup-indexes.ts @@ -6,7 +6,7 @@ */ import { NextApiRequest, NextApiResponse } from 'next'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; import { fullIndexCleanup } from '@/utils/cleanupIndexes'; /** diff --git a/src/pages/api/backstage/accounts/[id].ts b/src/pages/api/backstage/accounts/[id].ts index b09fd9f..d55a2a8 100644 --- a/src/pages/api/backstage/accounts/[id].ts +++ b/src/pages/api/backstage/accounts/[id].ts @@ -1,6 +1,6 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { Account } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { const { query: { id }, method } = req; diff --git a/src/pages/api/backstage/accounts/accountgrowth/clear.ts b/src/pages/api/backstage/accounts/accountgrowth/clear.ts new file mode 100644 index 0000000..02e844a --- /dev/null +++ b/src/pages/api/backstage/accounts/accountgrowth/clear.ts @@ -0,0 +1,102 @@ +/** + * 文件名: src/pages/api/backstage/accounts/accountgrowth/clear.ts + * 作者: 阿瑞 + * 功能: 清除指定账号的日增长数据 (开发调试功能) + * 版本: v1.0.0 + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { Account } from '@/models'; +import { Types } from 'mongoose'; + +interface ClearGrowthDataRequest { + accountId: string; + teamId: string; +} + +interface ApiResponse { + success: boolean; + message: string; + error?: string; +} + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + // ▸ 方法验证模块 + if (req.method !== 'DELETE') { + return res.status(405).json({ + success: false, + message: '方法不被允许' + }); + } + + // ▸ 开发环境检查模块 + if (process.env.NODE_ENV !== 'development') { + return res.status(403).json({ + success: false, + message: '此功能仅在开发环境下可用' + }); + } + + try { + // ▸ 请求数据验证模块 + const { accountId, teamId }: ClearGrowthDataRequest = req.body; + + if (!accountId || !teamId) { + return res.status(400).json({ + success: false, + message: '缺少必要参数:accountId 和 teamId' + }); + } + + // 验证 ObjectId 格式 + if (!Types.ObjectId.isValid(accountId) || !Types.ObjectId.isValid(teamId)) { + return res.status(400).json({ + success: false, + message: '无效的 ID 格式' + }); + } + + // ▸ 账号验证模块 + const account = await Account.findOne({ + _id: new Types.ObjectId(accountId), + 团队: new Types.ObjectId(teamId) + }); + + if (!account) { + return res.status(404).json({ + success: false, + message: '未找到指定账号' + }); + } + + // ▸ 数据清除模块 + const result = await Account.updateOne( + { _id: new Types.ObjectId(accountId) }, + { $set: { 日增长数据: [] } } + ); + + if (result.modifiedCount === 0) { + return res.status(400).json({ + success: false, + message: '清除操作失败' + }); + } + + // ▸ 成功响应模块 + return res.status(200).json({ + success: true, + message: `账号 ${account.账号编号} 的日增长数据已成功清除` + }); + + } catch (error) { + console.error('清除日增长数据错误:', error); + return res.status(500).json({ + success: false, + message: '服务器内部错误', + error: error instanceof Error ? error.message : '未知错误' + }); + } +} \ No newline at end of file diff --git a/src/pages/api/backstage/accounts/accountgrowth/index.ts b/src/pages/api/backstage/accounts/accountgrowth/index.ts index 43d4c75..ad27d0f 100644 --- a/src/pages/api/backstage/accounts/accountgrowth/index.ts +++ b/src/pages/api/backstage/accounts/accountgrowth/index.ts @@ -1,6 +1,6 @@ //src\pages\api\backstage\accounts\accountgrowth\index.ts import type { NextApiRequest, NextApiResponse } from 'next'; -import connectDB from '@/utils/connectDB'; // 使用高阶函数 +import connectDB from '@/lib/connectDB'; // 使用高阶函数 import { Account } from '@/models'; // 使用之前的 AccountSchema 模型 interface AccountData { diff --git a/src/pages/api/backstage/accounts/accountgrowth/register copy.ts b/src/pages/api/backstage/accounts/accountgrowth/register copy.ts deleted file mode 100644 index 31eabc4..0000000 --- a/src/pages/api/backstage/accounts/accountgrowth/register copy.ts +++ /dev/null @@ -1,56 +0,0 @@ -//src\pages\api\backstage\accounts\accountgrowth\register.ts -import type { NextApiRequest, NextApiResponse } from 'next'; -import connectDB from '@/utils/connectDB'; // 使用高阶函数确保数据库连接 -import { Account } from '@/models'; // 使用 Account 模型 -import dayjs from 'dayjs'; - -interface DailyGrowth { - 日期: Date; - 总人数: number; - 扣除人数: number; - 日增长人数: number; -} - -type Data = { - success?: boolean; - error?: string; -}; - -const handler = async (req: NextApiRequest, res: NextApiResponse) => { - const { accountId, date, totalPeople, deductedPeople } = req.body; - - try { - // 找到目标账号 - const account = await Account.findById(accountId); - if (!account) { - return res.status(404).json({ error: '账号未找到' }); - } - - // 查找是否已经存在当天的记录 - const existingRecord = account.日增长数据.find((record: DailyGrowth) => - dayjs(record.日期).isSame(dayjs(date), 'day') - ); - - if (existingRecord) { - // 更新已有记录 - existingRecord.总人数 = totalPeople; - existingRecord.扣除人数 = deductedPeople; - existingRecord.日增长人数 = totalPeople - deductedPeople; - } else { - // 添加新记录 - account.日增长数据.push({ - 日期: dayjs(date).toDate(), - 总人数: totalPeople, - 扣除人数: deductedPeople, - 日增长人数: totalPeople - deductedPeople, - }); - } - - await account.save(); - res.status(200).json({ success: true }); - } catch (error) { - res.status(500).json({ error: '保存增长记录失败' }); - } -}; - -export default connectDB(handler); // 使用 connectDB 包装 API 处理函数 diff --git a/src/pages/api/backstage/accounts/accountgrowth/register.ts b/src/pages/api/backstage/accounts/accountgrowth/register.ts index b3408a5..0fe08d5 100644 --- a/src/pages/api/backstage/accounts/accountgrowth/register.ts +++ b/src/pages/api/backstage/accounts/accountgrowth/register.ts @@ -1,13 +1,27 @@ import { NextApiRequest, NextApiResponse } from 'next'; import { broadcastUpdate } from './sse'; // 引入广播功能 import { Account } from '@/models'; +import connectDB from '@/lib/connectDB'; import dayjs from 'dayjs'; import { IDailyGrowth } from '@/models/types'; // 引入 DailyGrowth 类型 const handler = async (req: NextApiRequest, res: NextApiResponse) => { - const { accountId, date, totalPeople, deductedPeople } = req.body; + if (req.method !== 'POST') { + return res.status(405).json({ error: '仅支持POST请求' }); + } + + const { accountId, date, consumption, totalPeople, deductedPeople } = req.body; + + // 验证必要参数 + if (!accountId || !date || totalPeople === undefined || deductedPeople === undefined) { + return res.status(400).json({ + error: '缺少必要参数', + received: { accountId, date, consumption, totalPeople, deductedPeople } + }); + } try { + console.log('接收到的数据:', { accountId, date, consumption, totalPeople, deductedPeople }); const account = await Account.findById(accountId); if (!account) { return res.status(404).json({ error: '账号未找到' }); @@ -39,12 +53,14 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { // 更新现有记录或添加新记录 if (existingRecord) { + existingRecord.消耗 = consumption || 0; existingRecord.总人数 = totalPeople; existingRecord.扣除人数 = deductedPeople; existingRecord.日增长人数 = growthPeople; } else { account.日增长数据.push({ 日期: selectedDate.toDate(), + 消耗: consumption || 0, 总人数: totalPeople, 扣除人数: deductedPeople, 日增长人数: growthPeople, @@ -58,8 +74,12 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { res.status(200).json({ success: true }); } catch (error) { - res.status(500).json({ error: '保存增长记录失败' }); + console.error('保存增长记录失败:', error); + res.status(500).json({ + error: '保存增长记录失败', + details: error instanceof Error ? error.message : '未知错误' + }); } }; -export default handler; +export default connectDB(handler); diff --git a/src/pages/api/backstage/accounts/index.ts b/src/pages/api/backstage/accounts/index.ts index abe5216..db748f5 100644 --- a/src/pages/api/backstage/accounts/index.ts +++ b/src/pages/api/backstage/accounts/index.ts @@ -1,6 +1,6 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { Account } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === 'GET') { diff --git a/src/pages/api/backstage/balance/[id].ts b/src/pages/api/backstage/balance/[id].ts index 2f8ced7..0ac45d9 100644 --- a/src/pages/api/backstage/balance/[id].ts +++ b/src/pages/api/backstage/balance/[id].ts @@ -1,5 +1,5 @@ import { NextApiRequest, NextApiResponse } from 'next'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; import { Transaction } from '@/models'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { diff --git a/src/pages/api/backstage/brands/[id].ts b/src/pages/api/backstage/brands/[id].ts index e06c591..fa3bd24 100644 --- a/src/pages/api/backstage/brands/[id].ts +++ b/src/pages/api/backstage/brands/[id].ts @@ -1,6 +1,6 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { Brand } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { const { query: { id }, method } = req; diff --git a/src/pages/api/backstage/brands/index.ts b/src/pages/api/backstage/brands/index.ts index 8febc00..b188dbe 100644 --- a/src/pages/api/backstage/brands/index.ts +++ b/src/pages/api/backstage/brands/index.ts @@ -1,6 +1,6 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { Brand } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === 'GET') { diff --git a/src/pages/api/backstage/categories/[id].ts b/src/pages/api/backstage/categories/[id].ts index 196e3a1..b745047 100644 --- a/src/pages/api/backstage/categories/[id].ts +++ b/src/pages/api/backstage/categories/[id].ts @@ -1,6 +1,6 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { Category } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { const { query: { id }, method } = req; diff --git a/src/pages/api/backstage/categories/index.ts b/src/pages/api/backstage/categories/index.ts index 0a389e9..0bb2f84 100644 --- a/src/pages/api/backstage/categories/index.ts +++ b/src/pages/api/backstage/categories/index.ts @@ -1,6 +1,6 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { Category } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === 'GET') { diff --git a/src/pages/api/backstage/coupons/[id].ts b/src/pages/api/backstage/coupons/[id].ts index d201753..eba10bb 100644 --- a/src/pages/api/backstage/coupons/[id].ts +++ b/src/pages/api/backstage/coupons/[id].ts @@ -1,6 +1,6 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { Coupon } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { const { query: { id }, method } = req; diff --git a/src/pages/api/backstage/coupons/assign/[id].ts b/src/pages/api/backstage/coupons/assign/[id].ts index cc897c7..73299b8 100644 --- a/src/pages/api/backstage/coupons/assign/[id].ts +++ b/src/pages/api/backstage/coupons/assign/[id].ts @@ -1,7 +1,7 @@ //src\pages\api\backstage\coupons\assign\[id].ts import type { NextApiRequest, NextApiResponse } from 'next'; import { Customer, Coupon } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; import { v4 as uuidv4 } from 'uuid'; // 用于生成唯一券码 const handler = async (req: NextApiRequest, res: NextApiResponse) => { diff --git a/src/pages/api/backstage/coupons/assign/index.ts b/src/pages/api/backstage/coupons/assign/index.ts index 9641dc9..10370bf 100644 --- a/src/pages/api/backstage/coupons/assign/index.ts +++ b/src/pages/api/backstage/coupons/assign/index.ts @@ -1,7 +1,7 @@ //src\pages\api\backstage\coupons\assign\index.ts import type { NextApiRequest, NextApiResponse } from 'next'; import { Customer } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === 'GET') { diff --git a/src/pages/api/backstage/coupons/index.ts b/src/pages/api/backstage/coupons/index.ts index 5b73aa3..5a62c1b 100644 --- a/src/pages/api/backstage/coupons/index.ts +++ b/src/pages/api/backstage/coupons/index.ts @@ -1,7 +1,7 @@ //src\pages\api\backstage\coupons\index.ts import type { NextApiRequest, NextApiResponse } from 'next'; import { Coupon } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === 'GET') { diff --git a/src/pages/api/backstage/customers/[id].ts b/src/pages/api/backstage/customers/[id].ts index bd0886e..fa2fb7d 100644 --- a/src/pages/api/backstage/customers/[id].ts +++ b/src/pages/api/backstage/customers/[id].ts @@ -1,7 +1,7 @@ //src\pages\api\backstage\customers\[id].ts import type { NextApiRequest, NextApiResponse } from 'next'; import { Customer } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { const { query: { id }, method } = req; diff --git a/src/pages/api/backstage/customers/index.ts b/src/pages/api/backstage/customers/index.ts index 007c7cd..2079a22 100644 --- a/src/pages/api/backstage/customers/index.ts +++ b/src/pages/api/backstage/customers/index.ts @@ -1,7 +1,7 @@ //src\pages\api\backstage\customers\index.ts import type { NextApiRequest, NextApiResponse } from 'next'; import { Customer } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === 'GET') { diff --git a/src/pages/api/backstage/customers/sales/[id].ts b/src/pages/api/backstage/customers/sales/[id].ts index e759a6d..dbf7388 100644 --- a/src/pages/api/backstage/customers/sales/[id].ts +++ b/src/pages/api/backstage/customers/sales/[id].ts @@ -1,6 +1,6 @@ // src/pages/api/backstage/customers/sales/[id].ts import type { NextApiRequest, NextApiResponse } from 'next'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; import { Customer, SalesRecord, AfterSalesRecord } from '@/models'; // 导入模型 import { ICustomer } from '@/models/types'; diff --git a/src/pages/api/backstage/mine/info/[id].ts b/src/pages/api/backstage/mine/info/[id].ts index b569a19..604bf85 100644 --- a/src/pages/api/backstage/mine/info/[id].ts +++ b/src/pages/api/backstage/mine/info/[id].ts @@ -1,7 +1,7 @@ // src/pages/api/backstage/mine/info/[id].ts import { NextApiRequest, NextApiResponse } from 'next'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; import { User } from '@/models'; import { IPermission, IUser } from '@/models/types'; import { buildPermissionTree } from '@/pages/api/buildPermissionTree'; diff --git a/src/pages/api/backstage/mine/sales/index.ts b/src/pages/api/backstage/mine/sales/index.ts index 7d57c12..09a9ed3 100644 --- a/src/pages/api/backstage/mine/sales/index.ts +++ b/src/pages/api/backstage/mine/sales/index.ts @@ -1,7 +1,7 @@ // src/pages/api/backstage/mine/sales/index.ts import { NextApiRequest, NextApiResponse } from 'next'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; import { SalesRecord } from '@/models'; //import { ISalesRecord } from '@/models/types'; diff --git a/src/pages/api/backstage/payment-platforms/[id].ts b/src/pages/api/backstage/payment-platforms/[id].ts index 7231392..adc45f3 100644 --- a/src/pages/api/backstage/payment-platforms/[id].ts +++ b/src/pages/api/backstage/payment-platforms/[id].ts @@ -1,7 +1,7 @@ // src/pages/api/backstage/payment-platforms/[id].ts import type { NextApiRequest, NextApiResponse } from 'next'; import { PaymentPlatform } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { const { query: { id }, method } = req; diff --git a/src/pages/api/backstage/payment-platforms/index.ts b/src/pages/api/backstage/payment-platforms/index.ts index 67257de..0f72ba0 100644 --- a/src/pages/api/backstage/payment-platforms/index.ts +++ b/src/pages/api/backstage/payment-platforms/index.ts @@ -1,7 +1,7 @@ // src/pages/api/backstage/payment-platforms/index.ts import type { NextApiRequest, NextApiResponse } from 'next'; import { PaymentPlatform } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === 'GET') { diff --git a/src/pages/api/backstage/products/[id].ts b/src/pages/api/backstage/products/[id].ts index 3a21cfd..88e169f 100644 --- a/src/pages/api/backstage/products/[id].ts +++ b/src/pages/api/backstage/products/[id].ts @@ -1,7 +1,7 @@ //src\pages\api\backstage\products\[id].ts import type { NextApiRequest, NextApiResponse } from 'next'; import { Product } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { const { query: { id }, method } = req; diff --git a/src/pages/api/backstage/products/index.ts b/src/pages/api/backstage/products/index.ts index 866d9e7..941278d 100644 --- a/src/pages/api/backstage/products/index.ts +++ b/src/pages/api/backstage/products/index.ts @@ -1,6 +1,6 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { Product } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === 'GET') { diff --git a/src/pages/api/backstage/sales/Records/[id].ts b/src/pages/api/backstage/sales/Records/[id].ts index 9a60565..44719b7 100644 --- a/src/pages/api/backstage/sales/Records/[id].ts +++ b/src/pages/api/backstage/sales/Records/[id].ts @@ -1,7 +1,7 @@ // src/pages/api/backstage/sales/Records/[id].ts import type { NextApiRequest, NextApiResponse } from 'next'; import { SalesRecord } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { const { id } = req.query; // 从请求中获取销售记录的 ID diff --git a/src/pages/api/backstage/sales/Records/index.ts b/src/pages/api/backstage/sales/Records/index.ts index e7b55da..e6292de 100644 --- a/src/pages/api/backstage/sales/Records/index.ts +++ b/src/pages/api/backstage/sales/Records/index.ts @@ -1,7 +1,7 @@ //src\pages\api\backstage\sales\Records\index.ts import type { NextApiRequest, NextApiResponse } from 'next'; import { SalesRecord } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === 'GET') { diff --git a/src/pages/api/backstage/sales/[id].ts b/src/pages/api/backstage/sales/[id].ts index 7b6105f..15afa50 100644 --- a/src/pages/api/backstage/sales/[id].ts +++ b/src/pages/api/backstage/sales/[id].ts @@ -1,6 +1,6 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { SalesRecord } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { const { query: { id }, method } = req; diff --git a/src/pages/api/backstage/sales/aftersale/index.ts b/src/pages/api/backstage/sales/aftersale/index.ts index 6850b3a..e65bcb8 100644 --- a/src/pages/api/backstage/sales/aftersale/index.ts +++ b/src/pages/api/backstage/sales/aftersale/index.ts @@ -1,7 +1,7 @@ // src/pages/api/backstage/sales/aftersale/index.ts import type { NextApiRequest, NextApiResponse } from 'next'; import { AfterSalesRecord, SalesRecord } from '@/models'; // 售后记录模型文件名为 AfterSalesRecord -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; import mongoose from 'mongoose'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { diff --git a/src/pages/api/backstage/sales/aftersale/records/[id].ts b/src/pages/api/backstage/sales/aftersale/records/[id].ts index 21f31ca..a6019c6 100644 --- a/src/pages/api/backstage/sales/aftersale/records/[id].ts +++ b/src/pages/api/backstage/sales/aftersale/records/[id].ts @@ -1,5 +1,5 @@ import { NextApiRequest, NextApiResponse } from 'next'; -import connectDB from '@/utils/connectDB'; // 确保数据库连接 +import connectDB from '@/lib/connectDB'; // 确保数据库连接 import { AfterSalesRecord } from '@/models'; // 导入售后记录模型 //import { IAfterSalesRecord } from '@/models/types'; // 导入售后记录类型定义 import { isValidObjectId } from 'mongoose'; // Mongoose 工具函数 diff --git a/src/pages/api/backstage/sales/aftersale/records/index.ts b/src/pages/api/backstage/sales/aftersale/records/index.ts index 4d0a378..c5742f7 100644 --- a/src/pages/api/backstage/sales/aftersale/records/index.ts +++ b/src/pages/api/backstage/sales/aftersale/records/index.ts @@ -1,7 +1,7 @@ //src/pages/api/backstage/sales/aftersale/records/index.ts import type { NextApiRequest, NextApiResponse } from 'next'; import { AfterSalesRecord } from '@/models'; // 导入售后记录模型 -import connectDB from '@/utils/connectDB'; // 确保数据库已连接 +import connectDB from '@/lib/connectDB'; // 确保数据库已连接 import mongoose from 'mongoose'; interface IQuery { diff --git a/src/pages/api/backstage/sales/aftersale/records/updateProgress.ts b/src/pages/api/backstage/sales/aftersale/records/updateProgress.ts index 1d99124..b0bde81 100644 --- a/src/pages/api/backstage/sales/aftersale/records/updateProgress.ts +++ b/src/pages/api/backstage/sales/aftersale/records/updateProgress.ts @@ -1,6 +1,6 @@ //src\pages\api\backstage\sales\aftersale\records\updateProgress.ts import { NextApiRequest, NextApiResponse } from 'next'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; import { AfterSalesRecord, SalesRecord } from '@/models'; async function handler(req: NextApiRequest, res: NextApiResponse) { diff --git a/src/pages/api/backstage/sales/aftersale/uploadPaymentCode.ts b/src/pages/api/backstage/sales/aftersale/uploadPaymentCode.ts index 71d1543..d8d3a72 100644 --- a/src/pages/api/backstage/sales/aftersale/uploadPaymentCode.ts +++ b/src/pages/api/backstage/sales/aftersale/uploadPaymentCode.ts @@ -1,5 +1,5 @@ import { NextApiRequest, NextApiResponse } from 'next'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; import { CustomerPaymentCode } from '@/models'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { diff --git a/src/pages/api/backstage/sales/index.ts b/src/pages/api/backstage/sales/index.ts index c01dc53..58f7e3c 100644 --- a/src/pages/api/backstage/sales/index.ts +++ b/src/pages/api/backstage/sales/index.ts @@ -1,7 +1,7 @@ //src\pages\api\backstage\sales\index.ts import type { NextApiRequest, NextApiResponse } from 'next'; import { SalesRecord, Customer } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; interface CouponUsage { _id: string; 券码: string; diff --git a/src/pages/api/backstage/sales/updateStatus.ts b/src/pages/api/backstage/sales/updateStatus.ts index db24622..e51c410 100644 --- a/src/pages/api/backstage/sales/updateStatus.ts +++ b/src/pages/api/backstage/sales/updateStatus.ts @@ -4,7 +4,7 @@ * 版本: 1.0.0 */ import { NextApiRequest, NextApiResponse } from 'next'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; import { SalesRecord } from '@/models'; /** diff --git a/src/pages/api/backstage/suppliers/[id].ts b/src/pages/api/backstage/suppliers/[id].ts index 4015ce5..bc6b647 100644 --- a/src/pages/api/backstage/suppliers/[id].ts +++ b/src/pages/api/backstage/suppliers/[id].ts @@ -1,6 +1,6 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { Supplier } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { const { query: { id }, method } = req; diff --git a/src/pages/api/backstage/suppliers/index.ts b/src/pages/api/backstage/suppliers/index.ts index a77e658..f95de7d 100644 --- a/src/pages/api/backstage/suppliers/index.ts +++ b/src/pages/api/backstage/suppliers/index.ts @@ -1,6 +1,6 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { Supplier } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === 'GET') { diff --git a/src/pages/api/backstage/transactions/[id].ts b/src/pages/api/backstage/transactions/[id].ts index f649ed6..7a6fa60 100644 --- a/src/pages/api/backstage/transactions/[id].ts +++ b/src/pages/api/backstage/transactions/[id].ts @@ -1,6 +1,6 @@ //src\pages\api\backstage\transactions\[id].ts import { NextApiRequest, NextApiResponse } from 'next'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; import { Transaction } from '@/models'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { diff --git a/src/pages/api/backstage/transactions/index.ts b/src/pages/api/backstage/transactions/index.ts index 6d0a515..95bc6ee 100644 --- a/src/pages/api/backstage/transactions/index.ts +++ b/src/pages/api/backstage/transactions/index.ts @@ -1,5 +1,5 @@ import { NextApiRequest, NextApiResponse } from 'next'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; import { Transaction, Customer } from '@/models'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { diff --git a/src/pages/api/backstage/users/[id].ts b/src/pages/api/backstage/users/[id].ts index 547f20e..2d8b68d 100644 --- a/src/pages/api/backstage/users/[id].ts +++ b/src/pages/api/backstage/users/[id].ts @@ -1,7 +1,7 @@ //src\pages\api\backstage\users\[id].ts import type { NextApiRequest, NextApiResponse } from 'next'; import { User } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; import { IUser } from '@/models/types'; // 导入 IUser 接口类型 import bcrypt from 'bcryptjs'; diff --git a/src/pages/api/backstage/users/index.ts b/src/pages/api/backstage/users/index.ts index 7adc0ef..11c8093 100644 --- a/src/pages/api/backstage/users/index.ts +++ b/src/pages/api/backstage/users/index.ts @@ -1,7 +1,7 @@ //src\pages\api\backstage\users\index.ts import type { NextApiRequest, NextApiResponse } from 'next'; import { User } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; import bcrypt from 'bcryptjs'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { diff --git a/src/pages/api/login.ts b/src/pages/api/login.ts index c7021f1..290c69f 100644 --- a/src/pages/api/login.ts +++ b/src/pages/api/login.ts @@ -3,7 +3,7 @@ import jwt from 'jsonwebtoken'; import type { NextApiRequest, NextApiResponse } from 'next'; import { User } from '@/models'; import bcrypt from 'bcryptjs'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; import { buildPermissionTree } from './buildPermissionTree'; import type { IPermission } from '@/models/types'; diff --git a/src/pages/api/logistics/batchStatus.ts b/src/pages/api/logistics/batchStatus.ts index df6f6b7..4ccd381 100644 --- a/src/pages/api/logistics/batchStatus.ts +++ b/src/pages/api/logistics/batchStatus.ts @@ -1,5 +1,5 @@ import type { NextApiRequest, NextApiResponse } from 'next'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; import { LogisticsRecord } from '@/models'; const handler = connectDB(async (req: NextApiRequest, res: NextApiResponse) => { diff --git a/src/pages/api/logistics/details.ts b/src/pages/api/logistics/details.ts index a6a0375..65a392e 100644 --- a/src/pages/api/logistics/details.ts +++ b/src/pages/api/logistics/details.ts @@ -1,5 +1,5 @@ import type { NextApiRequest, NextApiResponse } from 'next'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; import { LogisticsRecord } from '@/models'; import mongoose from 'mongoose'; diff --git a/src/pages/api/logistics/status.ts b/src/pages/api/logistics/status.ts index edabf8a..c40d923 100644 --- a/src/pages/api/logistics/status.ts +++ b/src/pages/api/logistics/status.ts @@ -5,7 +5,7 @@ * 版本: v2.0 */ import type { NextApiRequest, NextApiResponse } from 'next'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; import { LogisticsRecord } from '@/models'; import mongoose from 'mongoose'; diff --git a/src/pages/api/paymentCodeImage/[id].ts b/src/pages/api/paymentCodeImage/[id].ts index 905a131..e51feb3 100644 --- a/src/pages/api/paymentCodeImage/[id].ts +++ b/src/pages/api/paymentCodeImage/[id].ts @@ -1,5 +1,5 @@ import { NextApiRequest, NextApiResponse } from 'next'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; import { CustomerPaymentCode } from '@/models'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { diff --git a/src/pages/api/products/batchImages.ts b/src/pages/api/products/batchImages.ts index 3366b4b..b1394b7 100644 --- a/src/pages/api/products/batchImages.ts +++ b/src/pages/api/products/batchImages.ts @@ -1,5 +1,5 @@ import type { NextApiRequest, NextApiResponse } from 'next'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; import { Product } from '@/models'; const handler = connectDB(async (req: NextApiRequest, res: NextApiResponse) => { diff --git a/src/pages/api/products/images/[id].ts b/src/pages/api/products/images/[id].ts index d983d56..41cda8a 100644 --- a/src/pages/api/products/images/[id].ts +++ b/src/pages/api/products/images/[id].ts @@ -5,7 +5,7 @@ * 版本: v2.0 */ import type { NextApiRequest, NextApiResponse } from 'next'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; import { Product } from '@/models'; const handler = connectDB(async (req: NextApiRequest, res: NextApiResponse) => { diff --git a/src/pages/api/roles/[id].ts b/src/pages/api/roles/[id].ts index 32f9a04..1cd5db8 100644 --- a/src/pages/api/roles/[id].ts +++ b/src/pages/api/roles/[id].ts @@ -1,7 +1,7 @@ //src\pages\api\roles\[id].ts import type { NextApiRequest, NextApiResponse } from 'next'; import { Role } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { const { query: { id }, method } = req; diff --git a/src/pages/api/roles/index.ts b/src/pages/api/roles/index.ts index 6f6d3cf..fe4c3f1 100644 --- a/src/pages/api/roles/index.ts +++ b/src/pages/api/roles/index.ts @@ -1,7 +1,7 @@ //src\pages\api\roles\index.ts import type { NextApiRequest, NextApiResponse } from 'next'; import { Role } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; import { buildPermissionTree } from '@/pages/api/buildPermissionTree'; import { IPermission } from '@/models/types'; diff --git a/src/pages/api/script-categories/[id].ts b/src/pages/api/script-categories/[id].ts index 31cfdef..1863543 100644 --- a/src/pages/api/script-categories/[id].ts +++ b/src/pages/api/script-categories/[id].ts @@ -6,7 +6,7 @@ */ import type { NextApiRequest, NextApiResponse } from 'next'; import { ScriptCategory, Script } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; // API处理函数 const handler = async (req: NextApiRequest, res: NextApiResponse) => { diff --git a/src/pages/api/script-categories/index.ts b/src/pages/api/script-categories/index.ts index 1affe23..78464a3 100644 --- a/src/pages/api/script-categories/index.ts +++ b/src/pages/api/script-categories/index.ts @@ -6,7 +6,7 @@ */ import type { NextApiRequest, NextApiResponse } from 'next'; import { ScriptCategory } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; // API处理函数 const handler = async (req: NextApiRequest, res: NextApiResponse) => { diff --git a/src/pages/api/scripts/[id].ts b/src/pages/api/scripts/[id].ts index 54084eb..9da2f13 100644 --- a/src/pages/api/scripts/[id].ts +++ b/src/pages/api/scripts/[id].ts @@ -6,7 +6,7 @@ */ import type { NextApiRequest, NextApiResponse } from 'next'; import { Script, TagInfo } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; // API处理函数 const handler = async (req: NextApiRequest, res: NextApiResponse) => { diff --git a/src/pages/api/scripts/index.ts b/src/pages/api/scripts/index.ts index 8a2634a..3eacf5d 100644 --- a/src/pages/api/scripts/index.ts +++ b/src/pages/api/scripts/index.ts @@ -6,7 +6,7 @@ */ import type { NextApiRequest, NextApiResponse } from 'next'; import { Script, ScriptCategory, TagInfo } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; // API处理函数 const handler = async (req: NextApiRequest, res: NextApiResponse) => { diff --git a/src/pages/api/team/create.ts b/src/pages/api/team/create.ts index b56b1ba..9a20e43 100644 --- a/src/pages/api/team/create.ts +++ b/src/pages/api/team/create.ts @@ -1,6 +1,6 @@ //src\pages\api\team\create.ts import { NextApiRequest, NextApiResponse } from 'next'; -import connectDB from '@/utils/connectDB'; // 确保数据库连接 +import connectDB from '@/lib/connectDB'; // 确保数据库连接 import { Team, User } from '@/models'; // 引入 Team 和 User 模型 import { ITeam } from '@/models/types'; // 引入类型定义 diff --git a/src/pages/api/team/index.ts b/src/pages/api/team/index.ts index 154c20f..239a4f1 100644 --- a/src/pages/api/team/index.ts +++ b/src/pages/api/team/index.ts @@ -1,6 +1,6 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { Team } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === 'GET') { diff --git a/src/pages/api/team/members.ts b/src/pages/api/team/members.ts index 57ecd76..13cfe81 100644 --- a/src/pages/api/team/members.ts +++ b/src/pages/api/team/members.ts @@ -6,7 +6,7 @@ */ import type { NextApiRequest, NextApiResponse } from 'next'; import { User } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; // API响应接口 interface ApiResponse { diff --git a/src/pages/api/test/batch-query-logistics.ts b/src/pages/api/test/batch-query-logistics.ts index b94cda0..990b598 100644 --- a/src/pages/api/test/batch-query-logistics.ts +++ b/src/pages/api/test/batch-query-logistics.ts @@ -6,7 +6,7 @@ */ import type { NextApiRequest, NextApiResponse } from 'next'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; import { LogisticsRecord } from '@/models'; /** diff --git a/src/pages/api/test/logistic-detail.ts b/src/pages/api/test/logistic-detail.ts index f0c2ef8..0eed2d4 100644 --- a/src/pages/api/test/logistic-detail.ts +++ b/src/pages/api/test/logistic-detail.ts @@ -6,7 +6,7 @@ */ import type { NextApiRequest, NextApiResponse } from 'next'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; import { LogisticsRecord } from '@/models'; /** diff --git a/src/pages/api/test/logistics-records.ts b/src/pages/api/test/logistics-records.ts index 47aef2d..9f21cf5 100644 --- a/src/pages/api/test/logistics-records.ts +++ b/src/pages/api/test/logistics-records.ts @@ -6,7 +6,7 @@ */ import type { NextApiRequest, NextApiResponse } from 'next'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; import { LogisticsRecord } from '@/models'; /** diff --git a/src/pages/api/todos/[id].ts b/src/pages/api/todos/[id].ts index 9aa2018..a586d31 100644 --- a/src/pages/api/todos/[id].ts +++ b/src/pages/api/todos/[id].ts @@ -7,7 +7,7 @@ import type { NextApiRequest } from 'next'; import type { NextApiResponseServerIO } from '@/types/socket'; import { Todo } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; // API处理函数 const handler = async (req: NextApiRequest, res: NextApiResponseServerIO) => { diff --git a/src/pages/api/todos/index.ts b/src/pages/api/todos/index.ts index fb99792..b8cb6c1 100644 --- a/src/pages/api/todos/index.ts +++ b/src/pages/api/todos/index.ts @@ -7,7 +7,7 @@ import type { NextApiRequest } from 'next'; import type { NextApiResponseServerIO } from '@/types/socket'; import { Todo } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; // API处理函数 const handler = async (req: NextApiRequest, res: NextApiResponseServerIO) => { diff --git a/src/pages/api/tools/SFExpress/updateLogisticsDetails.ts b/src/pages/api/tools/SFExpress/updateLogisticsDetails.ts index 07c451a..1528897 100644 --- a/src/pages/api/tools/SFExpress/updateLogisticsDetails.ts +++ b/src/pages/api/tools/SFExpress/updateLogisticsDetails.ts @@ -1,6 +1,6 @@ //src\pages/api/tools/SFExpress/updateLogisticsDetails.ts import type { NextApiRequest, NextApiResponse } from 'next'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; import { LogisticsRecord } from '@/models'; import { querySFExpress } from '@/utils/querySFExpress'; diff --git a/src/pages/api/tools/logistics/detail/[id].ts b/src/pages/api/tools/logistics/detail/[id].ts index 1a1ec11..ff1ef6f 100644 --- a/src/pages/api/tools/logistics/detail/[id].ts +++ b/src/pages/api/tools/logistics/detail/[id].ts @@ -1,6 +1,6 @@ // src/pages/api/logistics/detail/[id].ts import type { NextApiRequest, NextApiResponse } from 'next'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; import { LogisticsRecord } from '@/models'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { const { id } = req.query; // 从 URL 中获取订单 ID diff --git a/src/pages/api/tools/logistics/index.ts b/src/pages/api/tools/logistics/index.ts index 230a9f1..6ade79c 100644 --- a/src/pages/api/tools/logistics/index.ts +++ b/src/pages/api/tools/logistics/index.ts @@ -1,5 +1,5 @@ import type { NextApiRequest, NextApiResponse } from 'next'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; import { LogisticsRecord } from '@/models'; const handler = connectDB(async (req: NextApiRequest, res: NextApiResponse) => { diff --git a/src/pages/api/user.ts b/src/pages/api/user.ts index 19cd47d..1ce2dde 100644 --- a/src/pages/api/user.ts +++ b/src/pages/api/user.ts @@ -1,6 +1,6 @@ import { NextApiRequest, NextApiResponse } from 'next'; import { User } from '@/models'; // 用户模型存储在 models 目录中 -import connectDB from '@/utils/connectDB'; // 数据库连接工具 +import connectDB from '@/lib/connectDB'; // 数据库连接工具 import { buildPermissionTree } from './buildPermissionTree'; // 存在的权限树构建工具 export default connectDB(async (req: NextApiRequest, res: NextApiResponse) => { diff --git a/src/pages/api/users/[id].ts b/src/pages/api/users/[id].ts index bbd6aeb..6bfd8c0 100644 --- a/src/pages/api/users/[id].ts +++ b/src/pages/api/users/[id].ts @@ -1,7 +1,7 @@ //src\pages\api\users\[id].ts import type { NextApiRequest, NextApiResponse } from 'next'; import { User } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; import { IUser } from '@/models/types'; // 导入 IUser 接口类型 import bcrypt from 'bcryptjs'; diff --git a/src/pages/api/users/index.ts b/src/pages/api/users/index.ts index 410a38a..02af20a 100644 --- a/src/pages/api/users/index.ts +++ b/src/pages/api/users/index.ts @@ -1,7 +1,7 @@ //src\pages\api\users\index.ts import type { NextApiRequest, NextApiResponse } from 'next'; import { User } from '@/models'; -import connectDB from '@/utils/connectDB'; +import connectDB from '@/lib/connectDB'; import bcrypt from 'bcryptjs'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { diff --git a/src/pages/backstage/accounts/account-modal.tsx b/src/pages/backstage/accounts/account-modal.tsx index 229222c..ddeb720 100644 --- a/src/pages/backstage/accounts/account-modal.tsx +++ b/src/pages/backstage/accounts/account-modal.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { Modal, Form, Input, message, Select } from 'antd'; -import axios from 'axios'; +import { Modal, Form, Input, Select, App } from 'antd'; import { useUserInfo } from '@/store/userStore'; // 使用 Zustand 获取用户信息 import { IAccount, IUser, ICategory } from '@/models/types'; @@ -16,6 +15,7 @@ const AccountModal: React.FC = ({ visible, onOk, onCancel, ac const [users, setUsers] = useState([]); const [categories, setCategories] = useState([]); const userInfo = useUserInfo(); // 获取当前用户信息 + const { message } = App.useApp(); // 使用 useApp hook 获取 message /*useEffect(() => { fetchUsersAndCategories(userInfo.团队?._id); @@ -60,12 +60,21 @@ const AccountModal: React.FC = ({ visible, onOk, onCancel, ac const fetchUsersAndCategories = async (teamId: string) => { try { const [usersResponse, categoriesResponse] = await Promise.all([ - axios.get(`/api/backstage/users?teamId=${teamId}`), - axios.get(`/api/backstage/categories?teamId=${teamId}`) + fetch(`/api/backstage/users?teamId=${teamId}`), + fetch(`/api/backstage/categories?teamId=${teamId}`) ]); - setUsers(usersResponse.data.users); - setCategories(categoriesResponse.data.categories); - } catch (error) { + + if (!usersResponse.ok || !categoriesResponse.ok) { + throw new Error('获取数据失败'); + } + + const usersData = await usersResponse.json(); + const categoriesData = await categoriesResponse.json(); + + setUsers(usersData.users); + setCategories(categoriesData.categories); + } catch (error: unknown) { + console.error('获取用户或品类数据失败:', error); message.error('加载用户或品类数据失败'); } }; @@ -76,18 +85,24 @@ const AccountModal: React.FC = ({ visible, onOk, onCancel, ac const method = account ? 'PUT' : 'POST'; const url = account ? `/api/backstage/accounts/${account._id}` : '/api/backstage/accounts'; - await axios({ + const response = await fetch(url, { method, - url, - data: { + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ ...values, 团队: userInfo.团队?._id, - }, + }), }); + if (!response.ok) { + throw new Error('店铺账号操作失败'); + } + message.success('店铺账号操作成功'); onOk(); - } catch (error) { + } catch (error: unknown) { console.error('Validate Failed:', error); message.error('店铺账号操作失败'); } diff --git a/src/pages/backstage/accounts/index.tsx b/src/pages/backstage/accounts/index.tsx index 0055c32..1e221cf 100644 --- a/src/pages/backstage/accounts/index.tsx +++ b/src/pages/backstage/accounts/index.tsx @@ -1,6 +1,5 @@ import React, { useState, useEffect, useMemo } from 'react'; -import { Table, Button, message, Card, Tag, Typography, Popconfirm } from 'antd'; -import axios from 'axios'; +import { Table, Button, Card, Tag, Typography, Popconfirm, App } from 'antd'; import AccountModal from './account-modal'; // 引入店铺账号模态框组件 import { IAccount, ICategory } from '@/models/types'; // 确保有正确的类型定义 import { useUserInfo } from '@/store/userStore'; // 使用 Zustand 获取用户信息 @@ -15,6 +14,7 @@ const AccountsPage = () => { const [isModalVisible, setIsModalVisible] = useState(false); const [currentAccount, setCurrentAccount] = useState(null); const userInfo = useUserInfo(); // 获取当前用户信息 + const { message } = App.useApp(); // 使用 useApp hook 获取 message useEffect(() => { if (userInfo.团队?._id) { @@ -24,9 +24,16 @@ const AccountsPage = () => { const fetchAccounts = async (teamId: string) => { try { - const { data } = await axios.get(`/api/backstage/accounts?teamId=${teamId}`); + const response = await fetch(`/api/backstage/accounts?teamId=${teamId}`); + + if (!response.ok) { + throw new Error('获取店铺账号数据失败'); + } + + const data = await response.json(); setAccounts(data.accounts); - } catch (error) { + } catch (error: unknown) { + console.error('加载店铺账号数据失败:', error); message.error('加载店铺账号数据失败'); } }; @@ -45,12 +52,20 @@ const AccountsPage = () => { const handleDelete = async (id: string) => { try { - await axios.delete(`/api/backstage/accounts/${id}`); + const response = await fetch(`/api/backstage/accounts/${id}`, { + method: 'DELETE', + }); + + if (!response.ok) { + throw new Error('删除店铺账号失败'); + } + if (userInfo.团队?._id) { fetchAccounts(userInfo.团队._id); } message.success('店铺账号删除成功'); - } catch (error) { + } catch (error: unknown) { + console.error('删除店铺账号失败:', error); message.error('删除店铺账号失败'); } }; diff --git a/src/pages/team/AfterSaleRecord/index.tsx b/src/pages/team/AfterSaleRecord/index.tsx index 54bd2f2..1f42545 100644 --- a/src/pages/team/AfterSaleRecord/index.tsx +++ b/src/pages/team/AfterSaleRecord/index.tsx @@ -54,8 +54,8 @@ const buildCustomerAddress = (address: any): string => { }; // 导出Excel相关库 -//import * as XLSX from 'xlsx'; -//import { saveAs } from 'file-saver'; +import * as XLSX from 'xlsx'; +import { saveAs } from 'file-saver'; const AfterSaleRecordPage = () => { const { message } = App.useApp(); // 使用 App.useApp 获取 message 实例 @@ -662,7 +662,6 @@ const AfterSaleRecordPage = () => { }; // 导出Excel的函数 - /* const exportToExcel = () => { if (filteredRecords.length === 0) { message.warning('没有可导出的数据'); @@ -737,9 +736,9 @@ const AfterSaleRecordPage = () => { } }); // 创建工作簿和工作表 - //const ws = XLSX.utils.json_to_sheet(exportData); - //const wb = XLSX.utils.book_new(); - //XLSX.utils.book_append_sheet(wb, ws, '售后记录'); + const ws = XLSX.utils.json_to_sheet(exportData); + const wb = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(wb, ws, '售后记录'); // 设置列宽 (大致估计,可能需要调整) const colWidths = [ @@ -763,7 +762,7 @@ const AfterSaleRecordPage = () => { { wch: 10 }, // 待收 { wch: 20 }, // 备注 ]; - //ws['!cols'] = colWidths; + ws['!cols'] = colWidths; // 生成Excel文件并下载 const now = new Date(); @@ -781,7 +780,6 @@ const AfterSaleRecordPage = () => { message.error({ content: '导出失败,请重试', key: 'exporting' }); } }; - */ return ( @@ -862,7 +860,7 @@ const AfterSaleRecordPage = () => { size="small" type="primary" icon={} - //onClick={exportToExcel} + onClick={exportToExcel} style={{ marginLeft: 8 }} > 导出筛选 diff --git a/src/pages/team/SaleRecord/components/CustomerAnalysisModal.tsx b/src/pages/team/SaleRecord/components/CustomerAnalysisModal.tsx new file mode 100644 index 0000000..24d3f96 --- /dev/null +++ b/src/pages/team/SaleRecord/components/CustomerAnalysisModal.tsx @@ -0,0 +1,766 @@ +/** + * 客户分析模态框组件 + * 作者: 阿瑞 + * 功能: 展示团队客户列表和分析数据 + * 版本: v1.0.0 + */ + +import React, { useState, useEffect, useMemo, useRef } from 'react'; +import { Modal, Table, message, Spin, Tabs, Card, Row, Col, DatePicker, Button, Space, Divider, Statistic } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { FilterOutlined, ReloadOutlined, CalendarOutlined, UserOutlined, EnvironmentOutlined } from '@ant-design/icons'; +import ReactECharts from 'echarts-for-react'; +import * as echarts from 'echarts'; +import dayjs from 'dayjs'; + +const { RangePicker } = DatePicker; + +// 客户数据接口定义 +interface ICustomer { + _id: string; + 姓名: string; + 电话: string; + 地址: { + 省份: string; + 城市: string; + 区县: string; + 详细地址: string; + }; + 加粉日期: string; + createdAt: string; + updatedAt: string; +} + +// 组件Props接口定义 +interface CustomerAnalysisModalProps { + visible: boolean; + onCancel: () => void; + teamId: string; +} + +/** + * 客户分析模态框组件 + * 展示客户列表和基础分析信息 + */ +const CustomerAnalysisModal: React.FC = ({ + visible, + onCancel, + teamId +}) => { + // 状态管理 + const [customers, setCustomers] = useState([]); + const [loading, setLoading] = useState(false); + + // 筛选状态管理 + const [filters, setFilters] = useState({ + createTimeRange: null as [dayjs.Dayjs, dayjs.Dayjs] | null, + addTimeRange: null as [dayjs.Dayjs, dayjs.Dayjs] | null, + }); + + // ECharts实例引用 + const chartRef = useRef(null); + // 记录是否已经初始化过尺寸 + const [chartInitialized, setChartInitialized] = useState(false); + + /** + * 获取客户数据 + * 从API获取团队的所有客户信息 + */ + const fetchCustomers = async () => { + if (!teamId) return; + + setLoading(true); + try { + const response = await fetch(`/api/backstage/customers?teamId=${teamId}`); + const data = await response.json(); + + if (response.ok) { + setCustomers(data.customers || []); + } else { + message.error('获取客户数据失败'); + } + } catch (error) { + console.error('获取客户数据出错:', error); + message.error('获取客户数据出错'); + } finally { + setLoading(false); + } + }; + + /** + * 格式化地址显示 + * 将地址对象转换为完整的地址字符串 + */ + const formatAddress = (address: ICustomer['地址']): string => { + if (!address) return '-'; + const { 省份, 城市, 区县, 详细地址 } = address; + return [省份, 城市, 区县, 详细地址].filter(Boolean).join(' '); + }; + + /** + * 格式化日期显示 + * 将ISO日期字符串格式化为可读格式 + */ + const formatDate = (dateString: string): string => { + if (!dateString) return '-'; + return new Date(dateString).toLocaleDateString('zh-CN'); + }; + + /** + * 筛选条件处理函数 + * 管理各种筛选条件的变更 + */ + const handleFilterChange = { + // 创建时间范围筛选 + createTimeRange: (dates: any) => { + setFilters(prev => ({ ...prev, createTimeRange: dates })); + }, + // 加粉日期范围筛选 + addTimeRange: (dates: any) => { + setFilters(prev => ({ ...prev, addTimeRange: dates })); + }, + // 重置所有筛选 + reset: () => { + setFilters({ + createTimeRange: null, + addTimeRange: null, + }); + } + }; + + /** + * 根据筛选条件过滤客户数据 + * 应用时间范围筛选逻辑 + */ + const filteredCustomers = useMemo(() => { + let filtered = [...customers]; + + // 筛选创建时间范围 + if (filters.createTimeRange) { + const [start, end] = filters.createTimeRange; + filtered = filtered.filter(customer => { + const createTime = dayjs(customer.createdAt); + return createTime.isAfter(start.startOf('day')) && createTime.isBefore(end.endOf('day')); + }); + } + + // 筛选加粉日期范围 + if (filters.addTimeRange) { + const [start, end] = filters.addTimeRange; + filtered = filtered.filter(customer => { + if (!customer.加粉日期) return false; + const addTime = dayjs(customer.加粉日期); + return addTime.isAfter(start.startOf('day')) && addTime.isBefore(end.endOf('day')); + }); + } + + return filtered; + }, [customers, filters]); + + /** + * 统计客户地区分布数据 + * 按省份统计客户数量,用于地图展示 + */ + const customerLocationStats = useMemo(() => { + const stats: { [key: string]: number } = {}; + const details: { [key: string]: ICustomer[] } = {}; + + filteredCustomers.forEach(customer => { + const province = customer.地址?.省份; + if (province) { + // 统计数量 + stats[province] = (stats[province] || 0) + 1; + // 保存详细信息 + if (!details[province]) { + details[province] = []; + } + details[province].push(customer); + } + }); + + // 转换为echarts需要的数据格式 + const mapData = Object.entries(stats).map(([name, value]) => ({ + name, + value, + customers: details[name] + })); + + return { + stats, + mapData, + details + }; + }, [filteredCustomers]); + + /** + * 地区分布图表配置 + * 使用柱状图展示客户地区分布 + */ + const getChartOption = () => { + const { mapData } = customerLocationStats; + + // 按客户数量排序,显示前10个地区 + const sortedData = mapData + .sort((a, b) => b.value - a.value) + .slice(0, 10); + + return { + title: { + text: '客户地区分布TOP10', + left: 'center', + textStyle: { + fontSize: 16, + fontWeight: 'bold' + } + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow' + }, + formatter: (params: any) => { + const data = params[0]; + return `${data.name}
客户数量: ${data.value}人`; + } + }, + grid: { + left: '3%', + right: '4%', + bottom: '3%', + containLabel: true + }, + xAxis: { + type: 'category', + data: sortedData.map(item => item.name), + axisTick: { + alignWithLabel: true + }, + axisLabel: { + rotate: 45, + fontSize: 12 + } + }, + yAxis: { + type: 'value', + name: '客户数量(人)', + nameTextStyle: { + fontSize: 12 + } + }, + series: [ + { + name: '客户数量', + type: 'bar', + data: sortedData.map(item => ({ + value: item.value, + itemStyle: { + color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ + { offset: 0, color: '#83bff6' }, + { offset: 0.5, color: '#188df0' }, + { offset: 1, color: '#188df0' } + ]) + } + })), + emphasis: { + focus: 'series' + }, + animationDelay: (idx: number) => idx * 80, + animationDuration: 800, + animationEasing: 'cubicOut' + } + ], + // 全局动画配置 + animation: true, + animationDuration: 800, + animationEasing: 'cubicOut', + animationDelayUpdate: (idx: number) => idx * 30 + }; + }; + + // 表格列配置 + const columns: ColumnsType = [ + { + title: '姓名', + dataIndex: '姓名', + key: '姓名', + width: 120, + fixed: 'left', + }, + { + title: '电话', + dataIndex: '电话', + key: '电话', + width: 140, + }, + { + title: '地址', + dataIndex: '地址', + key: '地址', + width: 300, + render: (address: ICustomer['地址']) => formatAddress(address), + }, + { + title: '加粉日期', + dataIndex: '加粉日期', + key: '加粉日期', + width: 120, + render: (date: string) => formatDate(date), + sorter: (a, b) => new Date(a.加粉日期).getTime() - new Date(b.加粉日期).getTime(), + }, + { + title: '创建时间', + dataIndex: 'createdAt', + key: 'createdAt', + width: 150, + render: (date: string) => formatDate(date), + sorter: (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(), + defaultSortOrder: 'descend', + }, + ]; + + // 监听模态框打开事件,获取客户数据 + useEffect(() => { + if (visible && teamId) { + fetchCustomers(); + } + }, [visible, teamId]); + + // 处理图表容器尺寸问题 - 仅在Modal首次打开时执行 + useEffect(() => { + if (visible && !chartInitialized) { + // Modal完全显示后,延迟触发图表重新调整大小 + const timer = setTimeout(() => { + if (chartRef.current) { + const echartsInstance = chartRef.current.getEchartsInstance(); + if (echartsInstance) { + echartsInstance.resize(); + setChartInitialized(true); + } + } + }, 300); + + return () => clearTimeout(timer); + } + }, [visible, chartInitialized]); + + // 重置初始化状态,当Modal关闭时 + useEffect(() => { + if (!visible) { + setChartInitialized(false); + } + }, [visible]); + + // 创建筛选工具栏组件 + const FilterToolbar = () => ( + + + + + + 数据筛选 + + + + + + + + 创建时间 + + + + + + 加粉日期 + + + + + + + +
+ {filters.createTimeRange || filters.addTimeRange ? ( +
+
+ 🔍 已筛选 +
+
+ 显示 {filteredCustomers.length} / {customers.length} 条记录 +
+
+ ) : ( +
+ 更新时间:{new Date().toLocaleString('zh-CN')} +
+ )} +
+ +
+
+ ); + + // 创建数据统计卡片组件 + const StatisticsCards = () => ( + + + + 客户总数} + value={filteredCustomers.length} + suffix={} + valueStyle={{ color: '#fff', fontSize: '28px', fontWeight: 'bold' }} + prefix={} + /> + + + + + 覆盖省份} + value={Object.keys(customerLocationStats.stats).length} + suffix={} + valueStyle={{ color: '#fff', fontSize: '28px', fontWeight: 'bold' }} + prefix={} + /> + + + + + 最新加粉} + value={ + filteredCustomers.length > 0 + ? dayjs(Math.max(...filteredCustomers.map(c => new Date(c.加粉日期 || c.createdAt).getTime()))).format('MM-DD') + : '-' + } + valueStyle={{ color: '#fff', fontSize: '24px', fontWeight: 'bold' }} + prefix={} + /> + + + + ); + + // 创建Tabs标签页数据 + const tabItems = [ + { + key: 'overview', + label: ( + + + 数据概览 + + ), + children: ( +
+ + + + + + 📊 客户地区分布图表 + + } + size="small" + style={{ + height: '480px', + borderRadius: '8px', + boxShadow: '0 2px 8px rgba(0,0,0,0.1)' + }} + bodyStyle={{ padding: '16px' }} + > + { + // 图表初始化完成后,确保尺寸正确(仅在未初始化时) + if (!chartInitialized) { + setTimeout(() => { + if (chartRef.current) { + const echartsInstance = chartRef.current.getEchartsInstance(); + if (echartsInstance) { + echartsInstance.resize(); + setChartInitialized(true); + } + } + }, 100); + } + }} + /> + + + + + 📋 地区统计详情 + + } + size="small" + style={{ + height: '480px', + borderRadius: '8px', + boxShadow: '0 2px 8px rgba(0,0,0,0.1)' + }} + bodyStyle={{ padding: '12px' }} + > +
+ {Object.entries(customerLocationStats.stats) + .sort(([,a], [,b]) => b - a) + .map(([province, count], index) => ( +
{ + e.currentTarget.style.background = '#e6f7ff'; + e.currentTarget.style.transform = 'translateX(2px)'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.background = index % 2 === 0 ? '#fafafa' : '#fff'; + e.currentTarget.style.transform = 'translateX(0)'; + }} + > + + {province} + + + {count} + +
+ )) + } + {Object.keys(customerLocationStats.stats).length === 0 && ( +
+ + 暂无地区数据 +
+ )} +
+
+ +
+
+ ), + }, + { + key: 'list', + label: ( + + + 客户列表 + + ), + children: ( +
+
+ + + + 📋 客户详细列表 + + + + + 显示 {filteredCustomers.length} 条记录 + {(filters.createTimeRange || filters.addTimeRange) && ( + + (已筛选) + + )} + + + +
+ + `第 ${range[0]}-${range[1]} 条,共 ${total} 条记录`, + size: 'small' + }} + // 行样式优化 + rowClassName={(_, index) => (index % 2 === 0 ? 'even-row' : 'odd-row')} + style={{ + border: '1px solid #e9ecef', + borderRadius: '8px', + overflow: 'hidden' + }} + /> + + ), + }, + ]; + + return ( + + 📊 成交客户数据分析中心 + + } + open={visible} + onCancel={onCancel} + footer={null} + width={1400} + centered + destroyOnClose + style={{ top: 20 }} + bodyStyle={{ + padding: '20px', + background: '#fafafa', + maxHeight: '85vh', + overflow: 'auto' + }} + > + + { + // Tab切换时,如果切换到overview且未初始化,重新调整图表大小 + if (activeKey === 'overview' && !chartInitialized) { + setTimeout(() => { + if (chartRef.current) { + const echartsInstance = chartRef.current.getEchartsInstance(); + if (echartsInstance) { + echartsInstance.resize(); + setChartInitialized(true); + } + } + }, 100); + } + }} + /> + + + ); +}; + +export default CustomerAnalysisModal; \ No newline at end of file diff --git a/src/pages/team/SaleRecord/components/SalesAnalysisModal.tsx b/src/pages/team/SaleRecord/components/SalesAnalysisModal.tsx new file mode 100644 index 0000000..a3bbed6 --- /dev/null +++ b/src/pages/team/SaleRecord/components/SalesAnalysisModal.tsx @@ -0,0 +1,1077 @@ +/** + * 成交分析模态框组件 + * 作者: 阿瑞 + * 功能: 展示销售数据的多维度分析 + * 版本: v1.0.0 + */ + +import React, { useState, useEffect, useMemo, useRef } from 'react'; +import { Modal, Tabs, Card, Row, Col, DatePicker, Button, Space, Divider, Statistic, Select, Table } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { + FilterOutlined, + ReloadOutlined, + CalendarOutlined, + DollarOutlined, + UserOutlined, + ShoppingOutlined, + TrophyOutlined, + PieChartOutlined +} from '@ant-design/icons'; +import ReactECharts from 'echarts-for-react'; +import * as echarts from 'echarts'; +import dayjs from 'dayjs'; +import { ISalesRecord } from '@/models/types'; + +const { RangePicker } = DatePicker; +const { Option } = Select; + +// 组件Props接口定义 +interface SalesAnalysisModalProps { + visible: boolean; + onCancel: () => void; + salesRecords: ISalesRecord[]; +} + +// 导购业绩统计接口 +interface ISalesmanStats { + 导购姓名: string; + 成交数量: number; + 成交金额: number; + 收款金额: number; + 待收金额: number; + 平均客单价: number; + 成交率?: number; +} + +// 产品销售统计接口(按品牌+品类) +interface IProductStats { + 品牌品类: string; + 品牌名称: string; + 品类名称: string; + 销售数量: number; + 销售金额: number; + 占比: number; +} + +/** + * 成交分析模态框组件 + * 基于销售数据提供全面的分析视角 + */ +const SalesAnalysisModal: React.FC = ({ + visible, + onCancel, + salesRecords +}) => { + + // 筛选状态管理 + const [filters, setFilters] = useState({ + dateRange: null as [dayjs.Dayjs, dayjs.Dayjs] | null, + salesman: 'all', + paymentStatus: 'all', + }); + + // 图表实例引用 + const trendChartRef = useRef(null); + const salesmanChartRef = useRef(null); + const productChartRef = useRef(null); + + /** + * 筛选条件处理函数 + * 管理各种筛选条件的变更 + */ + const handleFilterChange = { + // 日期范围筛选 + dateRange: (dates: any) => { + setFilters(prev => ({ ...prev, dateRange: dates })); + }, + // 导购筛选 + salesman: (value: string) => { + setFilters(prev => ({ ...prev, salesman: value })); + }, + // 收款状态筛选 + paymentStatus: (value: string) => { + setFilters(prev => ({ ...prev, paymentStatus: value })); + }, + // 重置所有筛选 + reset: () => { + setFilters({ + dateRange: null, + salesman: 'all', + paymentStatus: 'all', + }); + } + }; + + /** + * 根据筛选条件过滤销售数据 + * 应用各种筛选逻辑 + */ + const filteredSalesRecords = useMemo(() => { + let filtered = [...salesRecords]; + + // 筛选日期范围 + if (filters.dateRange) { + const [start, end] = filters.dateRange; + filtered = filtered.filter(record => { + const recordDate = dayjs(record.成交日期); + return recordDate.isAfter(start.startOf('day')) && recordDate.isBefore(end.endOf('day')); + }); + } + + // 筛选导购 + if (filters.salesman !== 'all') { + filtered = filtered.filter(record => record.导购?.姓名 === filters.salesman); + } + + // 筛选收款状态 + if (filters.paymentStatus !== 'all') { + filtered = filtered.filter(record => record.收款状态 === filters.paymentStatus); + } + + return filtered; + }, [salesRecords, filters]); + + /** + * 计算基础统计数据 + * 核心业务指标计算 + */ + const basicStats = useMemo(() => { + const records = filteredSalesRecords; + + const totalRecords = records.length; + const totalAmount = records.reduce((sum, record) => sum + (record.应收金额 || 0), 0); + const receivedAmount = records.reduce((sum, record) => sum + (record.收款金额 || 0), 0); + const pendingAmount = records.reduce((sum, record) => sum + (record.待收款 || 0), 0); + const avgOrderValue = totalRecords > 0 ? totalAmount / totalRecords : 0; + const paymentRate = totalAmount > 0 ? (receivedAmount / totalAmount) * 100 : 0; + + // 今日数据 + const today = dayjs(); + const todayRecords = records.filter(record => + dayjs(record.成交日期).isSame(today, 'day') + ); + const todayAmount = todayRecords.reduce((sum, record) => sum + (record.应收金额 || 0), 0); + + return { + totalRecords, + totalAmount, + receivedAmount, + pendingAmount, + avgOrderValue, + paymentRate, + todayRecords: todayRecords.length, + todayAmount + }; + }, [filteredSalesRecords]); + + /** + * 成交趋势分析数据 + * 按日期统计成交数量和金额 + */ + const trendAnalysis = useMemo(() => { + const records = filteredSalesRecords; + const trendMap = new Map(); + + records.forEach(record => { + const dateKey = dayjs(record.成交日期).format('YYYY-MM-DD'); + const existing = trendMap.get(dateKey) || { count: 0, amount: 0 }; + trendMap.set(dateKey, { + count: existing.count + 1, + amount: existing.amount + (record.应收金额 || 0) + }); + }); + + const dates = Array.from(trendMap.keys()).sort(); + const counts = dates.map(date => trendMap.get(date)!.count); + const amounts = dates.map(date => trendMap.get(date)!.amount); + + return { dates, counts, amounts }; + }, [filteredSalesRecords]); + + /** + * 导购业绩分析数据 + * 统计各导购的业绩表现 + */ + const salesmanAnalysis = useMemo(() => { + const records = filteredSalesRecords; + const salesmanMap = new Map(); + + records.forEach(record => { + const salesmanName = record.导购?.姓名 || '未知导购'; + const existing = salesmanMap.get(salesmanName) || { + 导购姓名: salesmanName, + 成交数量: 0, + 成交金额: 0, + 收款金额: 0, + 待收金额: 0, + 平均客单价: 0 + }; + + salesmanMap.set(salesmanName, { + 导购姓名: salesmanName, + 成交数量: existing.成交数量 + 1, + 成交金额: existing.成交金额 + (record.应收金额 || 0), + 收款金额: existing.收款金额 + (record.收款金额 || 0), + 待收金额: existing.待收金额 + (record.待收款 || 0), + 平均客单价: 0 // 最后计算 + }); + }); + + // 计算平均客单价并转换为数组 + const result = Array.from(salesmanMap.values()).map(stats => ({ + ...stats, + 平均客单价: stats.成交数量 > 0 ? stats.成交金额 / stats.成交数量 : 0 + })).sort((a, b) => b.成交金额 - a.成交金额); + + return result; + }, [filteredSalesRecords]); + + /** + * 产品销售分析数据(按品牌+品类) + * 统计各品牌品类组合的销售情况 + */ + const productAnalysis = useMemo(() => { + const records = filteredSalesRecords; + const productMap = new Map(); + const totalAmount = records.reduce((sum, record) => sum + (record.应收金额 || 0), 0); + + records.forEach(record => { + record.产品?.forEach(product => { + const brandName = product.品牌?.name || '未知品牌'; + const categoryName = product.品类?.name || '未知品类'; + const brandCategoryKey = `${brandName} ${categoryName}`; + + const existing = productMap.get(brandCategoryKey) || { + 品牌品类: brandCategoryKey, + 品牌名称: brandName, + 品类名称: categoryName, + 销售数量: 0, + 销售金额: 0, + 占比: 0 + }; + + // 这里简化处理,假设一个产品在订单中的金额是平均分配的 + const productAmount = record.产品.length > 0 ? (record.应收金额 || 0) / record.产品.length : 0; + + productMap.set(brandCategoryKey, { + 品牌品类: brandCategoryKey, + 品牌名称: brandName, + 品类名称: categoryName, + 销售数量: existing.销售数量 + 1, + 销售金额: existing.销售金额 + productAmount, + 占比: 0 // 最后计算 + }); + }); + }); + + // 计算占比并转换为数组 + const result = Array.from(productMap.values()).map(stats => ({ + ...stats, + 占比: totalAmount > 0 ? (stats.销售金额 / totalAmount) * 100 : 0 + })).sort((a, b) => b.销售金额 - a.销售金额).slice(0, 10); // 取前10名 + + return result; + }, [filteredSalesRecords]); + + /** + * 成交趋势图表配置 + */ + const getTrendChartOption = () => { + const { dates, counts, amounts } = trendAnalysis; + + return { + title: { + text: '成交趋势分析', + left: 'center', + textStyle: { fontSize: 16, fontWeight: 'bold' } + }, + tooltip: { + trigger: 'axis', + axisPointer: { type: 'cross' } + }, + legend: { + data: ['成交数量', '成交金额'], + top: 30 + }, + grid: { + left: '3%', + right: '4%', + bottom: '3%', + containLabel: true, + top: 60 + }, + xAxis: { + type: 'category', + data: dates, + axisLabel: { + rotate: 45, + fontSize: 10 + } + }, + yAxis: [ + { + type: 'value', + name: '成交数量', + position: 'left', + nameTextStyle: { fontSize: 12 } + }, + { + type: 'value', + name: '成交金额(元)', + position: 'right', + nameTextStyle: { fontSize: 12 } + } + ], + series: [ + { + name: '成交数量', + type: 'bar', + data: counts, + itemStyle: { + color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ + { offset: 0, color: '#91d5ff' }, + { offset: 1, color: '#1890ff' } + ]) + } + }, + { + name: '成交金额', + type: 'line', + yAxisIndex: 1, + data: amounts, + smooth: true, + itemStyle: { color: '#52c41a' }, + areaStyle: { + color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ + { offset: 0, color: 'rgba(82, 196, 26, 0.3)' }, + { offset: 1, color: 'rgba(82, 196, 26, 0.1)' } + ]) + } + } + ], + animation: true, + animationDuration: 800, + animationEasing: 'cubicOut' + }; + }; + + /** + * 导购业绩图表配置 + */ + const getSalesmanChartOption = () => { + const top5Salesmen = salesmanAnalysis.slice(0, 5); + + return { + title: { + text: '导购业绩TOP5', + left: 'center', + textStyle: { fontSize: 16, fontWeight: 'bold' } + }, + tooltip: { + trigger: 'axis', + axisPointer: { type: 'shadow' } + }, + grid: { + left: '3%', + right: '4%', + bottom: '3%', + containLabel: true, + top: 40 + }, + xAxis: { + type: 'category', + data: top5Salesmen.map(item => item.导购姓名), + axisLabel: { fontSize: 12 } + }, + yAxis: { + type: 'value', + name: '成交金额(元)', + nameTextStyle: { fontSize: 12 } + }, + series: [ + { + name: '成交金额', + type: 'bar', + data: top5Salesmen.map(item => item.成交金额), + itemStyle: { + color: (params: any) => { + const colors = ['#ff4d4f', '#ff7a45', '#ffa940', '#ffec3d', '#73d13d']; + return colors[params.dataIndex] || '#1890ff'; + } + }, + emphasis: { focus: 'series' }, + animationDelay: (idx: number) => idx * 100 + } + ], + animation: true, + animationDuration: 800, + animationEasing: 'cubicOut' + }; + }; + + /** + * 产品销售饼图配置(按品牌+品类) + */ + const getProductChartOption = () => { + const top8Products = productAnalysis.slice(0, 8); + + return { + title: { + text: '品牌品类销售占比TOP8', + left: 'center', + textStyle: { fontSize: 16, fontWeight: 'bold' } + }, + tooltip: { + trigger: 'item', + formatter: function(params: any) { + const data = top8Products[params.dataIndex]; + return `${params.seriesName}
${params.name}
销售金额: ¥${params.value.toFixed(2)}
占比: ${params.percent}%
销售数量: ${data.销售数量}件`; + } + }, + legend: { + orient: 'vertical', + left: 'left', + top: 40, + textStyle: { fontSize: 11 }, + formatter: function(name: string) { + // 如果名称过长,进行截断 + return name.length > 12 ? name.substring(0, 12) + '...' : name; + } + }, + series: [ + { + name: '销售金额', + type: 'pie', + radius: ['40%', '70%'], + center: ['60%', '55%'], + avoidLabelOverlap: false, + itemStyle: { + borderRadius: 10, + borderColor: '#fff', + borderWidth: 2 + }, + label: { + show: false, + position: 'center' + }, + emphasis: { + label: { + show: true, + fontSize: 14, + fontWeight: 'bold', + formatter: function(params: any) { + const data = top8Products[params.dataIndex]; + return `${params.name}\n¥${params.value.toFixed(0)}\n${data.销售数量}件`; + } + } + }, + labelLine: { show: false }, + data: top8Products.map(item => ({ + value: item.销售金额, + name: item.品牌品类 + })), + animationType: 'scale', + animationEasing: 'elasticOut', + animationDelay: () => Math.random() * 200 + } + ] + }; + }; + + /** + * 导购业绩表格列配置 + */ + const salesmanColumns: ColumnsType = [ + { + title: '排名', + key: 'rank', + width: 60, + align: 'center', + render: (_, __, index) => ( + + {index + 1} + + ) + }, + { + title: '导购姓名', + dataIndex: '导购姓名', + key: '导购姓名', + width: 100 + }, + { + title: '成交数量', + dataIndex: '成交数量', + key: '成交数量', + width: 90, + align: 'center', + sorter: (a, b) => a.成交数量 - b.成交数量 + }, + { + title: '成交金额', + dataIndex: '成交金额', + key: '成交金额', + width: 120, + align: 'right', + render: (value: number) => `¥${value.toFixed(2)}`, + sorter: (a, b) => a.成交金额 - b.成交金额 + }, + { + title: '收款金额', + dataIndex: '收款金额', + key: '收款金额', + width: 120, + align: 'right', + render: (value: number) => `¥${value.toFixed(2)}`, + sorter: (a, b) => a.收款金额 - b.收款金额 + }, + { + title: '平均客单价', + dataIndex: '平均客单价', + key: '平均客单价', + width: 120, + align: 'right', + render: (value: number) => `¥${value.toFixed(2)}`, + sorter: (a, b) => a.平均客单价 - b.平均客单价 + } + ]; + + // 处理图表初始化 + useEffect(() => { + if (visible) { + const timer = setTimeout(() => { + [trendChartRef, salesmanChartRef, productChartRef].forEach(ref => { + if (ref.current) { + const echartsInstance = ref.current.getEchartsInstance(); + if (echartsInstance) { + echartsInstance.resize(); + } + } + }); + }, 300); + + return () => clearTimeout(timer); + } + }, [visible]); + + // 获取所有导购名单 + const allSalesmen = useMemo(() => { + const salesmenSet = new Set(); + salesRecords.forEach(record => { + if (record.导购?.姓名) { + salesmenSet.add(record.导购.姓名); + } + }); + return Array.from(salesmenSet); + }, [salesRecords]); + + return ( + + 📊 成交数据分析中心 + + } + open={visible} + onCancel={onCancel} + footer={null} + width={1600} + centered + destroyOnHidden + //style={{ top: 0 }} + styles={{ + body: { + padding: '24px', + background: '#fafafa', + maxHeight: '85vh', + overflow: 'auto' + } + }} + > +
+ {/* 筛选工具栏 */} + + +
+ + + 数据筛选 + + + + + + + + 成交日期 + + + + + + 导购 + + + + + + 收款状态 + + + + + + + +
+ {filters.dateRange || filters.salesman !== 'all' || filters.paymentStatus !== 'all' ? ( +
+
+ 🔍 已筛选 +
+
+ 显示 {filteredSalesRecords.length} / {salesRecords.length} 条记录 +
+
+ ) : ( +
+ 更新时间:{new Date().toLocaleString('zh-CN')} +
+ )} +
+ + + + + {/* 统计卡片 */} + + + + 成交总数} + value={basicStats.totalRecords} + suffix={} + valueStyle={{ color: '#fff', fontSize: '28px', fontWeight: 'bold' }} + prefix={} + /> + + + + + 成交金额} + value={basicStats.totalAmount} + precision={0} + suffix={} + valueStyle={{ color: '#fff', fontSize: '28px', fontWeight: 'bold' }} + prefix={} + /> + + + + + 平均客单价} + value={basicStats.avgOrderValue} + precision={2} + suffix={} + valueStyle={{ color: '#fff', fontSize: '24px', fontWeight: 'bold' }} + prefix={} + /> + + + + + 全款 收款率} + value={basicStats.paymentRate} + precision={1} + suffix={%} + valueStyle={{ + color: basicStats.paymentRate >= 80 ? '#52c41a' : basicStats.paymentRate >= 60 ? '#faad14' : '#ff4d4f', + fontSize: '24px', + fontWeight: 'bold' + }} + prefix={} + /> + + + + + {/* Tabs 内容区域 */} + { + // Tab切换时,重新调整图表大小 + setTimeout(() => { + [trendChartRef, salesmanChartRef, productChartRef].forEach(ref => { + if (ref.current) { + const echartsInstance = ref.current.getEchartsInstance(); + if (echartsInstance) { + echartsInstance.resize(); + } + } + }); + }, 100); + }} + items={[ + { + key: 'overview', + label: ( + + + 数据概览 + + ), + children: ( + + + + 📈 成交趋势分析 + + } + size="small" + style={{ + height: '400px', + borderRadius: '8px', + boxShadow: '0 2px 8px rgba(0,0,0,0.1)', + marginBottom: '16px' + }} + bodyStyle={{ padding: '16px' }} + > + + + + + ), + }, + { + key: 'salesman', + label: ( + + + 导购分析 + + ), + children: ( + + + + 🏆 导购业绩排行 + + } + size="small" + style={{ + height: '500px', + borderRadius: '8px', + boxShadow: '0 2px 8px rgba(0,0,0,0.1)' + }} + bodyStyle={{ padding: '16px' }} + > + + + + + + 📊 导购业绩详情 + + } + size="small" + style={{ + height: '500px', + borderRadius: '8px', + boxShadow: '0 2px 8px rgba(0,0,0,0.1)' + }} + bodyStyle={{ padding: '12px' }} + > +
+ + + + ), + }, + { + key: 'product', + label: ( + + + 品牌品类分析 + + ), + children: ( + + + + 🛍️ 品牌品类销售占比图 + + } + size="small" + style={{ + height: '500px', + borderRadius: '8px', + boxShadow: '0 2px 8px rgba(0,0,0,0.1)' + }} + bodyStyle={{ padding: '16px' }} + > + + + + + + 📊 品牌品类销售详情 + + } + size="small" + style={{ + height: '500px', + borderRadius: '8px', + boxShadow: '0 2px 8px rgba(0,0,0,0.1)' + }} + bodyStyle={{ padding: '12px' }} + > +
( + + {index + 1} + + ) + }, + { + title: '品牌', + dataIndex: '品牌名称', + key: '品牌名称', + width: 80, + ellipsis: true + }, + { + title: '品类', + dataIndex: '品类名称', + key: '品类名称', + width: 80, + ellipsis: true + }, + { + title: '销量', + dataIndex: '销售数量', + key: '销售数量', + width: 60, + align: 'center', + render: (value: number) => `${value}件`, + sorter: (a, b) => a.销售数量 - b.销售数量 + }, + { + title: '金额', + dataIndex: '销售金额', + key: '销售金额', + width: 90, + align: 'right', + render: (value: number) => `¥${value.toFixed(0)}`, + sorter: (a, b) => a.销售金额 - b.销售金额 + }, + { + title: '件单价', + key: '件单价', + width: 80, + align: 'right', + render: (_, record) => { + const unitPrice = record.销售数量 > 0 ? record.销售金额 / record.销售数量 : 0; + return `¥${unitPrice.toFixed(2)}`; + }, + sorter: (a, b) => { + const aUnitPrice = a.销售数量 > 0 ? a.销售金额 / a.销售数量 : 0; + const bUnitPrice = b.销售数量 > 0 ? b.销售金额 / b.销售数量 : 0; + return aUnitPrice - bUnitPrice; + } + }, + { + title: '占比', + dataIndex: '占比', + key: '占比', + width: 70, + align: 'right', + render: (value: number) => ( + = 10 ? '#52c41a' : value >= 5 ? '#faad14' : '#8c8c8c', + fontWeight: value >= 10 ? 'bold' : 'normal' + }}> + {value.toFixed(1)}% + + ), + sorter: (a, b) => a.占比 - b.占比 + } + ]} + dataSource={productAnalysis} + rowKey="品牌品类" + size="small" + scroll={{ y: 420 }} + pagination={false} + style={{ + border: '1px solid #e9ecef', + borderRadius: '8px', + overflow: 'hidden' + }} + /> + + + + ), + } + ]} + /> + + + ); +}; + +export default SalesAnalysisModal; \ No newline at end of file diff --git a/src/pages/team/SaleRecord/index.tsx b/src/pages/team/SaleRecord/index.tsx index 3187b4b..85bf1f4 100644 --- a/src/pages/team/SaleRecord/index.tsx +++ b/src/pages/team/SaleRecord/index.tsx @@ -109,6 +109,8 @@ const buildCustomerAddress = (address: any): string => { const EditSalesModal = lazy(() => import("./sales-modal")); const AfterSalesModal = lazy(() => import("./AfterSalesModal")); const ShipModal = lazy(() => import("./ship-modal")); +const CustomerAnalysisModal = lazy(() => import("./components/CustomerAnalysisModal")); +const SalesAnalysisModal = lazy(() => import("./components/SalesAnalysisModal")); const SalesPage = () => { const { message } = useApp(); @@ -141,17 +143,19 @@ const SalesPage = () => { } `; document.head.appendChild(style); - + return () => { document.head.removeChild(style); }; }, []); - + // 状态管理 const [modals, setModals] = useState({ edit: false, ship: false, afterSales: false, + customerAnalysis: false, + salesAnalysis: false, }); const [currentRecord, setCurrentRecord] = useState(null); const [loading, setLoading] = useState(false); @@ -173,7 +177,7 @@ const SalesPage = () => { // 立即显示发货模态框,不等待状态更新 setCurrentRecord(record); setModals(prev => ({ ...prev, ship: true })); - + // 在后台异步更新状态(非阻塞) if (record.处理状态 !== '已处理') { updateProcessingStatus(record._id, '已处理') @@ -193,11 +197,17 @@ const SalesPage = () => { setAfterSalesType(type); setModals(prev => ({ ...prev, afterSales: true })); }, - closeAll: () => { - setModals({ edit: false, ship: false, afterSales: false }); - setCurrentRecord(null); - setAfterSalesType(null); + showCustomerAnalysis: () => { + setModals(prev => ({ ...prev, customerAnalysis: true })); }, + showSalesAnalysis: () => { + setModals(prev => ({ ...prev, salesAnalysis: true })); + }, + closeAll: () => { + setModals({ edit: false, ship: false, afterSales: false, customerAnalysis: false, salesAnalysis: false }); + setCurrentRecord(null); + setAfterSalesType(null); + }, handleSuccess: () => { if (userInfo.团队?._id) { fetchSalesRecords(userInfo.团队._id); @@ -227,33 +237,8 @@ const SalesPage = () => { }); } }); - - // 批量预加载产品图片 - /*if (productIds.size > 0) { - fetch('/api/products/batchImages', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - productIds: Array.from(productIds) - }) - }).then(async (response) => { - if (response.ok) { - const data = await response.json(); - // 存储图片到本地缓存 - const imageData = data?.images || {}; - localStorage.setItem('productImageCache', JSON.stringify(imageData)); - console.log('预加载产品图片成功'); - } else { - throw new Error(`HTTP error! status: ${response.status}`); - } - }).catch((error: Error) => { - console.error('预加载产品图片失败', error); - }); - }*/ - // 批量预加载物流状态 + /* if (recordIds.size > 0) { fetch('/api/logistics/batchStatus', { method: 'POST', @@ -277,6 +262,7 @@ const SalesPage = () => { console.error('预加载物流状态失败', error); }); } + */ } }, [salesRecords]); @@ -324,12 +310,12 @@ const SalesPage = () => { } const data = await response.json(); - + if (data.success) { // 更新本地状态 - setSalesRecords(prevRecords => - prevRecords.map(record => - record._id === recordId + setSalesRecords(prevRecords => + prevRecords.map(record => + record._id === recordId ? { ...record, 处理状态: status } : record ) @@ -506,7 +492,7 @@ const SalesPage = () => { align: "center", render: (record: ISalesRecord) => { const currentStatus = record.处理状态 || '未处理'; - + // 状态配置 const statusConfig = { '未处理': { @@ -536,10 +522,10 @@ const SalesPage = () => { }; return ( -
@@ -548,84 +534,84 @@ const SalesPage = () => { onChange={(e) => updateProcessingStatus(record._id, e.target.value)} style={{ width: '100%' }} > -
{(['未处理', '处理中', '已处理'] as const).map((status) => { const config = statusConfig[status]; const isSelected = currentStatus === status; - - return ( - - -
- - {config.icon} - - - {status} - -
-
-
- ); - })} -
- -
- ); - }, - }, + + return ( + + +
+ + {config.icon} + + + {status} + +
+
+
+ ); + })} + + + + ); + }, + }, //备注 { title: "备注", @@ -905,9 +891,9 @@ const SalesPage = () => { ))}
-
( +
+ 销售记录 +
+ + + + + + + + + + + +
+
+ )} scroll={{ - // 表格高度 - 自动计算减去分页器高度,确保表格占满整个容器 - y: 'calc(100vh - 160px)', // 减去头部、分页器等组件的高度 + // 表格高度 - 自动计算减去分页器高度和标题高度,确保表格占满整个容器 + y: 'calc(100vh - 240px)', // 减去头部、标题栏、分页器等组件的高度 // 表格宽度 - 支持水平滚动,内容超出时可滚动 x: 'max-content' }} @@ -994,6 +1020,20 @@ const SalesPage = () => { record={currentRecord} /> )} + {modals.customerAnalysis && ( + + )} + {modals.salesAnalysis && ( + + )} ); diff --git a/src/pages/team/SaleRecord/test.tsx b/src/pages/team/SaleRecord/test.tsx new file mode 100644 index 0000000..5422e10 --- /dev/null +++ b/src/pages/team/SaleRecord/test.tsx @@ -0,0 +1,909 @@ +//src\pages\team\SaleRecord\index.tsx +import React, { useState, useEffect, useMemo } from "react"; +import { + Table, + Button, + TableColumnType, + Tag, + Tooltip, + DatePicker, + App, +} from "antd"; +import { ISalesRecord, ICoupon } from "@/models/types"; // 确保有正确的类型定义 +import { useUserInfo } from "@/store/userStore"; // 使用 Zustand 获取用户信息 +import { + FieldTimeOutlined, + MobileOutlined, + UserOutlined, + WechatOutlined, + DownloadOutlined, +} from "@ant-design/icons"; + +import MyTooltip from "@/components/tooltip/MyTooltip"; +import ProductCardList from "@/components/product/ProductCardList"; + +// 导出Excel相关库 +import * as XLSX from 'xlsx'; +import { saveAs } from 'file-saver'; + +// 移除了模态框相关的懒加载组件 + +const SalesPage = () => { + const { message } = App.useApp(); // 使用 useApp hook 获取 message + const [loading, setLoading] = useState(false); + + const [salesRecords, setSalesRecords] = useState([]); + const [filteredRecords, setFilteredRecords] = useState([]); + const [transactionDateRange, setTransactionDateRange] = useState<[Date | null, Date | null]>([null, null]); + const [addFansDateRange, setAddFansDateRange] = useState<[Date | null, Date | null]>([null, null]); + const userInfo = useUserInfo(); // 获取当前用户信息 + + useEffect(() => { + if (userInfo.团队?._id) { + fetchSalesRecords(userInfo.团队._id); + } + }, [userInfo]); // 恢复原来的依赖项,只在userInfo变化时重新获取数据 + + // 当日期范围筛选条件改变时应用筛选 + useEffect(() => { + if (salesRecords.length > 0) { + const filtered = applyFilters(salesRecords); + setFilteredRecords(filtered); + } + }, [transactionDateRange, addFansDateRange]); + + // 筛选函数:应用日期范围筛选条件 + const applyFilters = (records: ISalesRecord[]) => { + let filtered = [...records]; + + // 根据成交日期范围筛选 + if (transactionDateRange[0] && transactionDateRange[1]) { + filtered = filtered.filter(record => { + if (!record.成交日期) return false; + const recordDate = new Date(record.成交日期); + return recordDate >= transactionDateRange[0]! && + recordDate <= transactionDateRange[1]!; + }); + } + + // 根据加粉日期范围筛选 + if (addFansDateRange[0] && addFansDateRange[1]) { + filtered = filtered.filter(record => { + if (!record.客户?.加粉日期) return false; + const addFansDate = new Date(record.客户.加粉日期); + return addFansDate >= addFansDateRange[0]! && + addFansDate <= addFansDateRange[1]!; + }); + } + + return filtered; + }; + + useEffect(() => { + if (salesRecords.length > 0) { + // 预加载产品图片和物流状态 + const productIds = new Set(); + const recordIds = new Set(); + + salesRecords.forEach(record => { + if (record._id) recordIds.add(record._id); + if (record.产品 && record.产品.length > 0) { + record.产品.forEach(product => { + if (product._id) productIds.add(product._id); + }); + } + }); + + // 批量预加载产品图片 + if (productIds.size > 0) { + fetch('/api/products/batchImages', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + productIds: Array.from(productIds) + }) + }).then(async (response) => { + if (response.ok) { + const data = await response.json(); + const imageData = data?.images || {}; + localStorage.setItem('productImageCache', JSON.stringify(imageData)); + console.log('预加载产品图片成功'); + } + }).catch((error: unknown) => { + console.error('预加载产品图片失败', error); + }); + } + + // 批量预加载物流状态 + if (recordIds.size > 0) { + fetch('/api/logistics/batchStatus', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + recordIds: Array.from(recordIds) + }) + }).then(async (response) => { + if (response.ok) { + const data = await response.json(); + const statusData = data?.statuses || {}; + localStorage.setItem('logisticsStatusCache', JSON.stringify(statusData)); + console.log('预加载物流状态成功'); + } + }).catch((error: unknown) => { + console.error('预加载物流状态失败', error); + }); + } + } + }, [salesRecords]); + + // 获取销售记录 + const fetchSalesRecords = async (teamId: string) => { + setLoading(true); // 请求开始前设置加载状态为 true + try { + const response = await fetch( + `/api/backstage/sales/Records?teamId=${teamId}` + ); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + const records = data.salesRecords || []; + + setSalesRecords(records); // 设置销售记录 + setFilteredRecords(records); // 初始化filteredRecords + setLoading(false); + } catch (error: unknown) { + console.error('获取销售记录失败:', error); + message.error("加载销售记录失败"); // 处理错误 + } finally { + setLoading(false); // 无论成功还是失败都确保加载状态被设置为 false + } + }; + + // 提取唯一的导购姓名用于筛选 + // 提取唯一的成交日期用于筛选 + // 合并导购姓名和成交日期筛选 + //const columns: TableColumnType[] = useMemo(() => [ + const columns: TableColumnType[] = useMemo(() => { + const baseColumns: TableColumnType[] = [ + { + title: "来源", + width: 60, + key: "订单来源信息", + align: "center", + render: (record: any) => { + const wechatId = record.订单来源?.微信号 ?? "未知"; + const accountNumber = record.订单来源?.账号编号 ?? "未知"; + return ( +
+ } + color="blue" + style={{ marginBottom: 4 }} + > + {accountNumber} + + } color="green"> + {wechatId} + +
+ ); + }, + }, + + { + title: "日期/导购", + key: "日期和导购信息", + align: "center", + width: 160, // 减小宽度因为我们优化了控件大小 + render: (record: any) => { + // 格式化日期为更简洁的形式 (年-月-日) + const formatDate = (dateString: string) => { + if (!dateString) return "未知"; + const date = new Date(dateString); + return date.toLocaleDateString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }).replace(/\//g, '-'); + }; + const transactionDate = formatDate(record.成交日期); + const createdDate = formatDate(record.createdAt); + const guideName = record.导购?.姓名 ?? "未知"; + + // 为完整信息创建tooltip内容 + return ( +
+ {/* 导购信息 */} + } + color="green" + style={{ + marginBottom: 4, + textAlign: 'center', + fontWeight: 'bold' + }} + > + {guideName} + + + {/* 成交日期 */} + } + color="blue" + style={{ + margin: 0, + textAlign: 'center', + fontSize: '11px' + }} + > + 成交: {transactionDate} + + + {/* 创建日期 */} + } + color="default" + style={{ + margin: 0, + textAlign: 'center', + fontSize: '11px' + }} + > + 创建: {createdDate} + +
+ ); + }, + }, + { + title: "客户信息", + width: 60, + align: "center", + key: "客户信息", + render: (record: any) => { + const customerName = record.客户?.姓名 ?? "未知"; + + // 计算成交周期 + const transactionDate = record.成交日期 ? new Date(record.成交日期) : new Date(); + const addFansDate = record.客户?.加粉日期 ? new Date(record.客户.加粉日期) : null; + const diffDays = addFansDate ? Math.ceil( + Math.abs(transactionDate.getTime() - addFansDate.getTime()) / (1000 * 60 * 60 * 24) + ) : '未知'; + + // 从 record 获取待收金额 + const unreceivedAmount = record.待收款 + ? parseFloat(record.待收款.toFixed(2)) + : 0; + // 如果待收金额大于0,使用红色,否则使用蓝色 + const customerTagColor = unreceivedAmount > 0 ? "#cd201f" : "blue"; + + return ( + +
+ {/* 第一行:客户姓名 */} + } + color={customerTagColor} + style={{ margin: 0 }} + > + {record.客户?.姓名 ?? "未知"} + + + {/* 第二行:电话尾号 */} + } color="green" style={{ margin: 0 }}> + {record.客户?.电话 + ? `${record.客户.电话.slice(-4)}` + : "未知"} + + + {/* 第三行:加粉日期 */} + } + color="purple" + style={{ margin: 0 }} + > + {record.客户?.加粉日期 + ? new Date(record.客户.加粉日期).toLocaleDateString() + : "未知"} + + + {/* 第四行:成交周期 */} + + 成交周期:{diffDays}天 + +
+
+ ); + }, + }, + //备注 + { + title: "备注", + key: "备注", + dataIndex: "备注", + width: 260, + render: (text: string, _record: any) => { + + return ( + +
+ {text || "无备注"} +
+
+ ); + }, + }, + { + title: "产品信息", + width: 420, // 增加宽度以适应3个产品 (130px * 3 + 间距) + dataIndex: "产品", + key: "productImage", + render: (products: any[], record: ISalesRecord) => { + return ( +
+ +
+ ); + }, + }, + { + title: "财务信息", + key: "financialInfo", + width: 180, // 适当调整宽度 + render: (record: any) => { + const paymentPlatform = record.收款平台?.名称 ?? "未知"; + const receivedAmount = record.收款金额 + ? record.收款金额.toFixed(2) + : "0.00"; + const paymentStatus = record.收款状态 ?? "未知"; + const receivableAmount = record.应收金额 + ? record.应收金额.toFixed(2) + : "0.00"; + const unreceivedAmount = record.待收款 + ? record.待收款.toFixed(2) + : "0.00"; + const receivedPendingAmount = record.待收已收 + ? record.待收已收.toFixed(2) + : "0.00"; // 待收已收字段 + + return ( +
+
+ {paymentPlatform} + + 应收: ¥{receivableAmount} + +
+
+ {paymentStatus} + + 收款: ¥{receivedAmount} + +
+
+ 余额抵用: + + ¥{record.余额抵用?.金额 ?? 0} + +
+
+ 优惠券: +
+ {record.优惠券?.map((coupon: ICoupon) => ( + + 类型:{coupon._id.优惠券类型} +
+ 金额:{coupon._id.金额} +
+ 折扣:{coupon._id.折扣} + + } + > + + {coupon._id.优惠券类型} + +
+ ))} +
+
+ +
+ + 0 + ? "#ff4d4f" + : "inherit", // 只有大于0时才显示红色 + }} + > + 待收:¥{unreceivedAmount} + + +
+ 已收: + + ¥{receivedPendingAmount} + +
+
+
+ ); + }, + }, + ]; + + return baseColumns; + }, [userInfo]); + + // 导出Excel的函数 + const exportToExcel = () => { + if (filteredRecords.length === 0) { + message.warning('没有可导出的数据'); + return; + } + + message.loading({ content: '正在准备导出数据...', key: 'exporting', duration: 0 }); + + try { + // 准备导出数据 + const exportData = filteredRecords.map((record, index) => { + // 处理产品信息 - 合并成一个字符串 + const productsText = record.产品?.map(product => + `${product.名称 || '未知'}` + ).join(', ') || '无产品'; + + // 计算成交周期 + const transactionDate = record.成交日期 ? new Date(record.成交日期) : new Date(); + const addFansDate = record.客户?.加粉日期 ? new Date(record.客户.加粉日期) : null; + const diffDays = addFansDate ? Math.ceil( + Math.abs(transactionDate.getTime() - addFansDate.getTime()) / (1000 * 60 * 60 * 24) + ) : '未知'; + + // 格式化日期 + const formatDate = (dateStr: string | undefined | Date): string => { + if (!dateStr) return '未知'; + try { + const date = dateStr instanceof Date ? dateStr : new Date(dateStr); + if (isNaN(date.getTime())) return '无效日期'; + return date.toLocaleDateString('zh-CN'); + } catch { + return '无效日期'; + } + }; + + // 处理电话号码 - 只保留尾号 + const phoneNumber = record.客户?.电话 + ? `${record.客户.电话.slice(-4)}` + : '未知'; + + // 计算售后产品金额 + let afterSalesAmount = 0; + if (record.售后记录 && record.售后记录.length > 0 && record.产品 && record.产品.length > 0) { + const afterSalesProductIds = new Set( + record.售后记录.flatMap(afterSales => + afterSales.原产品.map(productId => String(productId)) + ) + ); + + record.产品.forEach(product => { + if (afterSalesProductIds.has(String(product._id))) { + afterSalesAmount += (product.售价 || 0); + } + }); + } + + // 计算产品总成本(成本价+包装费+运费) + let totalProductCost = 0; + if (record.产品 && record.产品.length > 0) { + totalProductCost = record.产品.reduce((sum, product) => { + const costPrice = product.成本?.成本价 || 0; + const packagingFee = product.成本?.包装费 || 0; + const shippingFee = product.成本?.运费 || 0; + return sum + costPrice + packagingFee + shippingFee; + }, 0); + } + + // 返回导出的行数据 + return { + '序号': index + 1, + '客户姓名': record.客户?.姓名 || '未知', + '电话尾号': phoneNumber, + '成交日期': formatDate(record.成交日期), + '加粉日期': formatDate(record.客户?.加粉日期), + '成交周期(天)': diffDays, + '导购': record.导购?.姓名 || '未知', + '账号编号': record.订单来源?.账号编号 || '未知', + '产品信息': productsText, + '应收金额': record.应收金额 || 0, + '收款金额': record.收款金额 || 0, + '待收金额': record.待收款 || 0, + '待收已收': record.待收已收 || 0, + '收款平台': record.收款平台?.名称 || '未知', + '收款状态': record.收款状态 || '未知', + '总成本': totalProductCost, + '售后金额': afterSalesAmount, + '备注': record.备注 || '无', + '创建时间': record.createdAt ? formatDate(new Date(record.createdAt)) : '未知', + }; + }); + + // 创建工作簿和工作表 + const ws = XLSX.utils.json_to_sheet(exportData); + const wb = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(wb, ws, '销售记录'); + + // 设置列宽 (大致估计,可能需要调整) + const colWidths = [ + { wch: 5 }, // 序号 + { wch: 10 }, // 客户姓名 + { wch: 10 }, // 电话尾号 + { wch: 10 }, // 成交日期 + { wch: 10 }, // 加粉日期 + { wch: 8 }, // 成交周期 + { wch: 10 }, // 导购 + { wch: 10 }, // 账号编号 + { wch: 40 }, // 产品信息 + { wch: 10 }, // 应收金额 + { wch: 10 }, // 收款金额 + { wch: 10 }, // 待收金额 + { wch: 10 }, // 待收已收 + { wch: 10 }, // 收款平台 + { wch: 10 }, // 收款状态 + { wch: 10 }, // 总成本 + { wch: 10 }, // 售后金额 + { wch: 20 }, // 备注 + { wch: 20 }, // 创建时间 + ]; + ws['!cols'] = colWidths; + + // 生成Excel文件并下载 + const now = new Date(); + const dateStr = `${now.getFullYear()}${(now.getMonth() + 1).toString().padStart(2, '0')}${now.getDate().toString().padStart(2, '0')}`; + const fileName = `销售记录导出_${dateStr}.xlsx`; + + // 使用file-saver保存文件 + const wbout = XLSX.write(wb, { bookType: 'xlsx', type: 'array' }); + const blob = new Blob([wbout], { type: 'application/octet-stream' }); + saveAs(blob, fileName); + + message.success({ content: '导出成功!', key: 'exporting' }); + } catch (error) { + console.error('导出Excel失败:', error); + message.error({ content: '导出失败,请重试', key: 'exporting' }); + } + }; + + return ( +
+
{ + // 计算统计数据 + const calculateStats = () => { + // 总记录数量 + const recordCount = filteredRecords.length; + + // 总出单数量(所有产品数量的总和) + const orderCount = filteredRecords.reduce((sum, record) => { + // 如果产品数组存在且有长度,就累加产品数量 + // 如果产品数组不存在或为空,就按1计算 + if (record.产品 && record.产品.length > 0) { + return sum + record.产品.length; + } + return sum + 1; // 如果没有产品信息,按一单计算 + }, 0); + + // 计算不重复的客户数量(根据客户电话号码去重) + const uniqueCustomers = new Set(); + filteredRecords.forEach(record => { + if (record.客户?.电话) { + uniqueCustomers.add(record.客户.电话); + } + }); + const customerCount = uniqueCustomers.size; + + // 计算总销售额(应收金额) + const totalSales = filteredRecords.reduce((sum, record) => { + return sum + (record.应收金额 || 0); + }, 0); + + // 计算总成本 + const totalCost = filteredRecords.reduce((sum, record) => { + if (record.产品 && record.产品.length > 0) { + // 累加每个产品的总成本(成本价+包装费+运费) + const productsCost = record.产品.reduce((costSum, product) => { + // 计算单个产品的总成本 = 成本价 + 包装费 + 运费 + const costPrice = product.成本?.成本价 || 0; + const packagingFee = product.成本?.包装费 || 0; + const shippingFee = product.成本?.运费 || 0; + const totalProductCost = costPrice + packagingFee + shippingFee; + + // 假设每个产品数量为1,如果有产品数量字段,应该乘以数量 + return costSum + totalProductCost; + }, 0); + return sum + productsCost; + } + return sum; + }, 0); + + // 计算毛利率 = (销售额-成本)/销售额 * 100% + const profitRate = totalSales > 0 + ? ((totalSales - totalCost) / totalSales * 100).toFixed(1) + : '0.0'; + + // 计算客单价 = 总销售额/客户数量 + const customerPrice = customerCount > 0 + ? (totalSales / customerCount).toFixed(2) + : '0.00'; + + // 计算件单价 = 总销售额/总出单数量 + const unitPrice = orderCount > 0 + ? (totalSales / orderCount).toFixed(2) + : '0.00'; + + // 计算售后产品金额 + let afterSalesAmount = 0; + filteredRecords.forEach(record => { + if (record.售后记录 && record.售后记录.length > 0 && record.产品 && record.产品.length > 0) { + const afterSalesProductIds = new Set( + record.售后记录.flatMap(afterSales => + afterSales.原产品.map(productId => String(productId)) + ) + ); + + record.产品.forEach(product => { + if (afterSalesProductIds.has(String(product._id))) { + afterSalesAmount += (product.售价 || 0); + } + }); + } + }); + + return { + orderCount, + customerCount, + totalSales, + totalCost, + profitRate, + recordCount, + customerPrice, + unitPrice, + afterSalesAmount + }; + }; + + // 获取统计数据 + const stats = calculateStats(); + + // 渲染筛选状态标签 + const renderFilterStatus = () => { + if (transactionDateRange[0] || addFansDateRange[0]) { + return ( + + + 共: {stats.recordCount} / {salesRecords.length} 条记录 + + + + ); + } + return null; + }; + + return ( +
+
+
+ 成交日期: + { + if (dateStrings[0] && dateStrings[1]) { + // 设置开始日期为当天的00:00:00 + const startDate = new Date(dateStrings[0]); + startDate.setHours(0, 0, 0, 0); + + // 设置结束日期为当天的23:59:59,确保包含当天的所有记录 + const endDate = new Date(dateStrings[1]); + endDate.setHours(23, 59, 59, 999); + + setTransactionDateRange([startDate, endDate]); + } else { + setTransactionDateRange([null, null]); + } + }} + /> +
+
+ 加粉日期: + { + if (dateStrings[0] && dateStrings[1]) { + // 设置开始日期为当天的00:00:00 + const startDate = new Date(dateStrings[0]); + startDate.setHours(0, 0, 0, 0); + + // 设置结束日期为当天的23:59:59,确保包含当天的所有记录 + const endDate = new Date(dateStrings[1]); + endDate.setHours(23, 59, 59, 999); + + setAddFansDateRange([startDate, endDate]); + } else { + setAddFansDateRange([null, null]); + } + }} + /> +
+ + {renderFilterStatus()} + + {/* 添加统计信息 */} +
+ + }> + 客户: {stats.customerCount} + + + + }> + 记录/出单: {stats.recordCount}/{stats.orderCount} + + + + + 销售额: ¥{stats.totalSales.toFixed(2)} + + + + + 成本: ¥{stats.totalCost.toFixed(2)} + + + + + 毛利: ¥{(stats.totalSales - stats.totalCost).toFixed(2)} + + + + + 毛利率: {stats.profitRate}% + + + + }> + 客单价: ¥{stats.customerPrice} + + + + }> + 件单价: ¥{stats.unitPrice} + + + + + 售后金额: ¥{stats.afterSalesAmount.toFixed(2)} + + + + {/* 添加导出按钮 */} + +
+
+
+ ); + }} + scroll={{ + y: `calc(100vh - 250px)`, // 适当调整高度 + x: 'max-content' + }} + pagination={{ + position: ['bottomRight'], + pageSize: 100, + showSizeChanger: true, + pageSizeOptions: ["10", "20", "50", "100"], + showTotal: (total) => `共 ${total} 条记录`, + size: 'small' + }} + size="small" + loading={loading} + columns={columns} + dataSource={filteredRecords} // 使用筛选后的记录 + rowKey="_id" + // 添加行渲染优化 + rowClassName={(_, index) => (index % 2 === 0 ? 'even-row' : 'odd-row')} + /> + + + ); +}; + +export default SalesPage; diff --git a/src/pages/team/account/index.tsx b/src/pages/team/account/index.tsx new file mode 100644 index 0000000..66d6817 --- /dev/null +++ b/src/pages/team/account/index.tsx @@ -0,0 +1,1191 @@ +/** + * 文件名: src/pages/team/account/index.tsx + * 作者: 阿瑞 + * 功能: 团队账号增长数据管理组件 + * 版本: v2.2.0 (UI/UX优化版本) + */ + +import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react'; +import { ProTable } from '@ant-design/pro-components'; +import { + InputNumber, + DatePicker, + Tag, + App, + Card, + Row, + Col, + Badge, + Alert, + Space, + Button, + Popconfirm +} from 'antd'; +import dayjs, { Dayjs } from 'dayjs'; +import { useUserInfo } from '@/store/userStore'; +import { ProColumns } from '@ant-design/pro-components'; +import { IAccount, IDailyGrowth } from '@/models/types'; +import { + ShopOutlined, + WechatOutlined, + ArrowUpOutlined, + ArrowDownOutlined, + SaveOutlined, + LoadingOutlined +} from '@ant-design/icons'; +import isBetween from 'dayjs/plugin/isBetween'; +dayjs.extend(isBetween); + +// ▸ 常量定义模块 +const CONSTANTS = { + DATE_BACKGROUND_COLORS: [ + 'linear-gradient(135deg, #E6F7FF 0%, #D1F0FF 100%)', + 'linear-gradient(135deg, #FFFB8F 0%, #FFF566 100%)', + 'linear-gradient(135deg, #FFCCC7 0%, #FFB3AE 100%)', + 'linear-gradient(135deg, #D9F7BE 0%, #C6F39A 100%)', + 'linear-gradient(135deg, #F4FFB8 0%, #EAFF8F 100%)' + ] as const, + CELL_PADDING: '4px', + CELL_BORDER_RADIUS: '4px', + DATA_CELL_BG: 'linear-gradient(135deg, #E6F7FF 0%, #F0F9FF 100%)', + EDITING_CELL_BG: 'linear-gradient(135deg, #FFF7E6 0%, #FFFCF0 100%)', + SUCCESS_CELL_BG: 'linear-gradient(135deg, #F6FFED 0%, #FCFFE6 100%)', + ERROR_CELL_BG: 'linear-gradient(135deg, #FFF2F0 0%, #FFEDE8 100%)', + INHERIT_BG: 'transparent', + COLUMN_WIDTHS: { + ACCOUNT_INFO: 180, + CONSUMPTION: 80, + TOTAL: 80, + DEDUCT: 80, + GROWTH: 80, + }, + SCROLL_HEIGHT: 'calc(100vh - 300px)', + COLORS: { + PRIMARY: '#1890ff', + SUCCESS: '#52c41a', + WARNING: '#faad14', + ERROR: '#f5222d', + PURPLE: '#722ed1', + }, + GROWTH_THRESHOLDS: { + EXCELLENT: 50, + GOOD: 20, + AVERAGE: 10, + }, + DEBOUNCE_DELAY: 200, + AUTO_SCROLL_DELAY: 1200, + MESSAGE_DURATION: 1.5, +} as const; + +// ▸ 类型定义模块 +type FieldType = 'consumption' | 'total' | 'deduct'; + +interface EditedDataItem { + 消耗: number; + 总人数: number; + 扣除人数: number; + isEditing?: boolean; + lastSaved?: Date; +} + +interface EditedData { + [accountId: string]: { + [date: string]: EditedDataItem; + }; +} + +interface CellStyleProps { + hasData: boolean; + isGrowthPositive?: boolean; + isEditing?: boolean; + saveStatus?: 'saving' | 'success' | 'error'; +} + +interface StatCardProps { + title: string; + value: number; + trend?: 'up' | 'down' | 'stable'; + color?: string; + suffix?: string; +} + +interface CellComponentProps { + record: IAccount; + dateKey: string; + editedData: EditedData; + onEdit: (accountId: string, date: string, field: FieldType, value: number) => void; + saving: boolean; +} + +// ▸ 样式工具函数模块 +const getCellStyle = ({ hasData, isGrowthPositive, isEditing, saveStatus }: CellStyleProps) => { + let background: string = CONSTANTS.INHERIT_BG; + + if (saveStatus === 'saving') { + background = CONSTANTS.EDITING_CELL_BG; + } else if (saveStatus === 'success') { + background = CONSTANTS.SUCCESS_CELL_BG; + } else if (saveStatus === 'error') { + background = CONSTANTS.ERROR_CELL_BG; + } else if (isEditing) { + background = CONSTANTS.EDITING_CELL_BG; + } else if (hasData || isGrowthPositive) { + background = CONSTANTS.DATA_CELL_BG; + } + + return { + background, + padding: CONSTANTS.CELL_PADDING, + borderRadius: CONSTANTS.CELL_BORDER_RADIUS, + transition: 'all 0.3s ease', + border: isEditing ? `1px solid ${CONSTANTS.COLORS.PRIMARY}` : '1px solid transparent', + boxShadow: isEditing ? `0 0 0 2px ${CONSTANTS.COLORS.PRIMARY}20` : 'none', + }; +}; + +const getDateBackgroundColor = (dateIndex: number): string => { + return CONSTANTS.DATE_BACKGROUND_COLORS[dateIndex % CONSTANTS.DATE_BACKGROUND_COLORS.length]; +}; + +const getGrowthIndicator = (growth: number) => { + if (growth > CONSTANTS.GROWTH_THRESHOLDS.EXCELLENT) { + return { icon: , color: CONSTANTS.COLORS.SUCCESS, level: '优秀' }; + } else if (growth > CONSTANTS.GROWTH_THRESHOLDS.GOOD) { + return { icon: , color: CONSTANTS.COLORS.PRIMARY, level: '良好' }; + } else if (growth > CONSTANTS.GROWTH_THRESHOLDS.AVERAGE) { + return { icon: , color: CONSTANTS.COLORS.WARNING, level: '一般' }; + } else if (growth < 0) { + return { icon: , color: CONSTANTS.COLORS.ERROR, level: '下降' }; + } + return { icon: null, color: '#666', level: '无变化' }; +}; + +// ▸ 统计卡片组件 +const StatCard: React.FC = ({ title, value, trend, color = CONSTANTS.COLORS.PRIMARY, suffix = '人' }) => ( + +
+
{title}
+
+ {trend === 'up' ? : trend === 'down' ? : null} + {value}{suffix} +
+
+
+); + +// ▸ 实时数据合并函数 - 将编辑数据与原始数据合并 +const getMergedAccounts = (accounts: IAccount[], editedData: EditedData): IAccount[] => { + return accounts.map(account => { + const accountEdits = editedData[account._id]; + if (!accountEdits) return account; + + // 创建新的日增长数据数组,包含编辑的数据 + const mergedDailyData = [...account.日增长数据]; + + Object.entries(accountEdits).forEach(([dateKey, editData]) => { + const existingIndex = mergedDailyData.findIndex( + item => dayjs(item.日期).isSame(dateKey, 'day') + ); + + if (existingIndex >= 0) { + // 更新现有记录 + mergedDailyData[existingIndex] = { + ...mergedDailyData[existingIndex], + 消耗: editData.消耗, + 总人数: editData.总人数, + 扣除人数: editData.扣除人数, + }; + } else { + // 添加新记录 + mergedDailyData.push({ + 日期: dayjs(dateKey).toDate(), + 消耗: editData.消耗, + 总人数: editData.总人数, + 扣除人数: editData.扣除人数, + }); + } + }); + + return { + ...account, + 日增长数据: mergedDailyData.sort((a, b) => + new Date(a.日期).getTime() - new Date(b.日期).getTime() + ) + }; + }); +}; + +// ▸ 计算辅助函数模块 +const calculateGrowth = (dailyRecords: IDailyGrowth[], currentRecord: IDailyGrowth, date: string): number => { + const sortedRecords = dailyRecords + .slice() + .sort((a, b) => new Date(a.日期).getTime() - new Date(b.日期).getTime()); + const currentIndex = sortedRecords.findIndex((item) => dayjs(item.日期).isSame(date, 'day')); + if (currentIndex === 0) return 0; + const previousRecord = sortedRecords[currentIndex - 1]; + return (currentRecord.总人数 || 0) - (previousRecord?.总人数 || 0); +}; + +const calculateMonthlyGrowth = (account: IAccount, selectedMonth: Dayjs): number => { + const startOfMonth = selectedMonth.startOf('month'); + const endOfMonth = selectedMonth.endOf('month'); + + return account.日增长数据.reduce((totalGrowth, dailyRecord) => { + const recordDate = dayjs(dailyRecord.日期); + if (recordDate.isBetween(startOfMonth, endOfMonth, 'day', '[]')) { + const growth = calculateGrowth( + account.日增长数据, + dailyRecord, + recordDate.format('YYYY-MM-DD'), + ); + return totalGrowth + growth; + } + return totalGrowth; + }, 0); +}; + +const calculateTotalGrowthPerDay = (accounts: IAccount[], date: string): number => { + return accounts.reduce((totalGrowth, account) => { + const dailyRecord = account.日增长数据.find((item: IDailyGrowth) => dayjs(item.日期).isSame(date, 'day')); + if (dailyRecord) { + const growth = calculateGrowth(account.日增长数据, dailyRecord, date); + return totalGrowth + growth; + } + return totalGrowth; + }, 0); +}; + +const calculateTotalConsumptionPerDay = (accounts: IAccount[], date: string): number => { + return accounts.reduce((totalConsumption, account) => { + const dailyRecord = account.日增长数据.find((item: IDailyGrowth) => dayjs(item.日期).isSame(date, 'day')); + if (dailyRecord) { + return totalConsumption + (dailyRecord.消耗 || 0); + } + return totalConsumption; + }, 0); +}; + +// ▸ 增强的输入框组件 +const EnhancedInputNumber: React.FC<{ + value: number; + onChange: (value: number) => void; + saveStatus?: 'saving' | 'success' | 'error'; + placeholder?: string; +}> = React.memo(({ value, onChange, saveStatus, placeholder }) => { + const [localValue, setLocalValue] = useState(value); + const [focused, setFocused] = useState(false); + const debounceTimeoutRef = useRef(null); + + // 当外部value变化时同步到本地state + useEffect(() => { + setLocalValue(value); + }, [value]); + + const getSuffixIcon = () => { + if (saveStatus === 'saving') return ; + if (saveStatus === 'success') return ; + if (saveStatus === 'error') return ; + return null; + }; + + const handleChange = useCallback((val: number | null) => { + const newValue = val || 0; + setLocalValue(newValue); + + // 清除之前的防抖定时器 + if (debounceTimeoutRef.current) { + clearTimeout(debounceTimeoutRef.current); + } + + // 设置新的防抖定时器 - 减少频繁状态更新 + debounceTimeoutRef.current = setTimeout(() => { + onChange(newValue); + }, CONSTANTS.DEBOUNCE_DELAY); // 200ms防抖延迟 + }, [onChange]); + + // 组件卸载时清理定时器 + useEffect(() => { + return () => { + if (debounceTimeoutRef.current) { + clearTimeout(debounceTimeoutRef.current); + } + }; + }, []); + + return ( + setFocused(true)} + onBlur={() => setFocused(false)} + placeholder={placeholder} + suffix={getSuffixIcon()} + style={{ + width: '100%', + borderColor: focused ? CONSTANTS.COLORS.PRIMARY : undefined, + boxShadow: focused ? `0 0 0 2px ${CONSTANTS.COLORS.PRIMARY}20` : undefined, + }} + size="small" + /> + ); +}); + +// ▸ 日期列渲染组件模块 +const TotalCell: React.FC = React.memo(({ record, dateKey, editedData, onEdit, saving }) => { + const cellData = editedData[record._id]?.[dateKey]; + const dailyRecord = useMemo(() => + record.日增长数据?.find((item: IDailyGrowth) => dayjs(item.日期).isSame(dateKey, 'day')), + [record.日增长数据, dateKey] + ); + + const value = cellData?.总人数 ?? dailyRecord?.总人数 ?? 0; + const hasData = !!value; + const isEditing = cellData?.isEditing; + const saveStatus = saving ? 'saving' : undefined; + + const handleChange = useCallback((newValue: number) => { + onEdit(record._id, dateKey, 'total', newValue); + }, [onEdit, record._id, dateKey]); + + return ( +
+ +
+ ); +}); + +const ConsumptionCell: React.FC = React.memo(({ record, dateKey, editedData, onEdit, saving }) => { + const cellData = editedData[record._id]?.[dateKey]; + const dailyRecord = useMemo(() => + record.日增长数据?.find((item: IDailyGrowth) => dayjs(item.日期).isSame(dateKey, 'day')), + [record.日增长数据, dateKey] + ); + + const value = cellData?.消耗 ?? dailyRecord?.消耗 ?? 0; + const hasData = !!value; + const isEditing = cellData?.isEditing; + const saveStatus = saving ? 'saving' : undefined; + + const handleChange = useCallback((newValue: number) => { + onEdit(record._id, dateKey, 'consumption', newValue); + }, [onEdit, record._id, dateKey]); + + return ( +
+ +
+ ); +}); + +const DeductCell: React.FC = React.memo(({ record, dateKey, editedData, onEdit, saving }) => { + const cellData = editedData[record._id]?.[dateKey]; + const dailyRecord = useMemo(() => + record.日增长数据?.find((item: IDailyGrowth) => dayjs(item.日期).isSame(dateKey, 'day')), + [record.日增长数据, dateKey] + ); + + const value = cellData?.扣除人数 ?? dailyRecord?.扣除人数 ?? 0; + const hasData = !!value; + const isEditing = cellData?.isEditing; + const saveStatus = saving ? 'saving' : undefined; + + const handleChange = useCallback((newValue: number) => { + onEdit(record._id, dateKey, 'deduct', newValue); + }, [onEdit, record._id, dateKey]); + + return ( +
+ +
+ ); +}); + +const GrowthCell: React.FC<{ + record: IAccount; + dateKey: string; + editedData: EditedData; +}> = React.memo(({ record, dateKey, editedData }) => { + // 获取编辑数据或原始数据 + const cellData = editedData[record._id]?.[dateKey]; + const dailyRecord = useMemo(() => + record.日增长数据?.find((item: IDailyGrowth) => dayjs(item.日期).isSame(dateKey, 'day')), + [record.日增长数据, dateKey] + ); + + // 使用编辑数据或原始数据计算当前总人数 + const currentTotal = cellData?.总人数 ?? dailyRecord?.总人数 ?? 0; + + // 查找前一天的数据 + const prevDate = dayjs(dateKey).subtract(1, 'day').format('YYYY-MM-DD'); + const prevCellData = editedData[record._id]?.[prevDate]; + const prevDailyRecord = record.日增长数据?.find((item: IDailyGrowth) => + dayjs(item.日期).isSame(prevDate, 'day') + ); + const prevTotal = prevCellData?.总人数 ?? prevDailyRecord?.总人数 ?? 0; + + // 实时计算增长 + const growth = currentTotal - prevTotal; + const indicator = getGrowthIndicator(growth); + + return ( +
0, isGrowthPositive: growth > 0 })}> +
+ {indicator.icon} + {growth} +
+
+ ); +}); + +// ▸ 动态日期列生成模块 +const generateDateColumns = ( + onEdit: (accountId: string, date: string, field: FieldType, value: number) => void, + editedData: EditedData, + _accounts: IAccount[], + mergedAccounts: IAccount[], + selectedMonth: Dayjs, + saving: boolean, +) => { + const startOfMonth = selectedMonth.startOf('month'); + const endOfMonth = selectedMonth.endOf('month'); + const daysInMonth = []; + + for ( + let date = startOfMonth, index = 0; + date.isBefore(endOfMonth) || date.isSame(endOfMonth); + date = date.add(1, 'day'), index++ + ) { + const dateKey = date.format('YYYY-MM-DD'); + const backgroundColor = getDateBackgroundColor(index); + const totalGrowth = calculateTotalGrowthPerDay(mergedAccounts, dateKey); + const totalConsumption = calculateTotalConsumptionPerDay(mergedAccounts, dateKey); + const isToday = date.isSame(dayjs(), 'day'); + const isWeekend = date.day() === 0 || date.day() === 6; + + daysInMonth.push({ + title: ( +
+
+ {date.format('MM-DD')} + {isToday && } +
+
+ 增长 {totalGrowth} + 消耗 ¥{totalConsumption} +
+ {isWeekend && ( +
+ 休 +
+ )} +
+ ), + children: [ + { + title: '消耗', + dataIndex: `${dateKey}-consumption`, + key: `${dateKey}-consumption`, + width: CONSTANTS.COLUMN_WIDTHS.CONSUMPTION, + render: (_: unknown, record: IAccount) => ( + + ), + }, + { + title: '总数', + dataIndex: `${dateKey}-total`, + key: `${dateKey}-total`, + width: CONSTANTS.COLUMN_WIDTHS.TOTAL, + render: (_: unknown, record: IAccount) => ( + + ), + }, + { + title: '扣除', + dataIndex: `${dateKey}-deduct`, + key: `${dateKey}-deduct`, + width: CONSTANTS.COLUMN_WIDTHS.DEDUCT, + render: (_: unknown, record: IAccount) => ( + + ), + }, + { + title: '增长', + dataIndex: `${dateKey}-growth`, + key: `${dateKey}-growth`, + width: CONSTANTS.COLUMN_WIDTHS.GROWTH, + render: (_: unknown, record: IAccount) => ( + + ), + }, + ], + }); + } + + return daysInMonth; +}; + +// ▸ 账号信息卡片组件 +const AccountInfoCard: React.FC<{ + record: IAccount; + monthlyGrowth: number; + isDevelopment: boolean; + onDeleteGrowthData: (accountId: string, accountCode: string) => void; +}> = React.memo(({ record, monthlyGrowth, isDevelopment, onDeleteGrowthData }) => { + const growthIndicator = getGrowthIndicator(monthlyGrowth); + + return ( + + +
+ } + color="orange" + style={{ fontWeight: 'bold', fontSize: '12px' }} + > + {record.账号编号} + + 0 ? 'success' : monthlyGrowth < 0 ? 'error' : 'default'} + style={{ fontWeight: 'bold', fontSize: '12px' }} + > + {monthlyGrowth}人 + +
+
+ } color="cyan" style={{ fontSize: '11px' }}> + {record.微信号} + + {/* 开发模式调试按钮 */} + {isDevelopment && ( + onDeleteGrowthData(record._id, record.账号编号)} + okText="确定删除" + cancelText="取消" + okType="danger" + placement="right" + > + + + )} +
+ {record.账号状态 && ( + + )} +
+
+ ); +}); + +const AccountGrowthTable: React.FC = () => { + // ▸ 状态管理模块 + const [accounts, setAccounts] = useState([]); + const [editedData, setEditedData] = useState({}); + const [selectedMonth, setSelectedMonth] = useState(dayjs()); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [shouldAutoScroll, setShouldAutoScroll] = useState(true); + + // ▸ 用户信息获取模块 + const userInfo = useUserInfo(); + const teamId = userInfo?.团队?._id; + const { message } = App.useApp(); + + // ▸ 开发模式检查 + const isDevelopment = process.env.NODE_ENV === 'development'; + + // ▸ 错误处理模块 + if (!teamId) { + return ( + + ); + } + + // ▸ 实时合并数据 - 将编辑中的数据与原始数据合并 + const mergedAccounts = useMemo(() => { + return getMergedAccounts(accounts, editedData); + }, [accounts, editedData]); + + // ▸ 统计数据计算模块 - 使用合并后的数据实时计算 + const statistics = useMemo(() => { + let totalMonthlyGrowth = 0; + let totalMonthlyConsumption = 0; + let activeAccounts = 0; + let totalUsers = 0; + + // 使用合并后的数据进行统计 + mergedAccounts.forEach(account => { + // 计算月度增长 + const monthlyGrowth = calculateMonthlyGrowth(account, selectedMonth); + totalMonthlyGrowth += monthlyGrowth; + + // 获取当月数据 + const monthlyRecords = account.日增长数据.filter(record => + dayjs(record.日期).isSame(selectedMonth, 'month') + ); + + if (monthlyRecords.length > 0) { + // 计算消耗总额 + totalMonthlyConsumption += monthlyRecords.reduce((sum, record) => sum + (record.消耗 || 0), 0); + + // 检查是否为活跃账号 + if (monthlyRecords.some(record => record.总人数 > 0)) { + activeAccounts++; + } + + // 获取最新记录的总人数 + const latestRecord = monthlyRecords.sort((a, b) => + new Date(b.日期).getTime() - new Date(a.日期).getTime() + )[0]; + totalUsers += (latestRecord?.总人数 || 0); + } + }); + + const daysInMonth = selectedMonth.daysInMonth(); + + return { + totalMonthlyGrowth, + averageDailyGrowth: Math.round((totalMonthlyGrowth / daysInMonth) * 10) / 10, + totalMonthlyConsumption, + averageDailyConsumption: Math.round((totalMonthlyConsumption / daysInMonth) * 10) / 10, + activeAccounts, + totalUsers, + totalAccounts: mergedAccounts.length + }; + }, [mergedAccounts, selectedMonth]); + + // ▸ 自动滚动到当前日期模块 + const scrollToCurrentDate = useCallback(() => { + if (accounts.length === 0 || !shouldAutoScroll) return; + + // 延迟执行,确保表格已完全渲染 + setTimeout(() => { + try { + const today = dayjs(); + // 检查今天是否在当前选择的月份内 + if (!today.isSame(selectedMonth, 'month')) return; + + const tableContainer = document.querySelector('.ant-table-container'); + const scrollContainer = document.querySelector('.ant-table-body'); + + if (tableContainer && scrollContainer) { + // 查找包含今天日期的表头元素 + const headers = document.querySelectorAll('.ant-table-thead th'); + let todayColumnIndex = -1; + let todayHeader: Element | null = null; + + headers.forEach((header, index) => { + if (header.textContent && header.textContent.includes(today.format('MM-DD'))) { + todayColumnIndex = index; + todayHeader = header; + } + }); + + if (todayColumnIndex > 0 && todayHeader) { + // 计算更精确的滚动位置 + const headerRect = (todayHeader as HTMLElement).getBoundingClientRect(); + const containerRect = scrollContainer.getBoundingClientRect(); + + // 计算需要滚动的距离,让目标列在容器中央显示 + const headerOffsetLeft = (todayHeader as HTMLElement).offsetLeft || 0; + const containerWidth = containerRect.width; + const headerWidth = headerRect.width; + + // 让目标列在容器中央显示 + const targetScrollLeft = headerOffsetLeft - (containerWidth / 2) + (headerWidth / 2); + + scrollContainer.scrollTo({ + left: Math.max(0, targetScrollLeft), // 确保不小于0 + behavior: 'smooth' + }); + + message.success('已定位到今日数据', CONSTANTS.MESSAGE_DURATION); + // 滚动完成后禁用自动滚动,避免用户操作时干扰 + setTimeout(() => setShouldAutoScroll(false), CONSTANTS.AUTO_SCROLL_DELAY); + } + } + } catch (error) { + console.log('自动滚动失败:', error); + } + }, CONSTANTS.AUTO_SCROLL_DELAY); // 增加延迟时间,确保表格完全渲染 + }, [accounts, selectedMonth, message, shouldAutoScroll]); + + // ▸ 数据获取模块 + const fetchAccounts = useCallback(async () => { + setLoading(true); + try { + const response = await fetch( + `/api/backstage/accounts/accountgrowth?teamId=${teamId}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + } + ); + + if (!response.ok) { + throw new Error(`HTTP错误: ${response.status}`); + } + + const data = await response.json(); + setAccounts(data.accounts); + } catch (error) { + console.error('Error fetching accounts:', error); + message.error('获取账号数据失败'); + } finally { + setLoading(false); + } + }, [teamId, message]); + + useEffect(() => { + fetchAccounts(); + }, [fetchAccounts]); + + // ▸ 监听数据初次加载,自动滚动到当前日期 + useEffect(() => { + if (!loading && accounts.length > 0 && shouldAutoScroll) { + scrollToCurrentDate(); + } + }, [loading, accounts, scrollToCurrentDate, shouldAutoScroll]); + + // ▸ 调试功能:删除账号日增长数据 + const handleDeleteAccountGrowthData = useCallback(async (accountId: string, accountCode: string) => { + try { + // 调用API删除该账号的日增长数据 + const response = await fetch(`/api/backstage/accounts/accountgrowth/clear`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + accountId, + teamId + }), + }); + + if (!response.ok) { + throw new Error(`HTTP错误: ${response.status}`); + } + + // 更新本地状态,清空该账号的日增长数据 + setAccounts(prevAccounts => + prevAccounts.map(account => + account._id === accountId + ? { ...account, 日增长数据: [] } + : account + ) + ); + + // 清除该账号的编辑状态 + setEditedData(prev => { + const updated = { ...prev }; + delete updated[accountId]; + return updated; + }); + + message.success(`账号 ${accountCode} 的日增长数据已清除`, CONSTANTS.MESSAGE_DURATION); + } catch (error) { + console.error('删除日增长数据失败:', error); + message.error('删除失败,请重试'); + } + }, [teamId, message]); + + // ▸ 本地编辑状态更新模块 + const handleEdit = useCallback(( + accountId: string, + date: string, + field: FieldType, + value: number + ) => { + setEditedData((prev: EditedData) => { + const currentData = prev[accountId]?.[date] || {}; + + // 更新对应字段 + const fieldMap = { + consumption: '消耗', + total: '总人数', + deduct: '扣除人数' + } as const; + + return { + ...prev, + [accountId]: { + ...prev[accountId], + [date]: { + ...currentData, + [fieldMap[field]]: value, + isEditing: true, + lastSaved: new Date() + } + } + }; + }); + }, []); + + // ▸ 批量保存模块 - 优化并行处理和性能监控 + const handleSaveAll = useCallback(async () => { + if (Object.keys(editedData).length === 0) { + message.warning('没有需要保存的数据'); + return; + } + + const startTime = performance.now(); + setSaving(true); + let successCount = 0; + let errorCount = 0; + const errors: Array<{ accountId: string; date: string; error: string }> = []; + + try { + // 构建所有保存任务 + const savePromises: Promise[] = []; + + for (const accountId of Object.keys(editedData)) { + for (const date of Object.keys(editedData[accountId])) { + const data = editedData[accountId][date]; + + const saveTask = async () => { + try { + const requestData = { + accountId, + date, + consumption: data.消耗 ?? 0, + totalPeople: data.总人数 ?? 0, + deductedPeople: data.扣除人数 ?? 0, + }; + + if (isDevelopment) { + console.log('发送数据:', requestData); + } + + const response = await fetch(`/api/backstage/accounts/accountgrowth/register`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestData), + }); + + if (!response.ok) { + let errorInfo = `状态码: ${response.status}`; + try { + const errorData = await response.json(); + errorInfo += ` - ${errorData.error || '未知错误'}`; + if (errorData.details) { + errorInfo += ` (${errorData.details})`; + } + } catch { + const errorText = await response.text(); + errorInfo += ` - ${errorText}`; + } + throw new Error(errorInfo); + } + + const result = await response.json(); + if (isDevelopment) { + console.log('保存成功:', result); + } + successCount++; + } catch (error) { + console.error(`保存 ${accountId} ${date} 数据失败:`, error); + errorCount++; + errors.push({ + accountId, + date, + error: error instanceof Error ? error.message : '未知错误' + }); + } + }; + + savePromises.push(saveTask()); + } + } + + // 并行执行所有保存任务 + await Promise.all(savePromises); + + // 清除所有已编辑数据并重新获取数据 + setEditedData({}); + + // 重新获取最新数据 + await fetchAccounts(); + + const endTime = performance.now(); + const duration = Math.round(endTime - startTime); + + if (errorCount === 0) { + message.success(`成功保存 ${successCount} 条记录 (耗时 ${duration}ms)`); + } else { + message.warning(`保存完成:成功 ${successCount} 条,失败 ${errorCount} 条 (耗时 ${duration}ms)`); + if (isDevelopment && errors.length > 0) { + console.error('保存失败的记录:', errors); + } + } + } catch (error) { + console.error('批量保存失败:', error); + message.error('保存失败,请重试'); + } finally { + setSaving(false); + } + }, [editedData, message, fetchAccounts, isDevelopment]); + + // ▸ 表格列配置模块 + const columns: ProColumns[] = useMemo( + () => [ + { + title: '账号信息', + dataIndex: '账号编号', + fixed: 'left', + align: 'center', + width: CONSTANTS.COLUMN_WIDTHS.ACCOUNT_INFO, + key: '账号编号', + render: (_: unknown, record: IAccount) => { + const monthlyGrowth = calculateMonthlyGrowth(record, selectedMonth); + return ( + + ); + }, + }, + ...generateDateColumns(handleEdit, editedData, accounts, mergedAccounts, selectedMonth, saving), + ], + [handleEdit, editedData, accounts, mergedAccounts, selectedMonth, saving, isDevelopment, handleDeleteAccountGrowthData], + ); + + // ▸ 月份选择处理模块 + const handleMonthChange = useCallback((value: Dayjs | null) => { + setSelectedMonth(value || dayjs()); + setEditedData({}); // 清空编辑状态 + setShouldAutoScroll(true); // 切换月份时重新启用自动滚动 + // 月份改变时尝试滚动到当前日期 + setTimeout(() => scrollToCurrentDate(), 100); + }, [scrollToCurrentDate]); + + // ▸ 主渲染模块 + return ( +
+ {/* 控制面板和统计概览 */} + + {/* 减小垂直间距 */} +
+ + 选择月份: + + + {Object.keys(editedData).length > 0 && ( + + {Object.values(editedData).reduce((total, accountData) => + total + Object.keys(accountData).length, 0 + )} 条待保存 + + )} + + + + {/* 减小卡片间距 */} + + 0 ? 'up' : statistics.totalMonthlyGrowth < 0 ? 'down' : 'stable'} + color={CONSTANTS.COLORS.SUCCESS} + /> + + + + + + + + + + + + + + + + {/* 数据表格 */} + + + sticky + scroll={{ + y: CONSTANTS.SCROLL_HEIGHT, + x: 'max-content', + }} + pagination={false} + size="small" + rowKey="_id" + columns={columns} + dataSource={accounts} + search={false} + editable={{ + type: 'multiple', + }} + toolBarRender={false} + cardBordered={false} + locale={{ + emptyText: ( +
+ 暂无账号数据,请先添加账号 +
+ ) + }} + rowClassName={(_record, index) => + index % 2 === 0 ? 'ant-table-row-even' : 'ant-table-row-odd' + } + /> +
+ + ); +}; + +export default AccountGrowthTable; diff --git a/src/pages/test/markdown/22.tsx b/src/pages/test/markdown/22.tsx new file mode 100644 index 0000000..845b3c8 --- /dev/null +++ b/src/pages/test/markdown/22.tsx @@ -0,0 +1,32 @@ +//src\pages\test\markdown\index.tsx +import { useEffect, useState } from 'react'; +import Markdown from '@/components/markdown2'; // 引入你刚刚创建的 Markdown 组件 + +export default function MarkdownPage() { + const [markdownContent, setMarkdownContent] = useState(null); + + // 使用 useEffect 钩子来加载 public 文件夹中的 Markdown 文件内容 + useEffect(() => { + // 从 public 文件夹中获取文件内容 + fetch('/md2.md') + + .then((response) => response.text()) + .then((text) => { + setMarkdownContent(text); + }) + .catch((error) => { + console.error('Error loading Markdown file:', error); + }); + }, []); + + // 当文件内容加载后,显示 Markdown 组件 + return ( +
+ {markdownContent ? ( + {markdownContent} + ) : ( +

Loading Markdown content...

+ )} +
+ ); +} diff --git a/src/pages/test/markdown/index.tsx b/src/pages/test/markdown/index.tsx new file mode 100644 index 0000000..ce21928 --- /dev/null +++ b/src/pages/test/markdown/index.tsx @@ -0,0 +1,31 @@ +//src\pages\test\markdown\index.tsx +import { useEffect, useState } from 'react'; +import Markdown from '@/components/markdown'; // 引入你刚刚创建的 Markdown 组件 + +export default function MarkdownPage() { + const [markdownContent, setMarkdownContent] = useState(null); + + // 使用 useEffect 钩子来加载 public 文件夹中的 Markdown 文件内容 + useEffect(() => { + // 从 public 文件夹中获取文件内容 + fetch('/test2.md') + .then((response) => response.text()) + .then((text) => { + setMarkdownContent(text); + }) + .catch((error) => { + console.error('Error loading Markdown file:', error); + }); + }, []); + + // 当文件内容加载后,显示 Markdown 组件 + return ( +
+ {markdownContent ? ( + {markdownContent} + ) : ( +

Loading Markdown content...

+ )} +
+ ); +} diff --git a/src/pages/test/userstore-debug.tsx b/src/pages/test/userstore-debug.tsx new file mode 100644 index 0000000..7b72a62 --- /dev/null +++ b/src/pages/test/userstore-debug.tsx @@ -0,0 +1,257 @@ +/** + * 文件: src/pages/test/userstore-debug.tsx + * 作者: 阿瑞 + * 功能: 用户状态管理调试页面 + * 版本: v1.0.1 + */ + +import React, { useState, useEffect, useMemo } from 'react'; +import { Card, Typography, Row, Col, Alert, Tag, Descriptions } from 'antd'; +import { + HomeOutlined, + InfoCircleOutlined +} from '@ant-design/icons'; + +// 导入用户状态相关的钩子函数 +import { + useUserInfo, + useAccessToken, + useRefreshToken, + //useUserActions, + useUserToken, + useUserHomePath +} from '@/store/userStore'; + +const { Title, Text } = Typography; + +// 定义 localStorage 状态接口 +interface LocalStorageState { + hasUserInfo: boolean; + hasAccessToken: boolean; + hasRefreshToken: boolean; +} + +/** + * 用户状态调试页面主组件 + * 用于展示和测试用户状态管理相关功能 + */ +const UserStoreDebugPage: React.FC = () => { + // 获取用户状态相关数据 + const userInfo = useUserInfo(); + const accessToken = useAccessToken(); + const refreshToken = useRefreshToken(); + //const userActions = useUserActions(); + const userToken = useUserToken(); + const userHomePath = useUserHomePath(); + + // 本地存储状态 + const [localStorageState, setLocalStorageState] = useState({ + hasUserInfo: false, + hasAccessToken: false, + hasRefreshToken: false, + }); + + // 使用 useMemo 优化 JSON 字符串化 + const userInfoJson = useMemo(() => JSON.stringify(userInfo, null, 2), [userInfo]); + + // 安全检查 localStorage 状态 + useEffect(() => { + const checkLocalStorage = () => { + if (typeof window !== 'undefined') { + setLocalStorageState({ + hasUserInfo: !!localStorage.getItem('userInfo'), + hasAccessToken: !!localStorage.getItem('userAccessToken'), + hasRefreshToken: !!localStorage.getItem('userRefreshToken'), + }); + } + }; + + checkLocalStorage(); + + // 每2秒检查一次 localStorage 状态变化(减少频率) + const interval = setInterval(checkLocalStorage, 2000); + + return () => clearInterval(interval); + }, []); + + return ( +
+ {/* 页面标题和说明 */} +
+ + <InfoCircleOutlined style={{ marginRight: '8px' }} /> + 用户状态管理调试面板 + + +
+ + + {/* 用户信息展示 */} +
+ Hook} + > + + + {userInfo._id || 未设置} + + + {userInfo.姓名 || 未设置} + + + {userInfo.邮箱 || 未设置} + + + {userInfo.电话 || 未设置} + + + {userInfo.角色?.名称 || 未设置} + + + {userInfo.微信昵称 || 未设置} + + +
+ JSON 数据: +
+                {userInfoJson}
+              
+
+
+ + + {/* 令牌信息展示 */} + + Tokens} + > + + + + {accessToken || '未设置'} + + + + + {refreshToken || '未设置'} + + + + + {userToken.accessToken || '未设置'} + + + + + {userToken.refreshToken || '未设置'} + + + + + + + {/* 用户主页路径展示 */} + + Navigation} + > +
+ +
+ + {userHomePath} + +
+ + 基于用户角色自动计算的主页路径 + +
+
+ + + {/* 用户操作方法展示 */} + + Actions} + > + + + function 设置用户信息 + + + function 设置用户令牌 + + + function 清除用户数据 + + + async function 获取用户信息 + + + + + + + {/* 调试信息 */} + Debug Info} + > + + + localStorage 状态: +
    +
  • + userInfo: {localStorageState.hasUserInfo ? + ✓ 存在 : + ✗ 不存在 + } +
  • +
  • + userAccessToken: {localStorageState.hasAccessToken ? + ✓ 存在 : + ✗ 不存在 + } +
  • +
  • + userRefreshToken: {localStorageState.hasRefreshToken ? + ✓ 存在 : + ✗ 不存在 + } +
  • +
+ + + 运行环境: +
    +
  • typeof window: {typeof window}
  • +
  • Node.js 环境: + {typeof window === 'undefined' ? '是' : '否'} +
  • +
+ + + + + ); +}; + +export default UserStoreDebugPage; \ No newline at end of file diff --git a/src/store/userStore.ts b/src/store/userStore.ts index 00ccb4a..5a9ffb0 100644 --- a/src/store/userStore.ts +++ b/src/store/userStore.ts @@ -117,5 +117,9 @@ export const useUserInfo = () => useUserStore((state: UserStore) => state.userIn export const useAccessToken = () => useUserStore((state: UserStore) => state.accessToken); export const useRefreshToken = () => useUserStore((state: UserStore) => state.refreshToken); export const useUserActions = () => useUserStore((state: UserStore) => state.actions); -export const useUserToken = () => useUserStore((state: UserStore) => ({ accessToken: state.accessToken, refreshToken: state.refreshToken })); +export const useUserToken = () => { + const accessToken = useUserStore((state: UserStore) => state.accessToken); + const refreshToken = useUserStore((state: UserStore) => state.refreshToken); + return { accessToken, refreshToken }; +}; export default useUserStore; diff --git a/src/utils/highlight.ts b/src/utils/highlight.ts new file mode 100644 index 0000000..58e70ec --- /dev/null +++ b/src/utils/highlight.ts @@ -0,0 +1,17 @@ +import 'highlight.js/styles/base16/tomorrow-night.css'; + +import hljs from 'highlight.js'; + +declare global { + interface Window { + hljs: any; + } +} + +hljs.configure({ + languages: ['javascript', 'sh', 'bash', 'html', 'scss', 'css', 'json'], +}); + +if (typeof window !== 'undefined') { + window.hljs = hljs; +}