This commit is contained in:
@@ -4,7 +4,7 @@ const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
reactStrictMode: true,
|
||||
|
||||
// 启用 standalone 输出模式,用于 Docker 部署
|
||||
// 启用 standalone 输出模式,用于 Docker 部署(Windows 本地开发时可能需要注释掉)
|
||||
output: 'standalone',
|
||||
|
||||
// 优化生产构建
|
||||
|
||||
@@ -16,26 +16,33 @@
|
||||
"@iconify/react": "^4.1.1",
|
||||
"@types/lodash": "^4.17.17",
|
||||
"antd": "^5.25.4",
|
||||
"apexcharts": "^4.7.0",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"color": "^5.0.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"geist": "^1.4.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"mongoose": "^8.15.1",
|
||||
"next": "15.3.3",
|
||||
"ramda": "^0.30.1",
|
||||
"react": "^19.0.0",
|
||||
"react-apexcharts": "^1.7.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-error-boundary": "^6.0.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"styled-components": "^6.0.9",
|
||||
"uuid": "^11.1.0",
|
||||
"zustand": "^5.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/jsonwebtoken": "^9.0.9",
|
||||
"@types/node": "^20",
|
||||
"@types/ramda": "^0.30.2",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
|
||||
160
pnpm-lock.yaml
generated
160
pnpm-lock.yaml
generated
@@ -29,9 +29,15 @@ importers:
|
||||
antd:
|
||||
specifier: ^5.25.4
|
||||
version: 5.25.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
apexcharts:
|
||||
specifier: ^4.7.0
|
||||
version: 4.7.0
|
||||
bcryptjs:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2
|
||||
color:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
dayjs:
|
||||
specifier: ^1.11.13
|
||||
version: 1.11.13
|
||||
@@ -50,9 +56,15 @@ importers:
|
||||
next:
|
||||
specifier: 15.3.3
|
||||
version: 15.3.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
ramda:
|
||||
specifier: ^0.30.1
|
||||
version: 0.30.1
|
||||
react:
|
||||
specifier: ^19.0.0
|
||||
version: 19.1.0
|
||||
react-apexcharts:
|
||||
specifier: ^1.7.0
|
||||
version: 1.7.0(apexcharts@4.7.0)(react@19.1.0)
|
||||
react-dom:
|
||||
specifier: ^19.0.0
|
||||
version: 19.1.0(react@19.1.0)
|
||||
@@ -65,6 +77,9 @@ importers:
|
||||
styled-components:
|
||||
specifier: ^6.0.9
|
||||
version: 6.1.18(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
uuid:
|
||||
specifier: ^11.1.0
|
||||
version: 11.1.0
|
||||
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))
|
||||
@@ -78,12 +93,18 @@ importers:
|
||||
'@types/node':
|
||||
specifier: ^20
|
||||
version: 20.17.57
|
||||
'@types/ramda':
|
||||
specifier: ^0.30.2
|
||||
version: 0.30.2
|
||||
'@types/react':
|
||||
specifier: ^19
|
||||
version: 19.1.6
|
||||
'@types/react-dom':
|
||||
specifier: ^19
|
||||
version: 19.1.5(@types/react@19.1.6)
|
||||
'@types/uuid':
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.0
|
||||
tailwindcss:
|
||||
specifier: ^4
|
||||
version: 4.1.8
|
||||
@@ -553,6 +574,31 @@ packages:
|
||||
react: '>=18.0.0'
|
||||
react-dom: '>=18.0.0'
|
||||
|
||||
'@svgdotjs/svg.draggable.js@3.0.6':
|
||||
resolution: {integrity: sha512-7iJFm9lL3C40HQcqzEfezK2l+dW2CpoVY3b77KQGqc8GXWa6LhhmX5Ckv7alQfUXBuZbjpICZ+Dvq1czlGx7gA==}
|
||||
peerDependencies:
|
||||
'@svgdotjs/svg.js': ^3.2.4
|
||||
|
||||
'@svgdotjs/svg.filter.js@3.0.9':
|
||||
resolution: {integrity: sha512-/69XMRCDoam2HgC4ldHIaDgeQf1ViHIsa0Ld4uWgiXtZ+E24DWHe/9Ib6kbNiZ7WRIdlVokUDR1Fg0kjIpkfbw==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
'@svgdotjs/svg.js@3.2.4':
|
||||
resolution: {integrity: sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg==}
|
||||
|
||||
'@svgdotjs/svg.resize.js@2.0.5':
|
||||
resolution: {integrity: sha512-4heRW4B1QrJeENfi7326lUPYBCevj78FJs8kfeDxn5st0IYPIRXoTtOSYvTzFWgaWWXd3YCDE6ao4fmv91RthA==}
|
||||
engines: {node: '>= 14.18'}
|
||||
peerDependencies:
|
||||
'@svgdotjs/svg.js': ^3.2.4
|
||||
'@svgdotjs/svg.select.js': ^4.0.1
|
||||
|
||||
'@svgdotjs/svg.select.js@4.0.3':
|
||||
resolution: {integrity: sha512-qkMgso1sd2hXKd1FZ1weO7ANq12sNmQJeGDjs46QwDVsxSRcHmvWKL2NDF7Yimpwf3sl5esOLkPqtV2bQ3v/Jg==}
|
||||
engines: {node: '>= 14.18'}
|
||||
peerDependencies:
|
||||
'@svgdotjs/svg.js': ^3.2.4
|
||||
|
||||
'@swc/counter@0.1.3':
|
||||
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
|
||||
|
||||
@@ -659,6 +705,9 @@ packages:
|
||||
'@types/node@20.17.57':
|
||||
resolution: {integrity: sha512-f3T4y6VU4fVQDKVqJV4Uppy8c1p/sVvS3peyqxyWnzkqXFJLRU7Y1Bl7rMS1Qe9z0v4M6McY0Fp9yBsgHJUsWQ==}
|
||||
|
||||
'@types/ramda@0.30.2':
|
||||
resolution: {integrity: sha512-PyzHvjCalm2BRYjAU6nIB3TprYwMNOUY/7P/N8bSzp9W/yM2YrtGtAnnVtaCNSeOZ8DzKyFDvaqQs7LnWwwmBA==}
|
||||
|
||||
'@types/react-dom@19.1.5':
|
||||
resolution: {integrity: sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg==}
|
||||
peerDependencies:
|
||||
@@ -670,6 +719,9 @@ packages:
|
||||
'@types/stylis@4.2.5':
|
||||
resolution: {integrity: sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==}
|
||||
|
||||
'@types/uuid@10.0.0':
|
||||
resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==}
|
||||
|
||||
'@types/webidl-conversions@7.0.3':
|
||||
resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==}
|
||||
|
||||
@@ -684,6 +736,9 @@ packages:
|
||||
peerDependencies:
|
||||
react: '*'
|
||||
|
||||
'@yr/monotone-cubic-spline@1.0.3':
|
||||
resolution: {integrity: sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==}
|
||||
|
||||
add-dom-event-listener@1.1.0:
|
||||
resolution: {integrity: sha512-WCxx1ixHT0GQU9hb0KI/mhgRQhnU+U3GvwY6ZvVjYq8rsihIGoaIOUbY0yMPBxLH5MDtr0kz3fisWGNcbWW7Jw==}
|
||||
|
||||
@@ -693,6 +748,9 @@ packages:
|
||||
react: '>=16.9.0'
|
||||
react-dom: '>=16.9.0'
|
||||
|
||||
apexcharts@4.7.0:
|
||||
resolution: {integrity: sha512-iZSrrBGvVlL+nt2B1NpqfDuBZ9jX61X9I2+XV0hlYXHtTwhwLTHDKGXjNXAgFBDLuvSYCB/rq2nPWVPRv2DrGA==}
|
||||
|
||||
bcryptjs@3.0.2:
|
||||
resolution: {integrity: sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==}
|
||||
hasBin: true
|
||||
@@ -728,16 +786,32 @@ packages:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
|
||||
color-convert@3.1.0:
|
||||
resolution: {integrity: sha512-TVoqAq8ZDIpK5lsQY874DDnu65CSsc9vzq0wLpNQ6UMBq81GSZocVazPiBbYGzngzBOIRahpkTzCLVe2at4MfA==}
|
||||
engines: {node: '>=14.6'}
|
||||
|
||||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
color-name@2.0.0:
|
||||
resolution: {integrity: sha512-SbtvAMWvASO5TE2QP07jHBMXKafgdZz8Vrsrn96fiL+O92/FN/PLARzUW5sKt013fjAprK2d2iCn2hk2Xb5oow==}
|
||||
engines: {node: '>=12.20'}
|
||||
|
||||
color-string@1.9.1:
|
||||
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
|
||||
|
||||
color-string@2.0.1:
|
||||
resolution: {integrity: sha512-5z9FbYTZPAo8iKsNEqRNv+OlpBbDcoE+SY9GjLfDUHEfcNNV7tS9eSAlFHEaub/r5tBL9LtskAeq1l9SaoZ5tQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
color@4.2.3:
|
||||
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
|
||||
engines: {node: '>=12.5.0'}
|
||||
|
||||
color@5.0.0:
|
||||
resolution: {integrity: sha512-16BlyiuyLq3MLxpRWyOTiWsO3ii/eLQLJUQXBSNcxMBBSnyt1ee9YUdaozQp03ifwm5woztEZGDbk9RGVuCsdw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
compute-scroll-into-view@3.1.1:
|
||||
resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==}
|
||||
|
||||
@@ -1034,6 +1108,9 @@ packages:
|
||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
ramda@0.30.1:
|
||||
resolution: {integrity: sha512-tEF5I22zJnuclswcZMc8bDIrwRHRzf+NqVEmqg50ShAZMP7MWeR/RGDthfM/p+BlqvF2fXAzpn8i+SJcYD3alw==}
|
||||
|
||||
rc-cascader@3.34.0:
|
||||
resolution: {integrity: sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag==}
|
||||
peerDependencies:
|
||||
@@ -1271,6 +1348,12 @@ packages:
|
||||
react: '>=16.9.0'
|
||||
react-dom: '>=16.9.0'
|
||||
|
||||
react-apexcharts@1.7.0:
|
||||
resolution: {integrity: sha512-03oScKJyNLRf0Oe+ihJxFZliBQM9vW3UWwomVn4YVRTN1jsIR58dLWt0v1sb8RwJVHDMbeHiKQueM0KGpn7nOA==}
|
||||
peerDependencies:
|
||||
apexcharts: '>=4.0.0'
|
||||
react: '>=0.13'
|
||||
|
||||
react-dom@19.1.0:
|
||||
resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==}
|
||||
peerDependencies:
|
||||
@@ -1408,12 +1491,18 @@ packages:
|
||||
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
ts-toolbelt@9.6.0:
|
||||
resolution: {integrity: sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==}
|
||||
|
||||
tslib@2.6.2:
|
||||
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
types-ramda@0.30.1:
|
||||
resolution: {integrity: sha512-1HTsf5/QVRmLzcGfldPFvkVsAdi1db1BBKzi7iW3KBUlOICg/nKnFS+jGqDJS3YD8VsWbAh7JiHeBvbsw8RPxA==}
|
||||
|
||||
typescript@5.8.3:
|
||||
resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
|
||||
engines: {node: '>=14.17'}
|
||||
@@ -1427,6 +1516,10 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
uuid@11.1.0:
|
||||
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
|
||||
hasBin: true
|
||||
|
||||
warning@4.0.3:
|
||||
resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==}
|
||||
|
||||
@@ -2000,6 +2093,25 @@ snapshots:
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
react-is: 18.3.1
|
||||
|
||||
'@svgdotjs/svg.draggable.js@3.0.6(@svgdotjs/svg.js@3.2.4)':
|
||||
dependencies:
|
||||
'@svgdotjs/svg.js': 3.2.4
|
||||
|
||||
'@svgdotjs/svg.filter.js@3.0.9':
|
||||
dependencies:
|
||||
'@svgdotjs/svg.js': 3.2.4
|
||||
|
||||
'@svgdotjs/svg.js@3.2.4': {}
|
||||
|
||||
'@svgdotjs/svg.resize.js@2.0.5(@svgdotjs/svg.js@3.2.4)(@svgdotjs/svg.select.js@4.0.3(@svgdotjs/svg.js@3.2.4))':
|
||||
dependencies:
|
||||
'@svgdotjs/svg.js': 3.2.4
|
||||
'@svgdotjs/svg.select.js': 4.0.3(@svgdotjs/svg.js@3.2.4)
|
||||
|
||||
'@svgdotjs/svg.select.js@4.0.3(@svgdotjs/svg.js@3.2.4)':
|
||||
dependencies:
|
||||
'@svgdotjs/svg.js': 3.2.4
|
||||
|
||||
'@swc/counter@0.1.3': {}
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
@@ -2091,6 +2203,10 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 6.19.8
|
||||
|
||||
'@types/ramda@0.30.2':
|
||||
dependencies:
|
||||
types-ramda: 0.30.1
|
||||
|
||||
'@types/react-dom@19.1.5(@types/react@19.1.6)':
|
||||
dependencies:
|
||||
'@types/react': 19.1.6
|
||||
@@ -2101,6 +2217,8 @@ snapshots:
|
||||
|
||||
'@types/stylis@4.2.5': {}
|
||||
|
||||
'@types/uuid@10.0.0': {}
|
||||
|
||||
'@types/webidl-conversions@7.0.3': {}
|
||||
|
||||
'@types/whatwg-url@11.0.5':
|
||||
@@ -2113,6 +2231,8 @@ snapshots:
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
|
||||
'@yr/monotone-cubic-spline@1.0.3': {}
|
||||
|
||||
add-dom-event-listener@1.1.0:
|
||||
dependencies:
|
||||
object-assign: 4.1.1
|
||||
@@ -2175,6 +2295,15 @@ snapshots:
|
||||
- luxon
|
||||
- moment
|
||||
|
||||
apexcharts@4.7.0:
|
||||
dependencies:
|
||||
'@svgdotjs/svg.draggable.js': 3.0.6(@svgdotjs/svg.js@3.2.4)
|
||||
'@svgdotjs/svg.filter.js': 3.0.9
|
||||
'@svgdotjs/svg.js': 3.2.4
|
||||
'@svgdotjs/svg.resize.js': 2.0.5(@svgdotjs/svg.js@3.2.4)(@svgdotjs/svg.select.js@4.0.3(@svgdotjs/svg.js@3.2.4))
|
||||
'@svgdotjs/svg.select.js': 4.0.3(@svgdotjs/svg.js@3.2.4)
|
||||
'@yr/monotone-cubic-spline': 1.0.3
|
||||
|
||||
bcryptjs@3.0.2: {}
|
||||
|
||||
bson@6.10.4: {}
|
||||
@@ -2200,21 +2329,36 @@ snapshots:
|
||||
color-name: 1.1.4
|
||||
optional: true
|
||||
|
||||
color-convert@3.1.0:
|
||||
dependencies:
|
||||
color-name: 2.0.0
|
||||
|
||||
color-name@1.1.4:
|
||||
optional: true
|
||||
|
||||
color-name@2.0.0: {}
|
||||
|
||||
color-string@1.9.1:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
simple-swizzle: 0.2.2
|
||||
optional: true
|
||||
|
||||
color-string@2.0.1:
|
||||
dependencies:
|
||||
color-name: 2.0.0
|
||||
|
||||
color@4.2.3:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
color-string: 1.9.1
|
||||
optional: true
|
||||
|
||||
color@5.0.0:
|
||||
dependencies:
|
||||
color-convert: 3.1.0
|
||||
color-string: 2.0.1
|
||||
|
||||
compute-scroll-into-view@3.1.1: {}
|
||||
|
||||
copy-to-clipboard@3.3.3:
|
||||
@@ -2475,6 +2619,8 @@ snapshots:
|
||||
|
||||
punycode@2.3.1: {}
|
||||
|
||||
ramda@0.30.1: {}
|
||||
|
||||
rc-cascader@3.34.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.4
|
||||
@@ -2811,6 +2957,12 @@ snapshots:
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
react-apexcharts@1.7.0(apexcharts@4.7.0)(react@19.1.0):
|
||||
dependencies:
|
||||
apexcharts: 4.7.0
|
||||
prop-types: 15.8.1
|
||||
react: 19.1.0
|
||||
|
||||
react-dom@19.1.0(react@19.1.0):
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
@@ -2952,10 +3104,16 @@ snapshots:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
ts-toolbelt@9.6.0: {}
|
||||
|
||||
tslib@2.6.2: {}
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
types-ramda@0.30.1:
|
||||
dependencies:
|
||||
ts-toolbelt: 9.6.0
|
||||
|
||||
typescript@5.8.3: {}
|
||||
|
||||
undici-types@6.19.8: {}
|
||||
@@ -2964,6 +3122,8 @@ snapshots:
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
|
||||
uuid@11.1.0: {}
|
||||
|
||||
warning@4.0.3:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
|
||||
30
src/components/chart/chart.tsx
Normal file
30
src/components/chart/chart.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 文件: chart/chart.tsx
|
||||
* 作者: 阿瑞
|
||||
* 功能: ApexChart 图表组件封装 - 支持 SSR
|
||||
* 版本: v1.0.0
|
||||
*/
|
||||
import dynamic from 'next/dynamic';
|
||||
import { memo } from 'react';
|
||||
import type { Props as ApexChartProps } from 'react-apexcharts';
|
||||
//import { StyledApexChart } from './styles';
|
||||
|
||||
//import { useSettings } from '@/store/settingStore';
|
||||
//import { useThemeToken } from '@/theme/hooks';
|
||||
|
||||
// 使用 next/dynamic 动态导入 ApexChart,并禁用 SSR
|
||||
const ApexChart = dynamic(() => import('react-apexcharts'), { ssr: false });
|
||||
function Chart(props: ApexChartProps) {
|
||||
// 检查是否在客户端环境
|
||||
if (typeof window === 'undefined') {
|
||||
return null; // 如果是在服务器端渲染,则返回 null,不加载图表
|
||||
}
|
||||
//const { themeMode } = useSettings();
|
||||
//const theme = useThemeToken();
|
||||
return (
|
||||
|
||||
<ApexChart {...props} />
|
||||
|
||||
);
|
||||
}
|
||||
export default memo(Chart);
|
||||
59
src/components/chart/styles.ts
Normal file
59
src/components/chart/styles.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
//src\components\chart\styles.ts
|
||||
import { GlobalToken } from 'antd';
|
||||
import Color from 'color';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { ThemeMode } from '@/types/enum';
|
||||
|
||||
export const StyledApexChart = styled.div<{ $thememode: ThemeMode; $theme: GlobalToken }>`
|
||||
.apexcharts-canvas {
|
||||
/* TOOLTIP */
|
||||
.apexcharts-tooltip {
|
||||
color: ${(props) => props.$theme.colorText};
|
||||
border-radius: 10px;
|
||||
backdrop-filter: blur(6px);
|
||||
background-color: ${(props) => Color(props.$theme.colorBgElevated).alpha(0.8).toString()};
|
||||
box-shadow: ${(props) =>
|
||||
props.$thememode === ThemeMode.Light
|
||||
? `rgba(145, 158, 171, 0.24) 0px 0px 2px 0px, rgba(145, 158, 171, 0.24) -20px 20px 40px -4px`
|
||||
: `rgba(0, 0, 0, 0.24) 0px 0px 2px 0px, rgba(0, 0, 0, 0.24) -20px 20px 40px -4px;`};
|
||||
.apexcharts-tooltip-title {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
background-color: rgba(145, 158, 171, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
/* TOOLTIP X */
|
||||
.apexcharts-xaxistooltip {
|
||||
color: ${(props) => props.$theme.colorText};
|
||||
border-radius: 10px;
|
||||
backdrop-filter: blur(6px);
|
||||
border-color: transparent;
|
||||
box-shadow: ${(props) =>
|
||||
props.$thememode === ThemeMode.Light
|
||||
? `rgba(145, 158, 171, 0.24) 0px 0px 2px 0px, rgba(145, 158, 171, 0.24) -20px 20px 40px -4px`
|
||||
: `rgba(0, 0, 0, 0.24) 0px 0px 2px 0px, rgba(0, 0, 0, 0.24) -20px 20px 40px -4px;`};
|
||||
background-color: ${(props) => Color(props.$theme.colorBgElevated).alpha(0.8).toString()};
|
||||
&::before {
|
||||
border-bottom-color: rgba(145, 158, 171, 0.24);
|
||||
}
|
||||
&::after {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
/* LEGEND */
|
||||
.apexcharts-legend {
|
||||
padding: 0;
|
||||
.apexcharts-legend-series {
|
||||
display: inline-flex !important;
|
||||
align-items: ecnter;
|
||||
}
|
||||
.apexcharts-legend-text {
|
||||
line-height: 18px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
210
src/components/chart/useChart.ts
Normal file
210
src/components/chart/useChart.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* 文件: chart/useChart.ts
|
||||
* 作者: 阿瑞
|
||||
* 功能: ApexCharts 主题配置 Hook
|
||||
* 版本: v2.0.0 - 修复 token 属性兼容性
|
||||
*/
|
||||
import { ApexOptions } from 'apexcharts';
|
||||
import { mergeDeepRight } from 'ramda';
|
||||
import { useThemeToken } from '@/theme/hooks';
|
||||
|
||||
export default function useChart(options: ApexOptions) {
|
||||
const theme = useThemeToken();
|
||||
|
||||
const LABEL_TOTAL = {
|
||||
show: true,
|
||||
label: 'Total',
|
||||
color: theme.colorTextSecondary,
|
||||
fontSize: `${theme.fontSizeLG || theme.fontSize || 16}px`,
|
||||
fontWeight: 'bold',
|
||||
};
|
||||
|
||||
const LABEL_VALUE = {
|
||||
offsetY: 8,
|
||||
color: theme.colorText,
|
||||
fontSize: `${theme.fontSizeLG || theme.fontSize || 14}px`,
|
||||
};
|
||||
|
||||
const baseOptions: ApexOptions = {
|
||||
// Colors
|
||||
colors: [
|
||||
theme.colorPrimary,
|
||||
theme.colorWarning || '#ff9640',
|
||||
theme.colorInfo || '#8e6bff',
|
||||
theme.colorError || '#ff4d4f',
|
||||
theme.colorSuccess || '#06d7b2',
|
||||
theme.colorWarningActive || theme.colorWarning || '#ff9640',
|
||||
theme.colorSuccessActive || theme.colorSuccess || '#06d7b2',
|
||||
theme.colorInfoActive || theme.colorInfo || '#8e6bff',
|
||||
theme.colorInfoText || theme.colorInfo || '#8e6bff',
|
||||
],
|
||||
|
||||
// Chart
|
||||
chart: {
|
||||
toolbar: { show: false },
|
||||
zoom: { enabled: false },
|
||||
foreColor: theme.colorTextTertiary || theme.colorTextSecondary,
|
||||
fontFamily: theme.fontFamily,
|
||||
},
|
||||
|
||||
// States
|
||||
states: {
|
||||
hover: {
|
||||
filter: {
|
||||
type: 'lighten',
|
||||
value: 0.04,
|
||||
} as any,
|
||||
},
|
||||
active: {
|
||||
filter: {
|
||||
type: 'darken',
|
||||
value: 0.88,
|
||||
} as any,
|
||||
},
|
||||
},
|
||||
|
||||
// Fill
|
||||
fill: {
|
||||
opacity: 1,
|
||||
gradient: {
|
||||
type: 'vertical',
|
||||
shadeIntensity: 0,
|
||||
opacityFrom: 0.4,
|
||||
opacityTo: 0,
|
||||
stops: [0, 100],
|
||||
},
|
||||
},
|
||||
|
||||
// Datalabels
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
|
||||
// Stroke
|
||||
stroke: {
|
||||
width: 3,
|
||||
curve: 'smooth',
|
||||
lineCap: 'round',
|
||||
},
|
||||
|
||||
// Grid
|
||||
grid: {
|
||||
strokeDashArray: 3,
|
||||
borderColor: theme.colorBorderSecondary || theme.colorBorder,
|
||||
xaxis: {
|
||||
lines: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Xaxis
|
||||
xaxis: {
|
||||
axisBorder: { show: false },
|
||||
axisTicks: { show: false },
|
||||
},
|
||||
|
||||
// Markers
|
||||
markers: {
|
||||
size: 0,
|
||||
},
|
||||
|
||||
// Tooltip
|
||||
tooltip: {
|
||||
theme: 'light',
|
||||
x: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
|
||||
// Legend
|
||||
legend: {
|
||||
show: true,
|
||||
fontSize: '13px',
|
||||
position: 'top',
|
||||
horizontalAlign: 'right',
|
||||
markers: {
|
||||
size: 12,
|
||||
},
|
||||
fontWeight: 500,
|
||||
itemMargin: {
|
||||
horizontal: 8,
|
||||
},
|
||||
labels: {
|
||||
colors: theme.colorText,
|
||||
},
|
||||
},
|
||||
|
||||
// plotOptions
|
||||
plotOptions: {
|
||||
// Bar
|
||||
bar: {
|
||||
borderRadius: 4,
|
||||
columnWidth: '28%',
|
||||
borderRadiusApplication: 'end',
|
||||
borderRadiusWhenStacked: 'last',
|
||||
},
|
||||
|
||||
// Pie + Donut
|
||||
pie: {
|
||||
donut: {
|
||||
labels: {
|
||||
show: true,
|
||||
value: LABEL_VALUE,
|
||||
total: LABEL_TOTAL,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Radialbar
|
||||
radialBar: {
|
||||
track: {
|
||||
strokeWidth: '100%',
|
||||
},
|
||||
dataLabels: {
|
||||
value: LABEL_VALUE,
|
||||
total: LABEL_TOTAL,
|
||||
},
|
||||
},
|
||||
|
||||
// Radar
|
||||
radar: {
|
||||
polygons: {
|
||||
fill: { colors: ['transparent'] },
|
||||
strokeColors: theme.colorBorderSecondary || theme.colorBorder,
|
||||
connectorColors: theme.colorBorderSecondary || theme.colorBorder,
|
||||
},
|
||||
},
|
||||
|
||||
// polarArea
|
||||
polarArea: {
|
||||
rings: {
|
||||
strokeColor: theme.colorBorderSecondary || theme.colorBorder,
|
||||
},
|
||||
spokes: {
|
||||
connectorColors: theme.colorBorderSecondary || theme.colorBorder,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Responsive
|
||||
responsive: [
|
||||
{
|
||||
// sm - 假设屏幕断点为 576px
|
||||
breakpoint: 576,
|
||||
options: {
|
||||
plotOptions: { bar: { columnWidth: '40%' } },
|
||||
},
|
||||
},
|
||||
{
|
||||
// md - 假设屏幕断点为 768px
|
||||
breakpoint: 768,
|
||||
options: {
|
||||
plotOptions: { bar: { columnWidth: '32%' } },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return mergeDeepRight(baseOptions, options) as ApexOptions;
|
||||
}
|
||||
90
src/pages/api/backstage/mine/info/[id].ts
Normal file
90
src/pages/api/backstage/mine/info/[id].ts
Normal file
@@ -0,0 +1,90 @@
|
||||
// src/pages/api/backstage/mine/info/[id].ts
|
||||
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import connectDB from '@/utils/connectDB';
|
||||
import { User } from '@/models';
|
||||
import { IPermission, IUser } from '@/models/types';
|
||||
import { buildPermissionTree } from '@/pages/api/buildPermissionTree';
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { id } = req.query;
|
||||
|
||||
if (!id) {
|
||||
return res.status(400).json({ error: '缺少用户ID' });
|
||||
}
|
||||
|
||||
switch (req.method) {
|
||||
case 'GET':
|
||||
try {
|
||||
const user = await User.findById(id)
|
||||
//关联团队
|
||||
.populate({
|
||||
path: '团队',
|
||||
select: '名称 拥有者',
|
||||
populate: [
|
||||
{ path: '拥有者', select: '姓名' },
|
||||
]
|
||||
})
|
||||
//关联角色
|
||||
.populate({
|
||||
path: '角色',
|
||||
select: '名称 描述 权限',
|
||||
populate: {
|
||||
path: '权限',
|
||||
populate: { path: '子级' },
|
||||
},
|
||||
})
|
||||
.select('-密码');
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: '用户不存在' });
|
||||
}
|
||||
|
||||
|
||||
// 构建权限树
|
||||
const permissionsTree = buildPermissionTree(
|
||||
user.角色.权限.map((perm: IPermission) => perm as IPermission),
|
||||
);
|
||||
|
||||
const userInfo = {
|
||||
_id: user._id,
|
||||
邮箱: user.邮箱,
|
||||
姓名: user.姓名,
|
||||
团队: user.团队, // 这将是一个完整的团队对象
|
||||
角色: {
|
||||
...user.角色.toObject(), // 将 Mongoose 文档转换为普通对象
|
||||
权限: permissionsTree, // 使用构建好的权限树替换原始权限列表
|
||||
},
|
||||
微信昵称: user.微信昵称,
|
||||
头像: user.头像,
|
||||
电话: user.电话,
|
||||
unionid: user.unionid,
|
||||
openid: user.openid,
|
||||
};
|
||||
|
||||
res.status(200).json(userInfo );
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: '服务器错误' });
|
||||
}
|
||||
break;
|
||||
|
||||
case 'PUT':
|
||||
try {
|
||||
const updateData = req.body as Partial<IUser>;
|
||||
|
||||
const user = await User.findByIdAndUpdate(id, updateData, { new: true }).select('-密码');
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: '用户不存在' });
|
||||
}
|
||||
res.status(200).json(user);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: '服务器错误' });
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
res.setHeader('Allow', ['GET', 'PUT']);
|
||||
res.status(405).end(`方法 ${req.method} 不被允许`);
|
||||
}
|
||||
};
|
||||
|
||||
export default connectDB(handler);
|
||||
33
src/pages/api/backstage/mine/sales/index.ts
Normal file
33
src/pages/api/backstage/mine/sales/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
// src/pages/api/backstage/mine/sales/index.ts
|
||||
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import connectDB from '@/utils/connectDB';
|
||||
import { SalesRecord } from '@/models';
|
||||
//import { ISalesRecord } from '@/models/types';
|
||||
|
||||
// 假设您有一个身份验证中间件来获取当前用户的ID,例如从 req.userId 获取
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
// 获取当前用户ID,您需要根据实际情况进行调整
|
||||
const userId = (req as any).userId as string;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: '未授权的访问' });
|
||||
}
|
||||
|
||||
switch (req.method) {
|
||||
case 'GET':
|
||||
try {
|
||||
const salesRecords = await SalesRecord.find({ 导购: userId });
|
||||
res.status(200).json(salesRecords);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: '服务器错误' });
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
res.setHeader('Allow', ['GET']);
|
||||
res.status(405).end(`方法 ${req.method} 不被允许`);
|
||||
}
|
||||
};
|
||||
|
||||
export default connectDB(handler);
|
||||
48
src/pages/api/backstage/sales/aftersale/index.ts
Normal file
48
src/pages/api/backstage/sales/aftersale/index.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
// 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 mongoose from 'mongoose';
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method === 'POST') {
|
||||
try {
|
||||
// 防御性检查,确保原产品和替换产品字段存在并且是数组
|
||||
const originalProductIds = Array.isArray(req.body.原产品)
|
||||
? req.body.原产品.map((id: string) => new mongoose.Types.ObjectId(id))
|
||||
: [];
|
||||
const replacementProductIds = Array.isArray(req.body.替换产品)
|
||||
? req.body.替换产品.map((id: string) => new mongoose.Types.ObjectId(id))
|
||||
: [];
|
||||
// 直接使用 req.body 传递的所有字段
|
||||
const newAfterSalesRecord = new AfterSalesRecord({
|
||||
...req.body,
|
||||
原产品: originalProductIds, // 保存原产品的 ObjectId 数组
|
||||
替换产品: replacementProductIds, // 保存替换产品的 ObjectId 数组
|
||||
日期: new Date(req.body.日期), // 确保日期字段是 Date 类型
|
||||
售后进度: '待处理', // 默认设置为 "待处理"
|
||||
});
|
||||
// 保存售后记录到数据库
|
||||
//await newAfterSalesRecord.save();
|
||||
|
||||
// 保存售后记录到数据库
|
||||
const savedAfterSalesRecord = await newAfterSalesRecord.save();
|
||||
|
||||
// 更新对应的销售记录,将新创建的售后记录的ID添加到 "售后记录" 数组中
|
||||
await SalesRecord.findByIdAndUpdate(req.body.销售记录, {
|
||||
$push: { 售后记录: savedAfterSalesRecord._id }
|
||||
});
|
||||
|
||||
// 返回成功响应
|
||||
res.status(201).json({ message: '售后记录创建成功', record: newAfterSalesRecord });
|
||||
} catch (error) {
|
||||
console.error('售后记录创建失败:', error);
|
||||
res.status(500).json({ message: '售后记录创建失败' });
|
||||
}
|
||||
} else {
|
||||
res.setHeader('Allow', ['POST']);
|
||||
res.status(405).end(`Method ${req.method} Not Allowed`);
|
||||
}
|
||||
};
|
||||
|
||||
export default connectDB(handler);
|
||||
81
src/pages/api/backstage/sales/aftersale/records/[id].ts
Normal file
81
src/pages/api/backstage/sales/aftersale/records/[id].ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import connectDB from '@/utils/connectDB'; // 确保数据库连接
|
||||
import { AfterSalesRecord } from '@/models'; // 导入售后记录模型
|
||||
//import { IAfterSalesRecord } from '@/models/types'; // 导入售后记录类型定义
|
||||
import { isValidObjectId } from 'mongoose'; // Mongoose 工具函数
|
||||
|
||||
// 定义 API 处理函数
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { method } = req;
|
||||
const { id } = req.query;
|
||||
|
||||
if (!isValidObjectId(id)) {
|
||||
return res.status(400).json({ message: '无效的售后记录 ID' });
|
||||
}
|
||||
|
||||
switch (method) {
|
||||
case 'PATCH':
|
||||
try {
|
||||
const updateData = req.body; // 接收前端传入的字段
|
||||
console.log('接收到的更新数据:', JSON.stringify(updateData, null, 2));
|
||||
|
||||
// 校验:根据不同的售后类型有不同的字段要求
|
||||
// 对于换货类型,放宽限制,只需要类型字段
|
||||
if (!updateData.类型) {
|
||||
return res.status(400).json({ message: '类型是必须的' });
|
||||
}
|
||||
|
||||
// 准备更新字段
|
||||
const updateFields = { ...updateData };
|
||||
|
||||
// 特殊处理替换产品字段,确保它是一个ID数组
|
||||
if (updateData.替换产品) {
|
||||
// 确保替换产品是一个数组
|
||||
const productIds = Array.isArray(updateData.替换产品)
|
||||
? updateData.替换产品
|
||||
: [updateData.替换产品];
|
||||
|
||||
// 验证每个ID是否有效
|
||||
const validProductIds = productIds.filter((id: string) => isValidObjectId(id));
|
||||
updateFields.替换产品 = validProductIds;
|
||||
}
|
||||
|
||||
// 特殊处理收支平台字段
|
||||
if (updateData.收支平台 === null) {
|
||||
// 如果前端明确传入null,则设置为null
|
||||
updateFields.收支平台 = null;
|
||||
} else if (updateData.收支平台 && updateData.收支平台._id) {
|
||||
// 如果是有效对象,则提取ID
|
||||
updateFields.收支平台 = updateData.收支平台._id;
|
||||
}
|
||||
|
||||
console.log('处理后的更新字段:', JSON.stringify(updateFields, null, 2));
|
||||
|
||||
// 使用 Mongoose 的 $set 操作动态更新传入的字段
|
||||
const updatedRecord = await AfterSalesRecord.findByIdAndUpdate(
|
||||
id,
|
||||
{ $set: updateFields, updatedAt: new Date() },
|
||||
{ new: true } // 返回更新后的记录
|
||||
).populate('替换产品'); // 确保返回替换产品的完整信息
|
||||
|
||||
if (!updatedRecord) {
|
||||
return res.status(404).json({ message: '售后记录未找到' });
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
message: '售后记录更新成功',
|
||||
data: updatedRecord,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('更新售后记录时出错:', error);
|
||||
res.status(500).json({ message: '服务器错误,更新售后记录失败' });
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
res.setHeader('Allow', ['PATCH']);
|
||||
res.status(405).end(`Method ${method} Not Allowed`);
|
||||
}
|
||||
};
|
||||
|
||||
export default connectDB(handler);
|
||||
84
src/pages/api/backstage/sales/aftersale/records/index.ts
Normal file
84
src/pages/api/backstage/sales/aftersale/records/index.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
//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 mongoose from 'mongoose';
|
||||
|
||||
interface IQuery {
|
||||
teamId?: string;
|
||||
salesRecordId?: string;
|
||||
}
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method === 'GET') {
|
||||
try {
|
||||
const { teamId, salesRecordId } = req.query as IQuery;
|
||||
|
||||
const query: any = {};
|
||||
|
||||
if (teamId) {
|
||||
query.团队 = new mongoose.Types.ObjectId(teamId);
|
||||
}
|
||||
|
||||
if (salesRecordId) {
|
||||
query.销售记录 = new mongoose.Types.ObjectId(salesRecordId);
|
||||
}
|
||||
|
||||
const afterSalesRecords = await AfterSalesRecord.find(query)
|
||||
.populate('团队')
|
||||
//.populate('销售记录')
|
||||
.populate({
|
||||
path: '销售记录',
|
||||
select: '订单来源 客户 导购 成交日期 订单状态 收款状态 收款平台 应收金额 收款金额 待收款',
|
||||
populate: [
|
||||
{ path: '客户', select: '姓名 电话 地址' },
|
||||
{ path: '导购', select: '姓名' },
|
||||
{ path: '订单来源', select: '账号编号' },
|
||||
{ path: '收款平台', select: '名称' },
|
||||
]
|
||||
})
|
||||
//原产品和替换产品排除图片字段
|
||||
.populate({
|
||||
path: '原产品',
|
||||
select: '名称 售价 成本 品类 品牌 供应商', // 选择需要的产品字段并排除图片
|
||||
populate: [
|
||||
{ path: '品类', select: 'name' }, // 填充品类,选择名称字段
|
||||
{ path: '品牌', select: 'name' }, // 填充品牌,选择名称字段
|
||||
{ path: '供应商', select: '供应商名称 联系方式' }, // 填充供应商,选择名称和联系方式字段
|
||||
]
|
||||
})
|
||||
.populate({
|
||||
path: '替换产品',
|
||||
select: '名称 售价 成本 品类 品牌 供应商', // 选择需要的产品字段并排除图片
|
||||
populate: [
|
||||
{ path: '品类', select: 'name' }, // 填充品类,选择名称字段
|
||||
{ path: '品牌', select: 'name' }, // 填充品牌,选择名称字段
|
||||
{ path: '供应商', select: '供应商名称 联系方式' }, // 填充供应商,选择名称和联系方式字段
|
||||
]
|
||||
})
|
||||
|
||||
//.populate('收支平台')
|
||||
.populate({
|
||||
path: '收支平台',
|
||||
select: '名称',
|
||||
})
|
||||
.populate({
|
||||
path: '收款码',
|
||||
select: '_id',
|
||||
})
|
||||
//按时间倒序排列
|
||||
.sort({ createdAt: -1 })
|
||||
.exec();
|
||||
|
||||
res.status(200).json({ records: afterSalesRecords });
|
||||
} catch (error) {
|
||||
console.error('获取售后记录失败:', error);
|
||||
res.status(500).json({ message: '获取售后记录失败' });
|
||||
}
|
||||
} else {
|
||||
res.setHeader('Allow', ['GET']);
|
||||
res.status(405).end(`Method ${req.method} Not Allowed`);
|
||||
}
|
||||
};
|
||||
|
||||
export default connectDB(handler);
|
||||
@@ -0,0 +1,71 @@
|
||||
//src\pages\api\backstage\sales\aftersale\records\updateProgress.ts
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import connectDB from '@/utils/connectDB';
|
||||
import { AfterSalesRecord, SalesRecord } from '@/models';
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { method } = req;
|
||||
|
||||
switch (method) {
|
||||
case 'PATCH':
|
||||
try {
|
||||
const { id, progress } = req.body;
|
||||
|
||||
if (!id || !progress) {
|
||||
return res.status(400).json({ message: '缺少必要的参数' });
|
||||
}
|
||||
|
||||
// 查找并更新售后进度
|
||||
const record = await AfterSalesRecord.findByIdAndUpdate(
|
||||
id,
|
||||
{ 售后进度: progress },
|
||||
{ new: true }
|
||||
).populate('销售记录');
|
||||
|
||||
if (!record) {
|
||||
return res.status(404).json({ message: '未找到对应的售后记录' });
|
||||
}
|
||||
|
||||
// 确保销售记录存在
|
||||
if (!record.销售记录 || !record.销售记录._id) {
|
||||
return res.status(404).json({ message: '未找到对应的销售记录' });
|
||||
}
|
||||
|
||||
// 更新销售记录的订单状态
|
||||
let orderStatusUpdate = '';
|
||||
if (progress === '处理中') {
|
||||
if (record.类型 === '退货') orderStatusUpdate = '退货中';
|
||||
if (record.类型 === '换货') orderStatusUpdate = '换货中';
|
||||
if (record.类型 === '补发') orderStatusUpdate = '补发中';
|
||||
} else if (progress === '已处理') {
|
||||
if (record.类型 === '退货') orderStatusUpdate = '已退款';
|
||||
if (record.类型 === '换货') orderStatusUpdate = '换货完成';
|
||||
if (record.类型 === '补发') orderStatusUpdate = '已补发';
|
||||
} else if (progress === '待处理') {
|
||||
if (record.类型 === '退货') orderStatusUpdate = '退货中';
|
||||
if (record.类型 === '换货') orderStatusUpdate = '换货中';
|
||||
if (record.类型 === '补发') orderStatusUpdate = '补发中';
|
||||
}
|
||||
|
||||
if (orderStatusUpdate) {
|
||||
await SalesRecord.findByIdAndUpdate(
|
||||
record.销售记录._id,
|
||||
{ 订单状态: orderStatusUpdate },
|
||||
{ new: true }
|
||||
);
|
||||
}
|
||||
|
||||
res.status(200).json({ message: '售后进度和订单状态更新成功', data: record });
|
||||
} catch (error) {
|
||||
console.error('售后进度更新失败:', error);
|
||||
res.status(500).json({ message: '服务器错误', error });
|
||||
}
|
||||
break;
|
||||
default:
|
||||
res.setHeader('Allow', ['PATCH']);
|
||||
res.status(405).json({ message: `不支持 ${method} 请求` });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export default connectDB(handler);
|
||||
22
src/pages/api/backstage/sales/aftersale/uploadPaymentCode.ts
Normal file
22
src/pages/api/backstage/sales/aftersale/uploadPaymentCode.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import connectDB from '@/utils/connectDB';
|
||||
import { CustomerPaymentCode } from '@/models';
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method === 'POST') {
|
||||
const { 收款码 } = req.body;
|
||||
|
||||
try {
|
||||
const paymentCode = new CustomerPaymentCode({ 收款码 });
|
||||
await paymentCode.save();
|
||||
|
||||
return res.status(200).json({ message: '收款码上传成功', paymentCodeId: paymentCode._id });
|
||||
} catch (error) {
|
||||
return res.status(500).json({ message: 'Internal server error', error });
|
||||
}
|
||||
} else {
|
||||
return res.status(405).json({ message: 'Method not allowed' });
|
||||
}
|
||||
};
|
||||
|
||||
export default connectDB(handler);
|
||||
65
src/pages/components/PaymentCodeImage.tsx
Normal file
65
src/pages/components/PaymentCodeImage.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
//src\pages\components\PaymentCodeImage.tsx
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Spin, Image } from 'antd';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
|
||||
interface PaymentCodeImageProps {
|
||||
paymentCodeId?: string;
|
||||
width?: number | string;
|
||||
height?: number | string;
|
||||
}
|
||||
|
||||
const PaymentCodeImageComponent: React.FC<PaymentCodeImageProps> = ({ paymentCodeId, width = '100%', height = 'auto' }) => {
|
||||
const [imageSrc, setImageSrc] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchImage = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/paymentCodeImage/${paymentCodeId}`);
|
||||
if (response.ok) {
|
||||
const imageBase64 = await response.text();
|
||||
setImageSrc(imageBase64);
|
||||
} else if (response.status === 404) {
|
||||
setImageSrc(null);
|
||||
} else {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch payment code image', error);
|
||||
setImageSrc(null);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (paymentCodeId) {
|
||||
fetchImage();
|
||||
}
|
||||
}, [paymentCodeId]);
|
||||
|
||||
const antIcon = <LoadingOutlined style={{ fontSize: 24 }} spin />;
|
||||
const fallbackImage = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200' viewBox='0 0 24 24' fill='none' stroke='%23adb5bd' stroke-width='1' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='3' width='18' height='18' rx='2' ry='2'/%3E%3Ccircle cx='8.5' cy='8.5' r='1.5'/%3E%3Cpolyline points='21 15 16 10 5 21'/%3E%3C/svg%3E";
|
||||
|
||||
return (
|
||||
isLoading ? (
|
||||
<div className="flex justify-center items-center" style={{ width, height }}>
|
||||
<Spin indicator={antIcon} />
|
||||
</div>
|
||||
) : imageSrc ? (
|
||||
<Image
|
||||
src={imageSrc}
|
||||
alt="Payment Code Image"
|
||||
fallback={fallbackImage}
|
||||
style={{ width, height }}
|
||||
//preview={false}
|
||||
//preview={{ visible: true }} // Enable image preview
|
||||
/>
|
||||
) : (
|
||||
<div className="text-gray-400 text-center" style={{ width, height }}>暂无图片</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentCodeImageComponent;
|
||||
88
src/pages/components/chart/index.tsx
Normal file
88
src/pages/components/chart/index.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Card, Col, Row } from 'antd';
|
||||
import ChartArea from './view/chart-area';
|
||||
import ChartBar from './view/chart-bar';
|
||||
import ChartColumnMultiple from './view/chart-column-multiple';
|
||||
import ChartColumnNegative from './view/chart-column-negative';
|
||||
import ChartColumnSingle from './view/chart-column-single';
|
||||
import ChartColumnStacked from './view/chart-column-Stacked';
|
||||
import ChartDonut from './view/chart-donut';
|
||||
import ChartLine from './view/chart-line';
|
||||
import ChartMixed from './view/chart-mixed';
|
||||
import ChartPie from './view/chart-pie';
|
||||
import ChartRadar from './view/chart-radar';
|
||||
import ChartRadial from './view/chart-radial';
|
||||
|
||||
export default function ChartPage() {
|
||||
//const { colorPrimary } = useThemeToken();
|
||||
return (
|
||||
<>
|
||||
<Row gutter={[16, 16]} justify="center">
|
||||
<Col span={23} lg={12}>
|
||||
<Card title="Area">
|
||||
<ChartArea />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={23} lg={12}>
|
||||
<Card title="Line">
|
||||
<ChartLine />
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col span={23} lg={12}>
|
||||
<Card title="Column Single">
|
||||
<ChartColumnSingle />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={23} lg={12}>
|
||||
<Card title="Column Multiple">
|
||||
<ChartColumnMultiple />
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col span={23} lg={12}>
|
||||
<Card title="Column Stacked">
|
||||
<ChartColumnStacked />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={23} lg={12}>
|
||||
<Card title="Column Negative">
|
||||
<ChartColumnNegative />
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col span={23} lg={12}>
|
||||
<Card title="Bar">
|
||||
<ChartBar />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={23} lg={12}>
|
||||
<Card title="Column Mixed">
|
||||
<ChartMixed />
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col span={24} lg={12}>
|
||||
<Card title="Pie">
|
||||
<ChartPie />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={23} lg={12}>
|
||||
<Card title="Donut">
|
||||
<ChartDonut />
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col span={23} lg={12}>
|
||||
<Card title="Radial Bar">
|
||||
<ChartRadial />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={23} lg={12}>
|
||||
<Card title="Radar">
|
||||
<ChartRadar />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}
|
||||
30
src/pages/components/chart/view/chart-area.tsx
Normal file
30
src/pages/components/chart/view/chart-area.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import Chart from '@/components/chart/chart';
|
||||
import useChart from '@/components/chart/useChart';
|
||||
|
||||
const series = [
|
||||
{ name: 'series1', data: [31, 40, 28, 51, 42, 109, 100] },
|
||||
{ name: 'series2', data: [11, 32, 45, 32, 34, 52, 41] },
|
||||
];
|
||||
export default function ChartArea() {
|
||||
const chartOptions = useChart({
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
categories: [
|
||||
'2018-09-19T00:00:00.000Z',
|
||||
'2018-09-19T01:30:00.000Z',
|
||||
'2018-09-19T02:30:00.000Z',
|
||||
'2018-09-19T03:30:00.000Z',
|
||||
'2018-09-19T04:30:00.000Z',
|
||||
'2018-09-19T05:30:00.000Z',
|
||||
'2018-09-19T06:30:00.000Z',
|
||||
],
|
||||
},
|
||||
tooltip: {
|
||||
x: {
|
||||
format: 'dd/MM/yy HH:mm',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return <Chart type="area" series={series} options={chartOptions} height={320} />;
|
||||
}
|
||||
29
src/pages/components/chart/view/chart-bar.tsx
Normal file
29
src/pages/components/chart/view/chart-bar.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import Chart from '@/components/chart/chart';
|
||||
import useChart from '@/components/chart/useChart';
|
||||
|
||||
const series = [400, 430, 448, 470, 540, 580, 690, 1100, 1200, 1380];
|
||||
|
||||
export default function ChartBar() {
|
||||
const chartOptions = useChart({
|
||||
stroke: { show: false },
|
||||
plotOptions: {
|
||||
bar: { horizontal: true, barHeight: '30%' },
|
||||
},
|
||||
xaxis: {
|
||||
categories: [
|
||||
'Italy',
|
||||
'Japan',
|
||||
'China',
|
||||
'Canada',
|
||||
'France',
|
||||
'Germany',
|
||||
'South Korea',
|
||||
'Netherlands',
|
||||
'United States',
|
||||
'United Kingdom',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
return <Chart type="bar" series={[{ data: series }]} options={chartOptions} height={320} />;
|
||||
}
|
||||
47
src/pages/components/chart/view/chart-column-Stacked.tsx
Normal file
47
src/pages/components/chart/view/chart-column-Stacked.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import Chart from '@/components/chart/chart';
|
||||
import useChart from '@/components/chart/useChart';
|
||||
|
||||
const series = [
|
||||
{ name: 'Product A', data: [44, 55, 41, 67, 22, 43] },
|
||||
{ name: 'Product B', data: [13, 23, 20, 8, 13, 27] },
|
||||
{ name: 'Product C', data: [11, 17, 15, 15, 21, 14] },
|
||||
{ name: 'Product D', data: [21, 7, 25, 13, 22, 8] },
|
||||
];
|
||||
export default function ChartColumnStacked() {
|
||||
const chartOptions = useChart({
|
||||
chart: {
|
||||
stacked: true,
|
||||
zoom: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
itemMargin: {
|
||||
vertical: 8,
|
||||
},
|
||||
position: 'right',
|
||||
offsetY: 20,
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
columnWidth: '16%',
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
show: false,
|
||||
},
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
categories: [
|
||||
'01/01/2011 GMT',
|
||||
'01/02/2011 GMT',
|
||||
'01/03/2011 GMT',
|
||||
'01/04/2011 GMT',
|
||||
'01/05/2011 GMT',
|
||||
'01/06/2011 GMT',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
return <Chart type="bar" series={series} options={chartOptions} height={320} />;
|
||||
}
|
||||
33
src/pages/components/chart/view/chart-column-multiple.tsx
Normal file
33
src/pages/components/chart/view/chart-column-multiple.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import Chart from '@/components/chart/chart';
|
||||
import useChart from '@/components/chart/useChart';
|
||||
|
||||
const series = [
|
||||
{
|
||||
name: 'Net Profit',
|
||||
data: [44, 55, 57, 56, 61, 58, 63, 60, 66],
|
||||
},
|
||||
{
|
||||
name: 'Revenue',
|
||||
data: [76, 85, 101, 98, 87, 105, 91, 114, 94],
|
||||
},
|
||||
];
|
||||
export default function ChartColumnMultiple() {
|
||||
const chartOptions = useChart({
|
||||
stroke: {
|
||||
show: true,
|
||||
width: 2,
|
||||
colors: ['transparent'],
|
||||
},
|
||||
xaxis: {
|
||||
categories: ['Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct'],
|
||||
},
|
||||
tooltip: {
|
||||
y: {
|
||||
formatter: (value: number) => `$ ${value} thousands`,
|
||||
},
|
||||
},
|
||||
plotOptions: { bar: { columnWidth: '36%' } },
|
||||
});
|
||||
|
||||
return <Chart type="bar" series={series} options={chartOptions} height={320} />;
|
||||
}
|
||||
80
src/pages/components/chart/view/chart-column-negative.tsx
Normal file
80
src/pages/components/chart/view/chart-column-negative.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import Chart from '@/components/chart/chart';
|
||||
import useChart from '@/components/chart/useChart';
|
||||
//import { useThemeToken } from '@/theme/hooks';
|
||||
|
||||
const series = [
|
||||
{
|
||||
name: 'Cash Flow',
|
||||
data: [
|
||||
1.45, 5.42, 5.9, -0.42, -12.6, -18.1, -18.2, -14.16, -11.1, -6.09, 0.34, 3.88, 13.07, 5.8, 2,
|
||||
7.37, 8.1, 13.57, 15.75, 17.1, 19.8, -27.03, -54.4, -47.2, -43.3, -18.6, -48.6, -41.1, -39.6,
|
||||
-37.6, -29.4, -21.4, -2.4,
|
||||
],
|
||||
},
|
||||
];
|
||||
export default function ChartColumnNegative() {
|
||||
//const theme = useThemeToken();
|
||||
const chartOptions = useChart({
|
||||
stroke: { show: false },
|
||||
yaxis: {
|
||||
labels: {
|
||||
formatter: (value: number) => `${value.toFixed(0)}%`,
|
||||
},
|
||||
},
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
categories: [
|
||||
'2011-01-01',
|
||||
'2011-02-01',
|
||||
'2011-03-01',
|
||||
'2011-04-01',
|
||||
'2011-05-01',
|
||||
'2011-06-01',
|
||||
'2011-07-01',
|
||||
'2011-08-01',
|
||||
'2011-09-01',
|
||||
'2011-10-01',
|
||||
'2011-11-01',
|
||||
'2011-12-01',
|
||||
'2012-01-01',
|
||||
'2012-02-01',
|
||||
'2012-03-01',
|
||||
'2012-04-01',
|
||||
'2012-05-01',
|
||||
'2012-06-01',
|
||||
'2012-07-01',
|
||||
'2012-08-01',
|
||||
'2012-09-01',
|
||||
'2012-10-01',
|
||||
'2012-11-01',
|
||||
'2012-12-01',
|
||||
'2013-01-01',
|
||||
'2013-02-01',
|
||||
'2013-03-01',
|
||||
'2013-04-01',
|
||||
'2013-05-01',
|
||||
'2013-06-01',
|
||||
'2013-07-01',
|
||||
'2013-08-01',
|
||||
'2013-09-01',
|
||||
],
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
columnWidth: '58%',
|
||||
colors: {
|
||||
ranges: [
|
||||
{ from: -100, to: -46,
|
||||
//color: theme.colorWarning
|
||||
},
|
||||
{ from: -45, to: 0,
|
||||
//color: theme.colorInfo
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return <Chart type="bar" series={series} options={chartOptions} height={320} />;
|
||||
}
|
||||
26
src/pages/components/chart/view/chart-column-single.tsx
Normal file
26
src/pages/components/chart/view/chart-column-single.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import Chart from '@/components/chart/chart';
|
||||
import useChart from '@/components/chart/useChart';
|
||||
|
||||
const series = [{ name: 'Net Profit', data: [44, 55, 57, 56, 61, 58, 63, 60, 66] }];
|
||||
export default function ChartColumnSingle() {
|
||||
const chartOptions = useChart({
|
||||
plotOptions: {
|
||||
bar: {
|
||||
columnWidth: '16%',
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
show: false,
|
||||
},
|
||||
xaxis: {
|
||||
categories: ['Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct'],
|
||||
},
|
||||
tooltip: {
|
||||
y: {
|
||||
formatter: (value: number) => `$ ${value} thousands`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return <Chart type="bar" series={series} options={chartOptions} height={320} />;
|
||||
}
|
||||
27
src/pages/components/chart/view/chart-donut.tsx
Normal file
27
src/pages/components/chart/view/chart-donut.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import Chart from '@/components/chart/chart';
|
||||
import useChart from '@/components/chart/useChart';
|
||||
|
||||
const series = [44, 55, 13, 43];
|
||||
export default function ChartDonut() {
|
||||
const chartOptions = useChart({
|
||||
labels: ['Apple', 'Mango', 'Orange', 'Watermelon'],
|
||||
stroke: {
|
||||
show: false,
|
||||
},
|
||||
legend: {
|
||||
horizontalAlign: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
fillSeriesColor: false,
|
||||
},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
donut: {
|
||||
size: '90%',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return <Chart type="donut" series={series} options={chartOptions} height={320} />;
|
||||
}
|
||||
24
src/pages/components/chart/view/chart-line.tsx
Normal file
24
src/pages/components/chart/view/chart-line.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import Chart from '@/components/chart/chart';
|
||||
import useChart from '@/components/chart/useChart';
|
||||
|
||||
const series = [
|
||||
{
|
||||
name: 'Desktops',
|
||||
data: [10, 41, 35, 51, 49, 62, 69, 91, 148],
|
||||
},
|
||||
];
|
||||
export default function ChartLine() {
|
||||
const chartOptions = useChart({
|
||||
xaxis: {
|
||||
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep'],
|
||||
},
|
||||
tooltip: {
|
||||
x: {
|
||||
show: false,
|
||||
},
|
||||
marker: { show: false },
|
||||
},
|
||||
});
|
||||
|
||||
return <Chart type="line" series={series} options={chartOptions} height={320} />;
|
||||
}
|
||||
68
src/pages/components/chart/view/chart-mixed.tsx
Normal file
68
src/pages/components/chart/view/chart-mixed.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import Chart from '@/components/chart/chart';
|
||||
import useChart from '@/components/chart/useChart';
|
||||
|
||||
const series = [
|
||||
{
|
||||
name: 'Team A',
|
||||
type: 'column',
|
||||
data: [23, 11, 22, 27, 13, 22, 37, 21, 44, 22, 30],
|
||||
},
|
||||
{
|
||||
name: 'Team B',
|
||||
type: 'area',
|
||||
data: [44, 55, 41, 67, 22, 43, 21, 41, 56, 27, 43],
|
||||
},
|
||||
{
|
||||
name: 'Team C',
|
||||
type: 'line',
|
||||
data: [30, 25, 36, 30, 45, 35, 64, 52, 59, 36, 39],
|
||||
},
|
||||
];
|
||||
|
||||
export default function ChartMixed() {
|
||||
const chartOptions = useChart({
|
||||
stroke: {
|
||||
width: [0, 2, 3],
|
||||
},
|
||||
plotOptions: {
|
||||
bar: { columnWidth: '20%' },
|
||||
},
|
||||
fill: {
|
||||
type: ['solid', 'gradient', 'solid'],
|
||||
},
|
||||
labels: [
|
||||
'01/01/2003',
|
||||
'02/01/2003',
|
||||
'03/01/2003',
|
||||
'04/01/2003',
|
||||
'05/01/2003',
|
||||
'06/01/2003',
|
||||
'07/01/2003',
|
||||
'08/01/2003',
|
||||
'09/01/2003',
|
||||
'10/01/2003',
|
||||
'11/01/2003',
|
||||
],
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
},
|
||||
yaxis: {
|
||||
title: { text: 'Points' },
|
||||
min: 0,
|
||||
},
|
||||
tooltip: {
|
||||
shared: true,
|
||||
intersect: false,
|
||||
y: {
|
||||
formatter: (value: number) => {
|
||||
if (typeof value !== 'undefined') {
|
||||
return `${value.toFixed(0)} points`;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return <Chart type="line" series={series} options={chartOptions} height={320} />;
|
||||
}
|
||||
35
src/pages/components/chart/view/chart-pie.tsx
Normal file
35
src/pages/components/chart/view/chart-pie.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import Chart from '@/components/chart/chart';
|
||||
import useChart from '@/components/chart/useChart';
|
||||
|
||||
const series = [44, 55, 13, 43];
|
||||
export default function ChartPie() {
|
||||
const chartOptions = useChart({
|
||||
labels: ['America', 'Asia', 'Europe', 'Africa'],
|
||||
legend: {
|
||||
horizontalAlign: 'center',
|
||||
},
|
||||
stroke: {
|
||||
show: false,
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
dropShadow: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
fillSeriesColor: false,
|
||||
},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
donut: {
|
||||
labels: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return <Chart type="pie" series={series} options={chartOptions} height={320} />;
|
||||
}
|
||||
44
src/pages/components/chart/view/chart-radar.tsx
Normal file
44
src/pages/components/chart/view/chart-radar.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import Chart from '@/components/chart/chart';
|
||||
import useChart from '@/components/chart/useChart';
|
||||
//import { useThemeToken } from '@/theme/hooks';
|
||||
|
||||
const series = [
|
||||
{
|
||||
name: 'Series 1',
|
||||
data: [80, 50, 30, 40, 100, 20],
|
||||
},
|
||||
{
|
||||
name: 'Series 2',
|
||||
data: [20, 30, 40, 80, 20, 80],
|
||||
},
|
||||
{
|
||||
name: 'Series 3',
|
||||
data: [44, 76, 78, 13, 43, 10],
|
||||
},
|
||||
];
|
||||
export default function ChartRadar() {
|
||||
//const { colorText } = useThemeToken();
|
||||
const chartOptions = useChart({
|
||||
stroke: {
|
||||
width: 2,
|
||||
},
|
||||
fill: {
|
||||
opacity: 0.48,
|
||||
},
|
||||
legend: {
|
||||
floating: true,
|
||||
position: 'bottom',
|
||||
horizontalAlign: 'center',
|
||||
},
|
||||
xaxis: {
|
||||
categories: ['2011', '2012', '2013', '2014', '2015', '2016'],
|
||||
labels: {
|
||||
style: {
|
||||
colors: '#9aa0ac',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return <Chart type="radar" series={series} options={chartOptions} height={320} />;
|
||||
}
|
||||
37
src/pages/components/chart/view/chart-radial.tsx
Normal file
37
src/pages/components/chart/view/chart-radial.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import Chart from '@/components/chart/chart';
|
||||
import useChart from '@/components/chart/useChart';
|
||||
//import { fNumber } from '@/utils/format-number';
|
||||
|
||||
const series = [44, 55];
|
||||
export default function ChartRadial() {
|
||||
const chartOptions = useChart({
|
||||
chart: {
|
||||
sparkline: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
labels: ['Apples', 'Oranges'],
|
||||
legend: {
|
||||
floating: true,
|
||||
position: 'bottom',
|
||||
horizontalAlign: 'center',
|
||||
},
|
||||
plotOptions: {
|
||||
radialBar: {
|
||||
hollow: {
|
||||
size: '68%',
|
||||
},
|
||||
dataLabels: {
|
||||
value: {
|
||||
offsetY: 16,
|
||||
},
|
||||
total: {
|
||||
//formatter: () => fNumber(2324),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return <Chart type="radialBar" series={series} options={chartOptions} height={320} />;
|
||||
}
|
||||
@@ -78,7 +78,7 @@ const UserInfoCard: React.FC<UserInfoCardProps> = ({ userInfo, loading = false }
|
||||
<Card
|
||||
className={styles.userInfoCard}
|
||||
loading={loading}
|
||||
bordered={false}
|
||||
variant="outlined"
|
||||
>
|
||||
<div className={styles.userHeader}>
|
||||
<Avatar
|
||||
@@ -137,7 +137,7 @@ const TeamInfoCard: React.FC<TeamInfoCardProps> = ({ userInfo }) => {
|
||||
|
||||
if (!team) {
|
||||
return (
|
||||
<Card title="团队信息" className={styles.teamInfoCard} bordered={false}>
|
||||
<Card title="团队信息" className={styles.teamInfoCard} variant="outlined">
|
||||
<Empty description="暂无团队信息" />
|
||||
</Card>
|
||||
);
|
||||
@@ -174,7 +174,7 @@ const TeamInfoCard: React.FC<TeamInfoCardProps> = ({ userInfo }) => {
|
||||
</Space>
|
||||
}
|
||||
className={styles.teamInfoCard}
|
||||
bordered={false}
|
||||
variant="outlined"
|
||||
>
|
||||
<div className={styles.teamContent}>
|
||||
<div className={styles.teamHeader}>
|
||||
@@ -221,7 +221,7 @@ const RolePermissionCard: React.FC<RolePermissionCardProps> = ({ userInfo }) =>
|
||||
|
||||
if (!role) {
|
||||
return (
|
||||
<Card title="角色权限" className={styles.rolePermissionCard} bordered={false}>
|
||||
<Card title="角色权限" className={styles.rolePermissionCard} variant="outlined">
|
||||
<Empty description="暂无角色信息" />
|
||||
</Card>
|
||||
);
|
||||
@@ -236,7 +236,7 @@ const RolePermissionCard: React.FC<RolePermissionCardProps> = ({ userInfo }) =>
|
||||
</Space>
|
||||
}
|
||||
className={styles.rolePermissionCard}
|
||||
bordered={false}
|
||||
variant="outlined"
|
||||
>
|
||||
<div className={styles.roleContent}>
|
||||
<div className={styles.roleHeader}>
|
||||
|
||||
@@ -38,8 +38,13 @@ import {
|
||||
GlobalOutlined,
|
||||
SafetyOutlined,
|
||||
LineChartOutlined,
|
||||
DashboardOutlined,
|
||||
UserOutlined,
|
||||
UsergroupAddOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import Link from 'next/link';
|
||||
import { useTheme } from '@/hooks/useTheme'; // 关键代码行注释:使用独立的useTheme Hook
|
||||
import { useUserInfo, useUserHomePath } from '@/store/userStore'; // 关键代码行注释:用户状态管理
|
||||
import { MdDarkMode, MdLightMode } from "react-icons/md";
|
||||
import styles from './index.module.css'; // 关键代码行注释:导入CSS模块样式
|
||||
|
||||
@@ -151,6 +156,10 @@ export default function HomePage(): React.ReactElement {
|
||||
const { isDark, toggleTheme, mounted, isTransitioning } = useTheme(); // 关键代码行注释:解构获取所需的状态
|
||||
const { message, notification, modal } = App.useApp();
|
||||
|
||||
// 关键代码行注释:用户登录状态管理
|
||||
const userInfo = useUserInfo(); // 关键代码行注释:获取用户信息
|
||||
const homePath = useUserHomePath(); // 关键代码行注释:获取用户首页路径
|
||||
|
||||
// 关键代码行注释:如果还未挂载,显示加载状态避免闪烁
|
||||
if (!mounted) {
|
||||
return <div style={{ height: '100vh', background: '#f6f9fc' }} />;
|
||||
@@ -221,13 +230,30 @@ export default function HomePage(): React.ReactElement {
|
||||
{isDark ? <MdLightMode /> : <MdDarkMode />}
|
||||
</Button>
|
||||
|
||||
<Button type="text" className={styles.loginBtn}>
|
||||
{/* 关键代码行注释:根据用户登录状态显示不同的按钮 */}
|
||||
{userInfo && userInfo._id ? (
|
||||
// 已登录状态:显示控制台按钮
|
||||
<Link href={homePath} legacyBehavior>
|
||||
<Button type="primary" icon={<DashboardOutlined />} className={styles.signupBtn}>
|
||||
控制台
|
||||
</Button>
|
||||
</Link>
|
||||
) : (
|
||||
// 未登录状态:显示登录和注册按钮
|
||||
<>
|
||||
<Link href="/start/login" legacyBehavior>
|
||||
<Button type="text" icon={<UserOutlined />} className={styles.loginBtn}>
|
||||
登录
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Button type="primary" className={styles.signupBtn}>
|
||||
免费试用
|
||||
<Link href="/start/register" legacyBehavior>
|
||||
<Button type="primary" icon={<UsergroupAddOutlined />} className={styles.signupBtn}>
|
||||
免费注册
|
||||
</Button>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Header>
|
||||
|
||||
206
src/pages/team/AfterSaleRecord/EditAfterSalesModal.tsx
Normal file
206
src/pages/team/AfterSaleRecord/EditAfterSalesModal.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* 文件: EditAfterSalesModal.tsx
|
||||
* 作者: 阿瑞
|
||||
* 功能: 编辑售后记录模态框组件
|
||||
* 版本: v2.0.0 - 使用 fetch 替换 axios
|
||||
*/
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Modal, Form, Input, Button, Select, InputNumber, App } from 'antd';
|
||||
import { IAfterSalesRecord, IPaymentPlatform, IProduct } from '@/models/types';
|
||||
import { useUserInfo } from '@/store/userStore';
|
||||
|
||||
interface EditAfterSalesModalProps {
|
||||
visible: boolean;
|
||||
onOk: () => void;
|
||||
onCancel: () => void;
|
||||
record: IAfterSalesRecord | null; // 传入的售后记录
|
||||
}
|
||||
|
||||
const EditAfterSalesModal: React.FC<EditAfterSalesModalProps> = ({ visible, onOk, onCancel, record }) => {
|
||||
const { message } = App.useApp(); // 使用 App.useApp 获取 message 实例
|
||||
const [form] = Form.useForm();
|
||||
const userInfo = useUserInfo();
|
||||
const [paymentPlatforms, setPaymentPlatforms] = useState<IPaymentPlatform[]>([]);
|
||||
const [products, setProducts] = useState<IProduct[]>([]);
|
||||
|
||||
// 获取产品数据
|
||||
const fetchProducts = async (teamId: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/backstage/products?teamId=${teamId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
setProducts(data.products || []);
|
||||
} catch (error: unknown) {
|
||||
console.error('加载产品数据失败:', error);
|
||||
message.error('加载产品数据失败');
|
||||
}
|
||||
};
|
||||
|
||||
//获取收支平台
|
||||
const fetchPayPlatforms = async (teamId: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/backstage/payment-platforms?teamId=${teamId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
setPaymentPlatforms(data.platforms || []);
|
||||
} catch (error: unknown) {
|
||||
console.error('加载平台数据失败:', error);
|
||||
message.error('加载平台数据失败');
|
||||
setPaymentPlatforms([]);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
form.resetFields();
|
||||
if (record && userInfo.团队?._id) {
|
||||
const teamId = userInfo.团队._id;
|
||||
fetchPayPlatforms(teamId);
|
||||
fetchProducts(teamId); // 加载产品列表
|
||||
|
||||
// 设置表单初始值
|
||||
const initialValues: any = {
|
||||
原因: record.原因,
|
||||
备注: record.备注,
|
||||
类型: record.类型,
|
||||
收支平台: record.收支平台?._id,
|
||||
收支金额: record.收支金额,
|
||||
待收: record.待收,
|
||||
收支类型: record.收支类型,
|
||||
};
|
||||
|
||||
// 处理替换产品
|
||||
if (record.替换产品 && Array.isArray(record.替换产品)) {
|
||||
// 如果替换产品是对象数组,提取ID
|
||||
if (record.替换产品.length > 0 && typeof record.替换产品[0] === 'object') {
|
||||
initialValues.替换产品 = record.替换产品.map(
|
||||
(product: any) => product._id || product
|
||||
);
|
||||
} else {
|
||||
// 如果已经是ID数组,直接使用
|
||||
initialValues.替换产品 = record.替换产品;
|
||||
}
|
||||
|
||||
console.log('设置替换产品值:', initialValues.替换产品);
|
||||
}
|
||||
|
||||
form.setFieldsValue(initialValues);
|
||||
}
|
||||
}, [record, userInfo.团队?._id, form]);
|
||||
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
const replacementProductIds = form.getFieldValue('替换产品') || [];
|
||||
const values = await form.validateFields();
|
||||
|
||||
// 构建更新对象,确保正确处理所有需要更新的字段
|
||||
const updateData = {
|
||||
原因: values.原因,
|
||||
备注: values.备注,
|
||||
类型: values.类型,
|
||||
收支平台: values.收支平台 ? { _id: values.收支平台 } : null,
|
||||
收支金额: values.收支金额,
|
||||
待收: values.待收,
|
||||
收支类型: values.收支类型,
|
||||
替换产品: replacementProductIds, // 使用表单中的替换产品ID数组
|
||||
};
|
||||
|
||||
console.log('更新售后记录数据:', updateData);
|
||||
|
||||
// 调用API更新售后记录
|
||||
const response = await fetch(`/api/backstage/sales/aftersale/records/${record?._id}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(updateData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
message.success('售后记录更新成功');
|
||||
onOk();
|
||||
} catch (error: unknown) {
|
||||
console.error('更新售后记录失败:', error);
|
||||
message.error('更新售后记录失败');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="编辑售后记录"
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={onCancel}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button key="submit" type="primary" onClick={handleOk}>
|
||||
保存
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
name="类型"
|
||||
label="售后类型"
|
||||
rules={[{ required: true, message: '请选择售后类型' }]}
|
||||
>
|
||||
<Select>
|
||||
<Select.Option value="退货">退货</Select.Option>
|
||||
<Select.Option value="换货">换货</Select.Option>
|
||||
<Select.Option value="补发">补发</Select.Option>
|
||||
<Select.Option value="补差">补差</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item name="替换产品" label="选择替换产品">
|
||||
<Select mode="multiple" placeholder="请选择替换产品">
|
||||
{products.map(product => (
|
||||
<Select.Option key={product._id} value={product._id}>
|
||||
【名称】{product.名称}【价格】{product.售价}【库存】{product.库存}【描述】{product.描述}【货号】{product.货号}【别名】{product.别名}【级别】{product.级别}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="收支类型" label="收支类型">
|
||||
<Select placeholder="请选择收支类型">
|
||||
<Select.Option value="收入">收入</Select.Option>
|
||||
<Select.Option value="支出">支出</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="收支金额" label="收支金额">
|
||||
<InputNumber style={{ width: '100%' }} min={0} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="待收" label="待收金额">
|
||||
<InputNumber style={{ width: '100%' }} min={0} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="收支平台" label="收支平台">
|
||||
<Select placeholder="请选择收支平台">
|
||||
{paymentPlatforms.map(platform => (
|
||||
<Select.Option key={platform._id} value={platform._id}>
|
||||
{platform.名称}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="备注"
|
||||
label="备注"
|
||||
>
|
||||
<Input.TextArea placeholder="请输入备注" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditAfterSalesModal;
|
||||
339
src/pages/team/AfterSaleRecord/MultiAfterSalesModal.tsx
Normal file
339
src/pages/team/AfterSaleRecord/MultiAfterSalesModal.tsx
Normal file
@@ -0,0 +1,339 @@
|
||||
/**
|
||||
* 文件: MultiAfterSalesModal.tsx
|
||||
* 作者: 阿瑞
|
||||
* 功能: 多重售后操作模态框组件
|
||||
* 版本: v2.0.0 - 使用 fetch 替换 axios
|
||||
*/
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Modal, Form, Select, DatePicker, Button, InputNumber, Input, App } from 'antd';
|
||||
import { IAfterSalesRecord, IPaymentPlatform, IProduct } from '@/models/types';
|
||||
import dayjs from 'dayjs';
|
||||
import { useUserInfo } from '@/store/userStore';
|
||||
import ProductImage from '@/components/product/ProductImage';
|
||||
import AddProductComponent from '../sale/components/AddProductComponent';
|
||||
import { CloseOutlined } from '@ant-design/icons';
|
||||
|
||||
interface MultiAfterSalesModalProps {
|
||||
visible: boolean;
|
||||
onOk: () => void;
|
||||
onCancel: () => void;
|
||||
record: IAfterSalesRecord | null; // 传递当前的售后记录
|
||||
type: '退货' | '换货' | '补发' | '补差';
|
||||
}
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const MultiAfterSalesModal: React.FC<MultiAfterSalesModalProps> = ({ visible, onOk, onCancel, record, type }) => {
|
||||
const { message } = App.useApp(); // 使用 App.useApp 获取 message 实例
|
||||
const [form] = Form.useForm();
|
||||
const [paymentPlatforms, setPaymentPlatforms] = useState<IPaymentPlatform[]>([]);
|
||||
const [selectedProducts, setSelectedProducts] = useState<IProduct[]>([]);
|
||||
const [products, setProducts] = useState<IProduct[]>([]);
|
||||
const userInfo = useUserInfo();
|
||||
const [paymentCode, setPaymentCode] = useState<string | null>(null);
|
||||
|
||||
//获取收支平台
|
||||
const fetchPayPlatforms = async (teamId: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/backstage/payment-platforms?teamId=${teamId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
setPaymentPlatforms(data.platforms || []);
|
||||
} catch (error: unknown) {
|
||||
console.error('加载平台数据失败:', error);
|
||||
message.error('加载平台数据失败');
|
||||
setPaymentPlatforms([]);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取产品数据
|
||||
const fetchProducts = async (teamId: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/backstage/products?teamId=${teamId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
setProducts(data.products || []);
|
||||
} catch (error: unknown) {
|
||||
console.error('加载产品数据失败:', error);
|
||||
message.error('加载产品数据失败');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
form.resetFields();
|
||||
setSelectedProducts([]);
|
||||
if (record && userInfo.团队?._id) {
|
||||
const teamId = userInfo.团队._id;
|
||||
fetchPayPlatforms(teamId);
|
||||
fetchProducts(teamId); // 加载产品列表
|
||||
|
||||
form.setFieldsValue({
|
||||
前一次售后: record._id, // 使用当前售后记录的ID作为前一次售后
|
||||
类型: type,
|
||||
日期: dayjs(),
|
||||
});
|
||||
}
|
||||
}, [record, type, userInfo.团队?._id]);
|
||||
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
if (!record) {
|
||||
message.error('未选择售后记录,无法创建新的售后记录');
|
||||
return;
|
||||
}
|
||||
const replacementProductIds = selectedProducts.map(product => product._id);
|
||||
let paymentCodeId = null;
|
||||
if (paymentCode) {
|
||||
const response = await fetch('/api/backstage/sales/aftersale/uploadPaymentCode', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ 收款码: paymentCode }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('收款码上传失败');
|
||||
}
|
||||
|
||||
const paymentCodeResponse = await response.json();
|
||||
paymentCodeId = paymentCodeResponse.paymentCodeId;
|
||||
}
|
||||
const afterSalesData = {
|
||||
销售记录: record.销售记录._id,
|
||||
前一次售后: record._id, // 传递前一次售后的 ID
|
||||
类型: type,
|
||||
原产品: values.原产品, // 传递原产品的 ID 数组
|
||||
替换产品: replacementProductIds, // 传递替换产品的 ID 数组
|
||||
团队: userInfo.团队?._id,
|
||||
日期: values.日期.toISOString(),
|
||||
收款码: paymentCodeId, // 添加收款码ID字段
|
||||
原因: values.原因, // 售后原因
|
||||
收支类型: values.收支类型,
|
||||
收支平台: values.收支平台,
|
||||
收支金额: values.收支金额,
|
||||
待收: values.待收,
|
||||
备注: values.备注,
|
||||
};
|
||||
|
||||
const response = await fetch('/api/backstage/sales/aftersale', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(afterSalesData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
message.success(`${type}操作成功`);
|
||||
onOk();
|
||||
} catch (error: unknown) {
|
||||
console.error(`${type}操作失败:`, error);
|
||||
message.error(`${type}操作失败`);
|
||||
}
|
||||
};
|
||||
|
||||
const [isProductModalVisible, setIsProductModalVisible] = useState(false);
|
||||
// 修改:新增产品成功后的回调函数,更新选中的产品状态
|
||||
const handleAddProductSuccess = (newProduct: IProduct) => {
|
||||
setSelectedProducts(prevProducts => [...prevProducts, newProduct]); // 更新选中的产品
|
||||
setIsProductModalVisible(false); // 关闭产品模态框
|
||||
};
|
||||
const handleProductSelectChange = (selectedProductIds: string[]) => {
|
||||
const selected = products.filter(product => selectedProductIds.includes(product._id));
|
||||
setSelectedProducts(selected);
|
||||
};
|
||||
const renderAdditionalFields = () => {
|
||||
if (type === '换货' || type === '补发') {
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginTop: 16 }}>
|
||||
{selectedProducts.map(product => (
|
||||
<div key={product._id} style={{ marginBottom: 8 }}>
|
||||
<ProductImage productId={product._id} alt={product.名称} width={72} height={72} />
|
||||
<span style={{ marginLeft: 8 }}>{product.名称}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => setIsProductModalVisible(true)}
|
||||
>
|
||||
添加 {type} 产品
|
||||
</Button>
|
||||
<AddProductComponent
|
||||
visible={isProductModalVisible}
|
||||
onClose={() => setIsProductModalVisible(false)}
|
||||
onSuccess={handleAddProductSuccess}
|
||||
/>
|
||||
<Form.Item name="替换产品" label="选择产品">
|
||||
<Select mode="multiple" placeholder="请选择产品" onChange={handleProductSelectChange}>
|
||||
{products.map(product => (
|
||||
<Select.Option key={product._id} value={product._id}>
|
||||
【名称】{product.名称}【价格】{product.售价}【库存】{product.库存}【描述】{product.描述}【货号】{product.货号}【别名】{product.别名}【级别】{product.级别}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const handlePaste = (event: React.ClipboardEvent<HTMLDivElement>) => {
|
||||
const items = event.clipboardData.items;
|
||||
for (const item of items) {
|
||||
if (item.type.startsWith('image/')) {
|
||||
const file = item.getAsFile();
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (e) => {
|
||||
const img = new Image();
|
||||
img.src = e.target?.result as string;
|
||||
img.onload = async () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const width = img.width / 2;
|
||||
const height = img.height / 2;
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
ctx?.drawImage(img, 0, 0, width, height);
|
||||
const compressedDataUrl = canvas.toDataURL('image/jpeg', 0.8);
|
||||
setPaymentCode(compressedDataUrl);
|
||||
};
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={visible}
|
||||
title={`创建${type}记录`}
|
||||
onCancel={onCancel}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={onCancel}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button key="submit" type="primary" onClick={handleOk}>
|
||||
保存
|
||||
</Button>,
|
||||
]}
|
||||
width="76vw"
|
||||
style={{ top: 20 }}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="col-span-1">
|
||||
<h3 className="font-bold">售后信息</h3>
|
||||
<Form.Item name="原产品" label="上一次售后产品" rules={[{ required: true, message: '请选择需要售后的产品' }]}>
|
||||
<Select mode="multiple" placeholder="请选择原订单中的产品">
|
||||
{record?.替换产品.map(product => (
|
||||
<Select.Option key={product._id} value={product._id}>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<ProductImage productId={product._id} alt={product.名称} width={40} height={40} />
|
||||
<span style={{ marginLeft: 8 }}>{product.名称}</span>
|
||||
</div>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="售后原因" name="原因" rules={[{ required: true, message: '请选择原因' }]}>
|
||||
<Select>
|
||||
<Option value="发货原因">发货原因</Option>
|
||||
<Option value="产品质量">产品质量</Option>
|
||||
<Option value="择优选购">择优选购</Option>
|
||||
<Option value="七日退换">七日退换</Option>
|
||||
<Option value="货不对板">货不对板</Option>
|
||||
<Option value="运输损坏">运输损坏</Option>
|
||||
<Option value="配件缺失">配件缺失</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<h3 className="font-bold">售后信息</h3>
|
||||
{renderAdditionalFields()}
|
||||
<br />
|
||||
<Form.Item name="日期" label="售后日期" rules={[{ required: true, message: '请选择售后日期' }]}>
|
||||
<DatePicker style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="备注" label="备注">
|
||||
<Input.TextArea
|
||||
//默认显示4行,最多6行
|
||||
autoSize={{ minRows: 4, maxRows: 6 }}
|
||||
placeholder="备注信息" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<h3 className="font-bold">收支信息</h3>
|
||||
<Form.Item name="收支平台" label="收支平台">
|
||||
<Select placeholder="请选择收支平台">
|
||||
{paymentPlatforms.map(platform => (
|
||||
<Select.Option key={platform._id} value={platform._id}>
|
||||
{platform.名称}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="收支类型"
|
||||
name="收支类型"
|
||||
>
|
||||
<Select placeholder="请选择收支类型" allowClear>
|
||||
<Option value="收入">收入</Option>
|
||||
<Option value="支出">支出</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item name="收支金额" label="收支金额" >
|
||||
<InputNumber min={0} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="待收"
|
||||
name="待收"
|
||||
>
|
||||
<InputNumber min={0} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="收款码"
|
||||
name="收款码"
|
||||
>
|
||||
<div
|
||||
onPaste={handlePaste}
|
||||
className="cursor-pointer flex flex-col items-center justify-center p-4 min-h-[260px] min-w-[260px] border-dashed border rounded-lg"
|
||||
>
|
||||
{paymentCode ? (
|
||||
<>
|
||||
<img src={paymentCode} alt="Payment Code" className="max-w-full max-h-[260px]" />
|
||||
<button
|
||||
onClick={() => setPaymentCode(null)}
|
||||
className="absolute top-2 right-2 p-1 bg-gray-100 hover:bg-gray-200 rounded-full"
|
||||
style={{ width: '22px', height: '22px', display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: '50%', outline: 'none', color: 'red' }}
|
||||
>
|
||||
<CloseOutlined />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<p>粘贴图片到此区域</p>
|
||||
)}
|
||||
</div>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default MultiAfterSalesModal;
|
||||
799
src/pages/team/AfterSaleRecord/index.tsx
Normal file
799
src/pages/team/AfterSaleRecord/index.tsx
Normal file
@@ -0,0 +1,799 @@
|
||||
/**
|
||||
* 文件: AfterSaleRecord/index.tsx
|
||||
* 作者: 阿瑞
|
||||
* 功能: 售后记录管理页面 - 展示和管理售后记录
|
||||
* 版本: v2.0.0 - 使用 fetch 替换 axios
|
||||
*/
|
||||
import React, { useEffect, useState, useMemo, useCallback } from 'react';
|
||||
import { Table, Button, Card, Tag, Tooltip
|
||||
, Input, Space, DatePicker, App } from 'antd';
|
||||
import { IAfterSalesRecord } from '@/models/types';
|
||||
import dayjs from 'dayjs';
|
||||
import { useUserInfo } from '@/store/userStore';
|
||||
import { ColumnsType } from 'antd/es/table';
|
||||
import { useRouter } from 'next/router';
|
||||
import { CheckCircleOutlined, ClockCircleOutlined, DownloadOutlined, FieldTimeOutlined, IdcardOutlined, MobileOutlined, SyncOutlined, UserOutlined, WechatOutlined
|
||||
, SearchOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { IconButton, Iconify } from '@/components/icon';
|
||||
import PaymentCodeImageComponent from '@/pages/components/PaymentCodeImage';
|
||||
import MultiAfterSalesModal from './MultiAfterSalesModal';
|
||||
import ShipModal from './ship-modal';
|
||||
import EditAfterSalesModal from './EditAfterSalesModal'; // 引入编辑售后模态框
|
||||
import ProductCardList from '@/components/product/ProductCardList'; // 引入产品卡片列表组件
|
||||
import MyTooltip from '@/components/tooltip/MyTooltip';
|
||||
|
||||
// 导出Excel相关库
|
||||
//import * as XLSX from 'xlsx';
|
||||
//import { saveAs } from 'file-saver';
|
||||
|
||||
const AfterSaleRecordPage = () => {
|
||||
const { message } = App.useApp(); // 使用 App.useApp 获取 message 实例
|
||||
const [records, setRecords] = useState<IAfterSalesRecord[]>([]);
|
||||
const [filteredRecords, setFilteredRecords] = useState<IAfterSalesRecord[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [transactionDateRange, setTransactionDateRange] = useState<[Date | null, Date | null]>([null, null]);
|
||||
const [afterSaleDateRange, setAfterSaleDateRange] = useState<[Date | null, Date | null]>([null, null]);
|
||||
const [transactionDateValue, setTransactionDateValue] = useState<any>(null);
|
||||
const [afterSaleDateValue, setAfterSaleDateValue] = useState<any>(null);
|
||||
const userInfo = useUserInfo();
|
||||
const router = useRouter();
|
||||
|
||||
const userRole = userInfo?.角色?.名称; // 用户角色名称
|
||||
const isAdmin = userRole === '系统管理员' || userRole === '团队管理员'; // 是否为管理员角色
|
||||
//const isAdmin = userInfo.角色?.名称 === '系统管理员';
|
||||
/*
|
||||
// 获取用户角色
|
||||
const userRole = userInfo?.角色?.名称; // 用户角色名称
|
||||
const isAdmin = userRole === '系统管理员' || userRole === '团队管理员'; // 是否为管理员角色
|
||||
const isNotFinanceRole = userRole !== '财务'; // 判断是否不是财务角色
|
||||
*/
|
||||
const [currentafterSalesRecord, setCurrentafterSalesRecord] = useState<IAfterSalesRecord | null>(null); // 当前操作的售后记录
|
||||
const [multiafterSalesVisible, setMultiafterSalesVisible] = useState(false);
|
||||
const [multiafterSalesType, setMultiAfterSalesType] = useState<'退货' | '换货' | '补发' | '补差' | null>(null);
|
||||
const [isShipModalVisible, setIsShipModalVisible] = useState(false); // 是否显示发货模态框
|
||||
const [currentRecord, setCurrentRecord] = useState<IAfterSalesRecord | null>(null); // 当前选中的销售记录
|
||||
const [editModalVisible, setEditModalVisible] = useState(false); // 是否显示编辑模态框
|
||||
const [currentEditRecord, setCurrentEditRecord] = useState<IAfterSalesRecord | null>(null); // 当前选中的售后记录
|
||||
|
||||
// 使用useCallback优化函数,避免不必要的重新创建
|
||||
const fetchRecords = useCallback(async (teamId: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/backstage/sales/aftersale/records?teamId=${teamId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
setRecords(data.records);
|
||||
setFilteredRecords(data.records); // 初始化filteredRecords为所有记录
|
||||
} catch (error: unknown) {
|
||||
console.error('加载售后记录失败:', error);
|
||||
message.error('加载售后记录失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [message]);
|
||||
|
||||
const showShipModal = useCallback((record: IAfterSalesRecord) => {
|
||||
setCurrentRecord(record);
|
||||
setIsShipModalVisible(true); // 显示发货模态框
|
||||
}, []);
|
||||
|
||||
const handleShipModalOk = useCallback(async () => {
|
||||
setIsShipModalVisible(false); // 关闭发货模态框
|
||||
// 发货操作完成后重新获取数据
|
||||
if (userInfo.团队?._id) {
|
||||
await fetchRecords(userInfo.团队._id);
|
||||
}
|
||||
}, [userInfo.团队?._id, fetchRecords]);
|
||||
|
||||
const handleModalCancel = useCallback(() => {
|
||||
//setIsModalVisible(false);
|
||||
setIsShipModalVisible(false); // 关闭发货模态框
|
||||
}, []);
|
||||
|
||||
const showEditModal = useCallback((record: IAfterSalesRecord) => {
|
||||
setCurrentEditRecord(record); // 设置当前要编辑的售后记录
|
||||
setEditModalVisible(true); // 显示编辑模态框
|
||||
}, []);
|
||||
|
||||
const handleEditModalOk = useCallback(async () => {
|
||||
setEditModalVisible(false); // 关闭编辑模态框
|
||||
// 编辑操作完成后重新加载售后记录
|
||||
if (userInfo.团队?._id) {
|
||||
await fetchRecords(userInfo.团队._id);
|
||||
}
|
||||
}, [userInfo.团队?._id, fetchRecords]);
|
||||
|
||||
const handleEditModalCancel = useCallback(() => {
|
||||
setEditModalVisible(false); // 关闭编辑模态框
|
||||
}, []);
|
||||
|
||||
const handleMultiAfterSales = useCallback((record: IAfterSalesRecord, type: '退货' | '换货' | '补发' | '补差') => {
|
||||
setCurrentafterSalesRecord(record);
|
||||
setMultiAfterSalesType(type);
|
||||
setMultiafterSalesVisible(true);
|
||||
}, []);
|
||||
|
||||
const handleMultiAfterSalesModalOk = useCallback(async () => {
|
||||
setMultiafterSalesVisible(false); // 关闭模态框
|
||||
// 多重售后操作完成后重新获取数据
|
||||
if (userInfo.团队?._id) {
|
||||
await fetchRecords(userInfo.团队._id);
|
||||
}
|
||||
}, [userInfo.团队?._id, fetchRecords]);
|
||||
|
||||
// 售后进度更新函数 - 提取到组件顶层避免重复创建
|
||||
const handleProgressChange = useCallback(async (id: string, newStatus: "待处理" | "处理中" | "已处理") => {
|
||||
try {
|
||||
if (!id) {
|
||||
throw new Error("无效的售后记录 ID");
|
||||
}
|
||||
|
||||
const response = await fetch('/api/backstage/sales/aftersale/records/updateProgress', {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ id, progress: newStatus }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
// 更新本地状态
|
||||
setRecords(prevRecords =>
|
||||
prevRecords.map((record: IAfterSalesRecord) =>
|
||||
record._id.toString() === id ? { ...record, 售后进度: newStatus } : record
|
||||
)
|
||||
);
|
||||
|
||||
message.success('售后进度更新成功');
|
||||
|
||||
// 进度更新后重新获取数据确保同步
|
||||
if (userInfo.团队?._id) {
|
||||
await fetchRecords(userInfo.团队._id);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error('售后进度更新失败:', error);
|
||||
message.error('售后进度更新失败');
|
||||
}
|
||||
}, [userInfo?.团队?._id, fetchRecords, message]);
|
||||
|
||||
useEffect(() => {
|
||||
if (userInfo.团队?._id) {
|
||||
fetchRecords(userInfo.团队._id);
|
||||
}
|
||||
}, [userInfo.团队?._id, fetchRecords]);
|
||||
|
||||
// 筛选记录的函数
|
||||
useEffect(() => {
|
||||
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 (afterSaleDateRange[0] && afterSaleDateRange[1]) {
|
||||
filtered = filtered.filter(record => {
|
||||
if (!record.日期) return false;
|
||||
const afterSaleDate = new Date(record.日期);
|
||||
// 确保日期比较是准确的
|
||||
return afterSaleDate >= afterSaleDateRange[0]! &&
|
||||
afterSaleDate <= afterSaleDateRange[1]!;
|
||||
});
|
||||
}
|
||||
|
||||
setFilteredRecords(filtered);
|
||||
}, [records, transactionDateRange, afterSaleDateRange]);
|
||||
|
||||
const columns: ColumnsType<IAfterSalesRecord> = useMemo(() => [
|
||||
{
|
||||
title: '销售记录',
|
||||
onHeaderCell: () => {
|
||||
return {
|
||||
style: {
|
||||
backgroundColor: '#ffbb96',
|
||||
}
|
||||
};
|
||||
},
|
||||
children: [
|
||||
//原订单信息
|
||||
{
|
||||
title: '客户信息',
|
||||
width: 160,
|
||||
key: '客户信息',
|
||||
// 增加filterDropdown用于尾号查询
|
||||
filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters }) => (
|
||||
<div style={{ padding: 8 }}>
|
||||
<Input
|
||||
placeholder="输入手机尾号"
|
||||
value={selectedKeys[0] ? String(selectedKeys[0]) : ''}
|
||||
onChange={e => setSelectedKeys(e.target.value ? [e.target.value] : [])}
|
||||
onPressEnter={() => confirm()}
|
||||
style={{ marginBottom: 8, display: 'block' }}
|
||||
/>
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => confirm()}
|
||||
size="small"
|
||||
>
|
||||
搜索
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
clearFilters?.(); // 使用可选链,避免类型错误
|
||||
confirm();
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
),
|
||||
filterIcon: filtered => (
|
||||
<SearchOutlined style={{ color: filtered ? '#1890ff' : undefined }} />
|
||||
),
|
||||
onFilter: (value, record: IAfterSalesRecord) => {
|
||||
const phoneTail = record.销售记录.客户?.电话
|
||||
? record.销售记录.客户.电话.slice(-4)
|
||||
: '';
|
||||
const searchVal = String(value);
|
||||
return phoneTail.includes(searchVal);
|
||||
},
|
||||
render: (record: IAfterSalesRecord) => {
|
||||
// 将地址的各个部分拼接成一个字符串
|
||||
const address = record.销售记录.客户?.地址
|
||||
? `${record.销售记录.客户.地址.省份 ?? ''} ${record.销售记录.客户.地址.城市 ?? ''} ${record.销售记录.客户.地址.区县 ?? ''} ${record.销售记录.客户.地址.详细地址 ?? ''}`
|
||||
: '未知';
|
||||
const customerInfo = `姓名:${record.销售记录.客户?.姓名 ?? '未知'}\n电话:${record.销售记录.客户?.电话 ?? '未知'}\n地址:${address}`;
|
||||
const customerName = record.销售记录.客户?.姓名 ?? '未知';
|
||||
//显示账号编号
|
||||
const accountNumber = record.销售记录.订单来源?.账号编号 ?? '未知';
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
{isAdmin && (
|
||||
<IconButton
|
||||
className="text-gray"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(customerInfo);
|
||||
message.success(`客户 ${customerName} 信息复制成功!`);
|
||||
} catch (error) {
|
||||
console.error('客户信息复制失败:', error);
|
||||
message.error('客户信息复制失败');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Iconify icon="eva:copy-fill" size={20} />
|
||||
</IconButton>
|
||||
)}
|
||||
<MyTooltip
|
||||
color="white"
|
||||
title={record.销售记录.客户?.姓名 ?? '未知'}>
|
||||
<div>
|
||||
<Tag icon={<UserOutlined />} color="">{record.销售记录.客户?.姓名 ?? '未知'}</Tag>
|
||||
<Tag icon={<FieldTimeOutlined />} color="">{record.销售记录.成交日期 ? new Date(record.销售记录.成交日期).toLocaleDateString() : '未知'}</Tag>
|
||||
<Tag icon={<MobileOutlined />} color="">{record.销售记录.客户?.电话 ? `****${record.销售记录.客户.电话.slice(-4)}` : '未知'}</Tag>
|
||||
{/*<div>状态:{record.销售记录?.订单状态?.[0]}</div>*/}
|
||||
{/* 将最后两个 Tag 放在一个 span 中,并使用 no-wrap 样式避免自动换行 */}
|
||||
<span style={{ whiteSpace: 'nowrap' }}>
|
||||
<Tag icon={<IdcardOutlined />} color="">
|
||||
{record.销售记录?.导购?.姓名 ?? '未知'}
|
||||
</Tag>
|
||||
<Tag icon={<WechatOutlined />} color="green">
|
||||
{accountNumber}
|
||||
</Tag>
|
||||
</span>
|
||||
</div>
|
||||
</MyTooltip>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '售后产品',
|
||||
dataIndex: '原产品',
|
||||
//width: 260,
|
||||
key: 'originalProducts',
|
||||
render: (products: any[], record: IAfterSalesRecord) => {
|
||||
return <ProductCardList products={products} record={record} />;
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
title: '财务信息',
|
||||
key: '财务信息',
|
||||
width: 110,
|
||||
render: (record: IAfterSalesRecord) => {
|
||||
return (
|
||||
<div>
|
||||
<div>平台:{record.销售记录?.收款平台?.名称}</div>
|
||||
<div>应收:{record.销售记录?.应收金额}</div>
|
||||
<div>已收:{record.销售记录?.收款金额}</div>
|
||||
<div>待收:{record.销售记录?.待收款}</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '售后信息',
|
||||
key: 'reasonAndType',
|
||||
//筛选售后类型
|
||||
filters: [
|
||||
{ text: '退货', value: '退货' },
|
||||
{ text: '换货', value: '换货' },
|
||||
{ text: '补发', value: '补发' },
|
||||
{ text: '补差', value: '补差' },
|
||||
],
|
||||
onFilter: (value, record) => record.类型 === value,
|
||||
render: (record: IAfterSalesRecord) => (
|
||||
<div>
|
||||
<div>售后日期:{dayjs(record.日期).format('YYYY-MM-DD')}</div>
|
||||
<div>原因:{record.原因}</div>
|
||||
<Tag color={record.类型 === '退货' ? 'red' : record.类型 === '换货' ? 'blue' : 'green'}>{record.类型}</Tag>
|
||||
<div>备注:{record.备注}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '售后信息',
|
||||
onHeaderCell: () => {
|
||||
return {
|
||||
style: {
|
||||
backgroundColor: '#ffbb96',
|
||||
}
|
||||
};
|
||||
},
|
||||
children: [
|
||||
{
|
||||
title: '替换产品',
|
||||
dataIndex: '替换产品',
|
||||
key: 'replacementProducts',
|
||||
render: (products: any[], record: IAfterSalesRecord) => {
|
||||
return <ProductCardList products={products} record={record} />;
|
||||
}
|
||||
},
|
||||
//财务信息
|
||||
{
|
||||
title: '财务信息',
|
||||
key: '财务信息',
|
||||
width: 110,
|
||||
render: (record: IAfterSalesRecord) => {
|
||||
return (
|
||||
<div>
|
||||
<div>平台:{record.收支平台?.名称}</div>
|
||||
<div>类型:{record.收支类型}</div>
|
||||
<div>金额:{record.收支金额}</div>
|
||||
<div>待收:{record.待收}</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '售后进度',
|
||||
key: '售后进度',
|
||||
width: 100,
|
||||
filters: [
|
||||
{ text: '待处理', value: '待处理' },
|
||||
{ text: '处理中', value: '处理中' },
|
||||
{ text: '已处理', value: '已处理' },
|
||||
],
|
||||
onFilter: (value, record) => record.售后进度 === value,
|
||||
render: (_: any, record: IAfterSalesRecord) => {
|
||||
const 售后进度 = record.售后进度;
|
||||
const progressOptions = [
|
||||
{ label: '待处理', value: '待处理', color: 'yellow', icon: <ClockCircleOutlined /> },
|
||||
{ label: '处理中', value: '处理中', color: 'blue', icon: 售后进度 === '处理中' ? <SyncOutlined spin /> : <SyncOutlined /> },
|
||||
{ label: '已处理', value: '已处理', color: 'green', icon: <CheckCircleOutlined /> },
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
{progressOptions.map((option) => (
|
||||
<Tag
|
||||
icon={option.icon}
|
||||
key={option.value}
|
||||
color={售后进度 === option.value ? option.color : undefined}
|
||||
style={售后进度 === option.value ? {} : {
|
||||
borderColor: 'gray', color: 'gray', borderStyle: 'solid', borderWidth: 1,
|
||||
backgroundColor: 'transparent',
|
||||
//cursor: isAdmin ? 'pointer' : 'default'
|
||||
}}
|
||||
onClick={() => handleProgressChange(record._id, option.value as "待处理" | "处理中" | "已处理")}
|
||||
>
|
||||
{option.label}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
//收款码,使用PaymentCodeImageComponent paymentCodeId= 组件显示图片
|
||||
{
|
||||
title: '收款码',
|
||||
key: '收款码',
|
||||
render: (record: IAfterSalesRecord) => {
|
||||
return record.收款码 ? (
|
||||
<div>
|
||||
<PaymentCodeImageComponent paymentCodeId={record.收款码._id} height={72} />
|
||||
</div>
|
||||
) : (
|
||||
<div>无收款码</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
align: 'center',
|
||||
width: 160,
|
||||
render: (_: any, record: IAfterSalesRecord) => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', gap: '8px' }}>
|
||||
<Tooltip title="创建退货记录">
|
||||
<Button size='small' onClick={() => handleMultiAfterSales(record, '退货')}>
|
||||
退
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title="创建换货记录">
|
||||
<Button size='small' onClick={() => handleMultiAfterSales(record, '换货')}>
|
||||
换
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title="创建补发记录">
|
||||
<Button size='small' onClick={() => handleMultiAfterSales(record, '补发')}>
|
||||
补
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title="创建差价补退记录">
|
||||
<Button size='small' onClick={() => handleMultiAfterSales(record, '补差')}>
|
||||
差
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', gap: '8px' }}>
|
||||
<Button size='small' type="primary"
|
||||
onClick={() => showShipModal(record)}
|
||||
>发货</Button>
|
||||
<Button size='small' type="primary"
|
||||
onClick={() => showEditModal(record)}
|
||||
>编辑</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
], [router, records, userInfo?.团队?._id, fetchRecords, isAdmin, handleMultiAfterSales, showShipModal, showEditModal, handleProgressChange]);
|
||||
|
||||
// 重置筛选条件
|
||||
const handleReset = () => {
|
||||
setTransactionDateRange([null, null]);
|
||||
setAfterSaleDateRange([null, null]);
|
||||
// 清空日期选择器显示的值
|
||||
setTransactionDateValue(null);
|
||||
setAfterSaleDateValue(null);
|
||||
};
|
||||
|
||||
// 导出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 originalProductsText = record.原产品?.map(product =>
|
||||
`${product.名称 || '未知'}`
|
||||
).join(', ') || '无产品';
|
||||
|
||||
// 处理替换产品信息
|
||||
const replacementProductsText = record.替换产品?.map(product =>
|
||||
`${product.名称 || '未知'}`
|
||||
).join(', ') || '无替换产品';
|
||||
|
||||
// 格式化日期
|
||||
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)}`
|
||||
: '未知';
|
||||
|
||||
// 返回导出的行数据
|
||||
return {
|
||||
'序号': index + 1,
|
||||
'客户姓名': record.销售记录.客户?.姓名 || '未知',
|
||||
'电话尾号': phoneNumber,
|
||||
'成交日期': record.销售记录.成交日期 ? formatDate(record.销售记录.成交日期) : '未知',
|
||||
'售后日期': formatDate(record.日期),
|
||||
'售后类型': record.类型 || '未知',
|
||||
'售后原因': record.原因 || '未知',
|
||||
'售后进度': record.售后进度 || '未知',
|
||||
'导购': record.销售记录.导购?.姓名 || '未知',
|
||||
'账号编号': record.销售记录.订单来源?.账号编号 || '未知',
|
||||
'原产品信息': originalProductsText,
|
||||
'替换产品信息': replacementProductsText,
|
||||
'应收金额': record.销售记录?.应收金额 || 0,
|
||||
'收款金额': record.销售记录?.收款金额 || 0,
|
||||
'收支平台': record.收支平台?.名称 || '未知',
|
||||
'收支类型': record.收支类型 || '未知',
|
||||
'收支金额': record.收支金额 || 0,
|
||||
'待收': record.待收 || 0,
|
||||
'备注': record.备注 || '无',
|
||||
};
|
||||
});
|
||||
|
||||
// 计算统计数据
|
||||
let totalIncome = 0;
|
||||
let totalExpense = 0;
|
||||
|
||||
filteredRecords.forEach(record => {
|
||||
if (record.收支类型 === '收入' && record.收支金额) {
|
||||
totalIncome += record.收支金额;
|
||||
} else if (record.收支类型 === '支出' && record.收支金额) {
|
||||
totalExpense += record.收支金额;
|
||||
}
|
||||
});
|
||||
// 创建工作簿和工作表
|
||||
//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: 20 }, // 售后原因
|
||||
{ wch: 8 }, // 售后进度
|
||||
{ wch: 10 }, // 导购
|
||||
{ wch: 10 }, // 账号编号
|
||||
{ wch: 40 }, // 原产品信息
|
||||
{ wch: 40 }, // 替换产品信息
|
||||
{ wch: 10 }, // 应收金额
|
||||
{ wch: 10 }, // 收款金额
|
||||
{ wch: 10 }, // 收支平台
|
||||
{ wch: 10 }, // 收支类型
|
||||
{ wch: 10 }, // 收支金额
|
||||
{ wch: 10 }, // 待收
|
||||
{ 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 (
|
||||
<Card>
|
||||
<div style={{
|
||||
marginBottom: 16,
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<span style={{ marginRight: 4, fontSize: '0.9em' }}>成交日期:</span>
|
||||
<DatePicker.RangePicker
|
||||
size="small"
|
||||
style={{ width: 160 }}
|
||||
value={transactionDateValue}
|
||||
onChange={(dates, dateStrings) => {
|
||||
setTransactionDateValue(dates);
|
||||
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]);
|
||||
}
|
||||
}}
|
||||
allowClear={true}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<span style={{ marginRight: 4, fontSize: '0.9em' }}>售后日期:</span>
|
||||
<DatePicker.RangePicker
|
||||
size="small"
|
||||
style={{ width: 160 }}
|
||||
value={afterSaleDateValue}
|
||||
onChange={(dates, dateStrings) => {
|
||||
setAfterSaleDateValue(dates);
|
||||
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);
|
||||
|
||||
setAfterSaleDateRange([startDate, endDate]);
|
||||
} else {
|
||||
setAfterSaleDateRange([null, null]);
|
||||
}
|
||||
}}
|
||||
allowClear={true}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={handleReset}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
{(transactionDateRange[0] || afterSaleDateRange[0]) && (
|
||||
<Tooltip title={`在${records.length}条记录中,筛选出${filteredRecords.length}条符合条件的记录`}>
|
||||
<Tag color="blue">
|
||||
共: {filteredRecords.length} / {records.length} 条记录
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* 添加导出按钮 */}
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<DownloadOutlined />}
|
||||
//onClick={exportToExcel}
|
||||
style={{ marginLeft: 8 }}
|
||||
>
|
||||
导出筛选
|
||||
</Button>
|
||||
|
||||
{/* 添加售后统计信息 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
marginLeft: 16,
|
||||
borderLeft: '1px solid #f0f0f0',
|
||||
paddingLeft: 16,
|
||||
gap: 8
|
||||
}}>
|
||||
{(() => {
|
||||
// 计算售后收入总和、支出总和和售后总额
|
||||
const calculateAfterSaleStats = () => {
|
||||
let totalIncome = 0;
|
||||
let totalExpense = 0;
|
||||
|
||||
filteredRecords.forEach(record => {
|
||||
// 根据收支类型计算收入或支出
|
||||
if (record.收支类型 === '收入' && record.收支金额) {
|
||||
totalIncome += record.收支金额;
|
||||
} else if (record.收支类型 === '支出' && record.收支金额) {
|
||||
totalExpense += record.收支金额;
|
||||
}
|
||||
});
|
||||
|
||||
// 计算售后总额(收入-支出)
|
||||
const totalAfterSale = totalIncome - totalExpense;
|
||||
|
||||
return {
|
||||
totalIncome,
|
||||
totalExpense,
|
||||
totalAfterSale
|
||||
};
|
||||
};
|
||||
|
||||
const stats = calculateAfterSaleStats();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip title="售后收入总和">
|
||||
<Tag color="green">
|
||||
收入: ¥{stats.totalIncome.toFixed(2)}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
<Tooltip title="售后支出总和">
|
||||
<Tag color="red">
|
||||
支出: ¥{stats.totalExpense.toFixed(2)}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
<Tooltip title="售后总额(收入-支出)">
|
||||
<Tag color={stats.totalAfterSale >= 0 ? "blue" : "volcano"}>
|
||||
售后总额: ¥{stats.totalAfterSale.toFixed(2)}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
<Table
|
||||
sticky
|
||||
scroll={{
|
||||
y: `calc(100vh - 300px)`,
|
||||
//x: 'max-content'
|
||||
}}
|
||||
//pagination={false} // 禁用分页
|
||||
pagination={{
|
||||
pageSize: 100,
|
||||
showSizeChanger: true, // 显示切换每页数量的下拉框
|
||||
pageSizeOptions: ['20', '50', '100', '120', '150', '200'], // 指定每页可以显示多少条
|
||||
}}
|
||||
size='small'
|
||||
rowKey="_id"
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={filteredRecords}
|
||||
/>
|
||||
<MultiAfterSalesModal
|
||||
visible={multiafterSalesVisible}
|
||||
onOk={handleMultiAfterSalesModalOk}
|
||||
onCancel={() => setMultiafterSalesVisible(false)}
|
||||
record={currentafterSalesRecord}
|
||||
type={multiafterSalesType!}
|
||||
/>
|
||||
<ShipModal
|
||||
visible={isShipModalVisible} // 是否显示发货模态框
|
||||
onOk={handleShipModalOk}
|
||||
onCancel={handleModalCancel}
|
||||
record={currentRecord} // 当前选中的销售记录
|
||||
/>
|
||||
<EditAfterSalesModal
|
||||
visible={editModalVisible}
|
||||
onOk={handleEditModalOk}
|
||||
onCancel={handleEditModalCancel}
|
||||
record={currentEditRecord}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default AfterSaleRecordPage;
|
||||
202
src/pages/team/AfterSaleRecord/ship-modal.tsx
Normal file
202
src/pages/team/AfterSaleRecord/ship-modal.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
/**
|
||||
* 文件: ship-modal.tsx
|
||||
* 作者: 阿瑞
|
||||
* 功能: 售后发货模态框组件
|
||||
* 版本: v2.0.0 - 使用 fetch 替换 axios
|
||||
*/
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Modal, Form, Input, Button, App } from 'antd';
|
||||
import { IAfterSalesRecord } from '@/models/types';
|
||||
import { useUserInfo } from '@/store/userStore';
|
||||
|
||||
interface ShipModalProps {
|
||||
visible: boolean;
|
||||
onOk: () => void;
|
||||
onCancel: () => void;
|
||||
record: IAfterSalesRecord | null; // 传入的售后记录
|
||||
}
|
||||
|
||||
const ShipModal: React.FC<ShipModalProps> = ({ visible, onOk, onCancel, record }) => {
|
||||
const { message } = App.useApp(); // 使用 App.useApp 获取 message 实例
|
||||
const [form] = Form.useForm();
|
||||
const [logisticsNumbers, setLogisticsNumbers] = useState<{ [key: string]: string }>({}); // 保存每个产品的物流单号
|
||||
const userInfo = useUserInfo(); // 获取当前用户信息
|
||||
|
||||
useEffect(() => {
|
||||
if (record) {
|
||||
// 清空表单
|
||||
form.resetFields();
|
||||
form.setFieldsValue({
|
||||
客户尾号: record?.销售记录?.客户.电话 ? record.销售记录.客户.电话.slice(-4) : '', // 自动填入客户电话尾号
|
||||
});
|
||||
|
||||
// 初始化物流单号状态
|
||||
const initialLogisticsNumbers: { [key: string]: string } = {};
|
||||
|
||||
// 初始化原产品的物流单号
|
||||
record?.原产品?.forEach(product => {
|
||||
initialLogisticsNumbers[product._id] = ''; // 初始化原产品的物流单号为空
|
||||
});
|
||||
|
||||
// 初始化替换产品的物流单号
|
||||
record?.替换产品?.forEach(product => {
|
||||
initialLogisticsNumbers[product._id] = ''; // 初始化替换产品的物流单号为空
|
||||
});
|
||||
|
||||
setLogisticsNumbers(initialLogisticsNumbers);
|
||||
|
||||
// 获取已有的物流记录并填充单号
|
||||
const fetchLogisticsRecords = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/tools/logistics?关联记录=${record._id}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const logisticsRecords = await response.json();
|
||||
if (logisticsRecords && Array.isArray(logisticsRecords)) {
|
||||
const updatedLogisticsNumbers: { [key: string]: string } = { ...initialLogisticsNumbers };
|
||||
|
||||
// 遍历物流记录,将已有的单号填充到对应的产品
|
||||
logisticsRecords.forEach((logisticsRecord: any) => {
|
||||
const productId = logisticsRecord.产品?._id || logisticsRecord.产品;
|
||||
if (productId && logisticsRecord.物流单号) {
|
||||
updatedLogisticsNumbers[productId] = logisticsRecord.物流单号;
|
||||
}
|
||||
});
|
||||
|
||||
setLogisticsNumbers(updatedLogisticsNumbers); // 更新状态以填充表单
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error('获取物流记录失败:', error);
|
||||
message.error('获取物流记录失败');
|
||||
}
|
||||
};
|
||||
|
||||
fetchLogisticsRecords();
|
||||
}
|
||||
}, [record, form, message]);
|
||||
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
|
||||
// 过滤出有物流单号的产品
|
||||
const originalProductsWithLogisticsNumbers = (record?.原产品 || [])
|
||||
.map(product => ({
|
||||
productId: product._id,
|
||||
logisticsNumber: logisticsNumbers[product._id]
|
||||
}))
|
||||
.filter(item => item.logisticsNumber); // 只保留填写了物流单号的原产品
|
||||
|
||||
const replacementProductsWithLogisticsNumbers = (record?.替换产品 || [])
|
||||
.map(product => ({
|
||||
productId: product._id,
|
||||
logisticsNumber: logisticsNumbers[product._id]
|
||||
}))
|
||||
.filter(item => item.logisticsNumber); // 只保留填写了物流单号的替换产品
|
||||
|
||||
const productsWithLogisticsNumbers = [...originalProductsWithLogisticsNumbers, ...replacementProductsWithLogisticsNumbers];
|
||||
|
||||
if (productsWithLogisticsNumbers.length === 0) {
|
||||
message.error('请至少为一个产品填写物流单号');
|
||||
return;
|
||||
}
|
||||
|
||||
const logisticsData = {
|
||||
...values,
|
||||
团队: userInfo.团队?._id,
|
||||
关联记录: record?._id,
|
||||
类型: 'AfterSalesRecord', // 确保类型为售后记录
|
||||
产品: productsWithLogisticsNumbers // 只提交填写了物流单号的产品
|
||||
};
|
||||
|
||||
const response = await fetch('/api/tools/logistics', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(logisticsData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
message.success('发货信息提交成功');
|
||||
onOk(); // 关闭模态框
|
||||
} catch (error: unknown) {
|
||||
console.error('发货信息提交失败:', error);
|
||||
message.error('发货信息提交失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogisticsNumberChange = (productId: string, value: string) => {
|
||||
setLogisticsNumbers(prevState => ({
|
||||
...prevState,
|
||||
[productId]: value // 更新每个产品的物流单号
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={visible}
|
||||
title="发货信息"
|
||||
onCancel={onCancel}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={onCancel}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button key="submit" type="primary" onClick={handleOk}>
|
||||
保存
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
name="客户尾号"
|
||||
label="请核对客户尾号"
|
||||
rules={[{ required: true, message: '请输入客户电话尾号' }]}
|
||||
>
|
||||
<Input placeholder="自动填入客户电话尾号"
|
||||
//disabled
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 动态生成原产品的物流单号输入框 */}
|
||||
{record?.原产品?.map(product => (
|
||||
<Form.Item
|
||||
key={product._id}
|
||||
label={`原产品退回单号 (${product.名称})`}
|
||||
>
|
||||
<Input
|
||||
placeholder={`请输入${product.名称}的物流单号`}
|
||||
value={logisticsNumbers[product._id] || ''}
|
||||
onChange={e => handleLogisticsNumberChange(product._id, e.target.value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
))}
|
||||
|
||||
{/* 动态生成替换产品的物流单号输入框 */}
|
||||
{record?.替换产品?.map(product => (
|
||||
<Form.Item
|
||||
key={product._id}
|
||||
label={`替换产品发出单号 (${product.名称})`}
|
||||
>
|
||||
<Input
|
||||
placeholder={`请输入${product.名称}的物流单号`}
|
||||
value={logisticsNumbers[product._id] || ''}
|
||||
//onChange={e => handleLogisticsNumberChange(product._id, e.target.value)}
|
||||
onChange={e => {
|
||||
const cleanedValue = e.target.value.replace(/[\s.,/#!$%\^&\*;:{}=\-_`~()<>[\]'"|\\?@+]/g, '');//过滤特殊字符和空格
|
||||
handleLogisticsNumberChange(product._id, cleanedValue);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
))}
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShipModal;
|
||||
@@ -319,7 +319,7 @@ const AddProductComponent: React.FC<AddProductProps> = ({ visible, onClose, onSu
|
||||
onCancel={onClose}
|
||||
width="90%"
|
||||
footer={null}
|
||||
destroyOnClose={true}
|
||||
destroyOnHidden={true}
|
||||
maskClosable={false}
|
||||
zIndex={1100}
|
||||
getContainer={false}
|
||||
|
||||
@@ -293,7 +293,7 @@ const SalesRecordPage: React.FC<SalesRecordPageProps> = ({ salesRecord, onCancel
|
||||
//padding: '20px',
|
||||
borderRadius: '8px',
|
||||
}}>
|
||||
<Card bordered style={{ width: '100%', borderRadius: '8px', boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
<Card style={{ width: '100%', borderRadius: '8px' }}>
|
||||
<Row gutter={24} >
|
||||
<Col span={8} >
|
||||
<CustomerInfoComponent
|
||||
@@ -403,7 +403,8 @@ const SalesRecordPage: React.FC<SalesRecordPageProps> = ({ salesRecord, onCancel
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
bordered style={{ width: '100%', borderRadius: '8px', boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
variant="outlined"
|
||||
style={{ width: '100%', borderRadius: '8px', boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
|
||||
@@ -508,7 +508,7 @@ export default function AntdUITestPage(): React.ReactElement {
|
||||
<Title level={4}>卡片组件</Title>
|
||||
<Row gutter={[16, 16]} className="mt-4">
|
||||
<Col span={8}>
|
||||
<Card title="基础卡片" bordered={false}>
|
||||
<Card title="基础卡片" variant="outlined">
|
||||
<p>这是一个基础的卡片组件</p>
|
||||
<p>可以包含任何内容</p>
|
||||
</Card>
|
||||
|
||||
2
src/theme/hooks/index.ts
Normal file
2
src/theme/hooks/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { useThemeToken } from './use-theme-token';
|
||||
export { useResponsive } from './use-reponsive';
|
||||
40
src/theme/hooks/use-reponsive.ts
Normal file
40
src/theme/hooks/use-reponsive.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Grid, theme } from 'antd';
|
||||
import { Breakpoint, ScreenMap, ScreenSizeMap } from 'antd/es/_util/responsiveObserver';
|
||||
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
export function useResponsive() {
|
||||
const {
|
||||
token: { screenXS, screenSM, screenMD, screenLG, screenXL, screenXXL },
|
||||
} = theme.useToken();
|
||||
const screenArray: Breakpoint[] = ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'];
|
||||
|
||||
const screenEnum: ScreenSizeMap = {
|
||||
xs: screenXS,
|
||||
sm: screenSM,
|
||||
md: screenMD,
|
||||
lg: screenLG,
|
||||
xl: screenXL,
|
||||
xxl: screenXXL,
|
||||
};
|
||||
const screenMap: ScreenMap = useBreakpoint();
|
||||
/*
|
||||
const currentScrren = screenArray.findLast((item) => {
|
||||
const result = screenMap[item];
|
||||
return result === true;
|
||||
});
|
||||
*/
|
||||
|
||||
// 使用 [...screenArray].reverse().find() 来代替 findLast 方法,避免兼容性问题
|
||||
// [...screenArray] 创建了一个 screenArray 的副本,这样 reverse 方法不会改变原数组的顺序
|
||||
const currentScrren = [...screenArray].reverse().find((item) => {
|
||||
const result = screenMap[item];
|
||||
return result === true;
|
||||
});
|
||||
|
||||
return {
|
||||
screenEnum,
|
||||
screenMap,
|
||||
currentScrren,
|
||||
};
|
||||
}
|
||||
9
src/theme/hooks/use-theme-token.ts
Normal file
9
src/theme/hooks/use-theme-token.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { theme } from 'antd';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export function useThemeToken() {
|
||||
const { token } = theme.useToken();
|
||||
|
||||
// 确保 useMemo 仅依赖稳定的值
|
||||
return useMemo(() => token, [token]);
|
||||
}
|
||||
Reference in New Issue
Block a user