0607.4
Some checks failed
Next.js CI/CD 流水线 / deploy (push) Failing after 40s

This commit is contained in:
2025-06-07 01:41:58 +08:00
parent 7c11b0e57f
commit eb79e416db
39 changed files with 3177 additions and 25 deletions

View File

@@ -4,7 +4,7 @@ const nextConfig: NextConfig = {
/* config options here */ /* config options here */
reactStrictMode: true, reactStrictMode: true,
// 启用 standalone 输出模式,用于 Docker 部署 // 启用 standalone 输出模式,用于 Docker 部署Windows 本地开发时可能需要注释掉)
output: 'standalone', output: 'standalone',
// 优化生产构建 // 优化生产构建

View File

@@ -16,26 +16,33 @@
"@iconify/react": "^4.1.1", "@iconify/react": "^4.1.1",
"@types/lodash": "^4.17.17", "@types/lodash": "^4.17.17",
"antd": "^5.25.4", "antd": "^5.25.4",
"apexcharts": "^4.7.0",
"bcryptjs": "^3.0.2", "bcryptjs": "^3.0.2",
"color": "^5.0.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"geist": "^1.4.2", "geist": "^1.4.2",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mongoose": "^8.15.1", "mongoose": "^8.15.1",
"next": "15.3.3", "next": "15.3.3",
"ramda": "^0.30.1",
"react": "^19.0.0", "react": "^19.0.0",
"react-apexcharts": "^1.7.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-error-boundary": "^6.0.0", "react-error-boundary": "^6.0.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"styled-components": "^6.0.9", "styled-components": "^6.0.9",
"uuid": "^11.1.0",
"zustand": "^5.0.5" "zustand": "^5.0.5"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"@types/jsonwebtoken": "^9.0.9", "@types/jsonwebtoken": "^9.0.9",
"@types/node": "^20", "@types/node": "^20",
"@types/ramda": "^0.30.2",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"@types/uuid": "^10.0.0",
"tailwindcss": "^4", "tailwindcss": "^4",
"typescript": "^5" "typescript": "^5"
} }

160
pnpm-lock.yaml generated
View File

@@ -29,9 +29,15 @@ importers:
antd: antd:
specifier: ^5.25.4 specifier: ^5.25.4
version: 5.25.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) 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: bcryptjs:
specifier: ^3.0.2 specifier: ^3.0.2
version: 3.0.2 version: 3.0.2
color:
specifier: ^5.0.0
version: 5.0.0
dayjs: dayjs:
specifier: ^1.11.13 specifier: ^1.11.13
version: 1.11.13 version: 1.11.13
@@ -50,9 +56,15 @@ importers:
next: next:
specifier: 15.3.3 specifier: 15.3.3
version: 15.3.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) 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: react:
specifier: ^19.0.0 specifier: ^19.0.0
version: 19.1.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: react-dom:
specifier: ^19.0.0 specifier: ^19.0.0
version: 19.1.0(react@19.1.0) version: 19.1.0(react@19.1.0)
@@ -65,6 +77,9 @@ importers:
styled-components: styled-components:
specifier: ^6.0.9 specifier: ^6.0.9
version: 6.1.18(react-dom@19.1.0(react@19.1.0))(react@19.1.0) 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: zustand:
specifier: ^5.0.5 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)) 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': '@types/node':
specifier: ^20 specifier: ^20
version: 20.17.57 version: 20.17.57
'@types/ramda':
specifier: ^0.30.2
version: 0.30.2
'@types/react': '@types/react':
specifier: ^19 specifier: ^19
version: 19.1.6 version: 19.1.6
'@types/react-dom': '@types/react-dom':
specifier: ^19 specifier: ^19
version: 19.1.5(@types/react@19.1.6) version: 19.1.5(@types/react@19.1.6)
'@types/uuid':
specifier: ^10.0.0
version: 10.0.0
tailwindcss: tailwindcss:
specifier: ^4 specifier: ^4
version: 4.1.8 version: 4.1.8
@@ -553,6 +574,31 @@ packages:
react: '>=18.0.0' react: '>=18.0.0'
react-dom: '>=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': '@swc/counter@0.1.3':
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
@@ -659,6 +705,9 @@ packages:
'@types/node@20.17.57': '@types/node@20.17.57':
resolution: {integrity: sha512-f3T4y6VU4fVQDKVqJV4Uppy8c1p/sVvS3peyqxyWnzkqXFJLRU7Y1Bl7rMS1Qe9z0v4M6McY0Fp9yBsgHJUsWQ==} resolution: {integrity: sha512-f3T4y6VU4fVQDKVqJV4Uppy8c1p/sVvS3peyqxyWnzkqXFJLRU7Y1Bl7rMS1Qe9z0v4M6McY0Fp9yBsgHJUsWQ==}
'@types/ramda@0.30.2':
resolution: {integrity: sha512-PyzHvjCalm2BRYjAU6nIB3TprYwMNOUY/7P/N8bSzp9W/yM2YrtGtAnnVtaCNSeOZ8DzKyFDvaqQs7LnWwwmBA==}
'@types/react-dom@19.1.5': '@types/react-dom@19.1.5':
resolution: {integrity: sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg==} resolution: {integrity: sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg==}
peerDependencies: peerDependencies:
@@ -670,6 +719,9 @@ packages:
'@types/stylis@4.2.5': '@types/stylis@4.2.5':
resolution: {integrity: sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==} 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': '@types/webidl-conversions@7.0.3':
resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==}
@@ -684,6 +736,9 @@ packages:
peerDependencies: peerDependencies:
react: '*' react: '*'
'@yr/monotone-cubic-spline@1.0.3':
resolution: {integrity: sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==}
add-dom-event-listener@1.1.0: add-dom-event-listener@1.1.0:
resolution: {integrity: sha512-WCxx1ixHT0GQU9hb0KI/mhgRQhnU+U3GvwY6ZvVjYq8rsihIGoaIOUbY0yMPBxLH5MDtr0kz3fisWGNcbWW7Jw==} resolution: {integrity: sha512-WCxx1ixHT0GQU9hb0KI/mhgRQhnU+U3GvwY6ZvVjYq8rsihIGoaIOUbY0yMPBxLH5MDtr0kz3fisWGNcbWW7Jw==}
@@ -693,6 +748,9 @@ packages:
react: '>=16.9.0' react: '>=16.9.0'
react-dom: '>=16.9.0' react-dom: '>=16.9.0'
apexcharts@4.7.0:
resolution: {integrity: sha512-iZSrrBGvVlL+nt2B1NpqfDuBZ9jX61X9I2+XV0hlYXHtTwhwLTHDKGXjNXAgFBDLuvSYCB/rq2nPWVPRv2DrGA==}
bcryptjs@3.0.2: bcryptjs@3.0.2:
resolution: {integrity: sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==} resolution: {integrity: sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==}
hasBin: true hasBin: true
@@ -728,16 +786,32 @@ packages:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'} engines: {node: '>=7.0.0'}
color-convert@3.1.0:
resolution: {integrity: sha512-TVoqAq8ZDIpK5lsQY874DDnu65CSsc9vzq0wLpNQ6UMBq81GSZocVazPiBbYGzngzBOIRahpkTzCLVe2at4MfA==}
engines: {node: '>=14.6'}
color-name@1.1.4: color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 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: color-string@1.9.1:
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} 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: color@4.2.3:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
engines: {node: '>=12.5.0'} engines: {node: '>=12.5.0'}
color@5.0.0:
resolution: {integrity: sha512-16BlyiuyLq3MLxpRWyOTiWsO3ii/eLQLJUQXBSNcxMBBSnyt1ee9YUdaozQp03ifwm5woztEZGDbk9RGVuCsdw==}
engines: {node: '>=18'}
compute-scroll-into-view@3.1.1: compute-scroll-into-view@3.1.1:
resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==} resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==}
@@ -1034,6 +1108,9 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'} engines: {node: '>=6'}
ramda@0.30.1:
resolution: {integrity: sha512-tEF5I22zJnuclswcZMc8bDIrwRHRzf+NqVEmqg50ShAZMP7MWeR/RGDthfM/p+BlqvF2fXAzpn8i+SJcYD3alw==}
rc-cascader@3.34.0: rc-cascader@3.34.0:
resolution: {integrity: sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag==} resolution: {integrity: sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag==}
peerDependencies: peerDependencies:
@@ -1271,6 +1348,12 @@ packages:
react: '>=16.9.0' react: '>=16.9.0'
react-dom: '>=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: react-dom@19.1.0:
resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==}
peerDependencies: peerDependencies:
@@ -1408,12 +1491,18 @@ packages:
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
engines: {node: '>=18'} engines: {node: '>=18'}
ts-toolbelt@9.6.0:
resolution: {integrity: sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==}
tslib@2.6.2: tslib@2.6.2:
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
tslib@2.8.1: tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
types-ramda@0.30.1:
resolution: {integrity: sha512-1HTsf5/QVRmLzcGfldPFvkVsAdi1db1BBKzi7iW3KBUlOICg/nKnFS+jGqDJS3YD8VsWbAh7JiHeBvbsw8RPxA==}
typescript@5.8.3: typescript@5.8.3:
resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
@@ -1427,6 +1516,10 @@ packages:
peerDependencies: peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 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: warning@4.0.3:
resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==}
@@ -2000,6 +2093,25 @@ snapshots:
react-dom: 19.1.0(react@19.1.0) react-dom: 19.1.0(react@19.1.0)
react-is: 18.3.1 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/counter@0.1.3': {}
'@swc/helpers@0.5.15': '@swc/helpers@0.5.15':
@@ -2091,6 +2203,10 @@ snapshots:
dependencies: dependencies:
undici-types: 6.19.8 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)': '@types/react-dom@19.1.5(@types/react@19.1.6)':
dependencies: dependencies:
'@types/react': 19.1.6 '@types/react': 19.1.6
@@ -2101,6 +2217,8 @@ snapshots:
'@types/stylis@4.2.5': {} '@types/stylis@4.2.5': {}
'@types/uuid@10.0.0': {}
'@types/webidl-conversions@7.0.3': {} '@types/webidl-conversions@7.0.3': {}
'@types/whatwg-url@11.0.5': '@types/whatwg-url@11.0.5':
@@ -2113,6 +2231,8 @@ snapshots:
dependencies: dependencies:
react: 19.1.0 react: 19.1.0
'@yr/monotone-cubic-spline@1.0.3': {}
add-dom-event-listener@1.1.0: add-dom-event-listener@1.1.0:
dependencies: dependencies:
object-assign: 4.1.1 object-assign: 4.1.1
@@ -2175,6 +2295,15 @@ snapshots:
- luxon - luxon
- moment - 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: {} bcryptjs@3.0.2: {}
bson@6.10.4: {} bson@6.10.4: {}
@@ -2200,21 +2329,36 @@ snapshots:
color-name: 1.1.4 color-name: 1.1.4
optional: true optional: true
color-convert@3.1.0:
dependencies:
color-name: 2.0.0
color-name@1.1.4: color-name@1.1.4:
optional: true optional: true
color-name@2.0.0: {}
color-string@1.9.1: color-string@1.9.1:
dependencies: dependencies:
color-name: 1.1.4 color-name: 1.1.4
simple-swizzle: 0.2.2 simple-swizzle: 0.2.2
optional: true optional: true
color-string@2.0.1:
dependencies:
color-name: 2.0.0
color@4.2.3: color@4.2.3:
dependencies: dependencies:
color-convert: 2.0.1 color-convert: 2.0.1
color-string: 1.9.1 color-string: 1.9.1
optional: true optional: true
color@5.0.0:
dependencies:
color-convert: 3.1.0
color-string: 2.0.1
compute-scroll-into-view@3.1.1: {} compute-scroll-into-view@3.1.1: {}
copy-to-clipboard@3.3.3: copy-to-clipboard@3.3.3:
@@ -2475,6 +2619,8 @@ snapshots:
punycode@2.3.1: {} 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): rc-cascader@3.34.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies: dependencies:
'@babel/runtime': 7.27.4 '@babel/runtime': 7.27.4
@@ -2811,6 +2957,12 @@ snapshots:
react: 19.1.0 react: 19.1.0
react-dom: 19.1.0(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): react-dom@19.1.0(react@19.1.0):
dependencies: dependencies:
react: 19.1.0 react: 19.1.0
@@ -2952,10 +3104,16 @@ snapshots:
dependencies: dependencies:
punycode: 2.3.1 punycode: 2.3.1
ts-toolbelt@9.6.0: {}
tslib@2.6.2: {} tslib@2.6.2: {}
tslib@2.8.1: {} tslib@2.8.1: {}
types-ramda@0.30.1:
dependencies:
ts-toolbelt: 9.6.0
typescript@5.8.3: {} typescript@5.8.3: {}
undici-types@6.19.8: {} undici-types@6.19.8: {}
@@ -2964,6 +3122,8 @@ snapshots:
dependencies: dependencies:
react: 19.1.0 react: 19.1.0
uuid@11.1.0: {}
warning@4.0.3: warning@4.0.3:
dependencies: dependencies:
loose-envify: 1.4.0 loose-envify: 1.4.0

View 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);

View 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;
}
}
}
`;

View 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;
}

View 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);

View 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);

View 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);

View 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);

View 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);

View File

@@ -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);

View 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);

View 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;

View 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>
</>
);
}

View 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} />;
}

View 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} />;
}

View 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} />;
}

View 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} />;
}

View 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} />;
}

View 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} />;
}

View 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} />;
}

View 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} />;
}

View 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} />;
}

View 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} />;
}

View 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} />;
}

View 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} />;
}

View File

@@ -78,7 +78,7 @@ const UserInfoCard: React.FC<UserInfoCardProps> = ({ userInfo, loading = false }
<Card <Card
className={styles.userInfoCard} className={styles.userInfoCard}
loading={loading} loading={loading}
bordered={false} variant="outlined"
> >
<div className={styles.userHeader}> <div className={styles.userHeader}>
<Avatar <Avatar
@@ -137,7 +137,7 @@ const TeamInfoCard: React.FC<TeamInfoCardProps> = ({ userInfo }) => {
if (!team) { if (!team) {
return ( return (
<Card title="团队信息" className={styles.teamInfoCard} bordered={false}> <Card title="团队信息" className={styles.teamInfoCard} variant="outlined">
<Empty description="暂无团队信息" /> <Empty description="暂无团队信息" />
</Card> </Card>
); );
@@ -174,7 +174,7 @@ const TeamInfoCard: React.FC<TeamInfoCardProps> = ({ userInfo }) => {
</Space> </Space>
} }
className={styles.teamInfoCard} className={styles.teamInfoCard}
bordered={false} variant="outlined"
> >
<div className={styles.teamContent}> <div className={styles.teamContent}>
<div className={styles.teamHeader}> <div className={styles.teamHeader}>
@@ -221,7 +221,7 @@ const RolePermissionCard: React.FC<RolePermissionCardProps> = ({ userInfo }) =>
if (!role) { if (!role) {
return ( return (
<Card title="角色权限" className={styles.rolePermissionCard} bordered={false}> <Card title="角色权限" className={styles.rolePermissionCard} variant="outlined">
<Empty description="暂无角色信息" /> <Empty description="暂无角色信息" />
</Card> </Card>
); );
@@ -236,7 +236,7 @@ const RolePermissionCard: React.FC<RolePermissionCardProps> = ({ userInfo }) =>
</Space> </Space>
} }
className={styles.rolePermissionCard} className={styles.rolePermissionCard}
bordered={false} variant="outlined"
> >
<div className={styles.roleContent}> <div className={styles.roleContent}>
<div className={styles.roleHeader}> <div className={styles.roleHeader}>

View File

@@ -38,8 +38,13 @@ import {
GlobalOutlined, GlobalOutlined,
SafetyOutlined, SafetyOutlined,
LineChartOutlined, LineChartOutlined,
DashboardOutlined,
UserOutlined,
UsergroupAddOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import Link from 'next/link';
import { useTheme } from '@/hooks/useTheme'; // 关键代码行注释使用独立的useTheme Hook import { useTheme } from '@/hooks/useTheme'; // 关键代码行注释使用独立的useTheme Hook
import { useUserInfo, useUserHomePath } from '@/store/userStore'; // 关键代码行注释:用户状态管理
import { MdDarkMode, MdLightMode } from "react-icons/md"; import { MdDarkMode, MdLightMode } from "react-icons/md";
import styles from './index.module.css'; // 关键代码行注释导入CSS模块样式 import styles from './index.module.css'; // 关键代码行注释导入CSS模块样式
@@ -150,6 +155,10 @@ export default function HomePage(): React.ReactElement {
// 关键代码行注释:状态管理 - 使用_app.tsx的useTheme Hook确保mounted状态 // 关键代码行注释:状态管理 - 使用_app.tsx的useTheme Hook确保mounted状态
const { isDark, toggleTheme, mounted, isTransitioning } = useTheme(); // 关键代码行注释:解构获取所需的状态 const { isDark, toggleTheme, mounted, isTransitioning } = useTheme(); // 关键代码行注释:解构获取所需的状态
const { message, notification, modal } = App.useApp(); const { message, notification, modal } = App.useApp();
// 关键代码行注释:用户登录状态管理
const userInfo = useUserInfo(); // 关键代码行注释:获取用户信息
const homePath = useUserHomePath(); // 关键代码行注释:获取用户首页路径
// 关键代码行注释:如果还未挂载,显示加载状态避免闪烁 // 关键代码行注释:如果还未挂载,显示加载状态避免闪烁
if (!mounted) { if (!mounted) {
@@ -221,13 +230,30 @@ export default function HomePage(): React.ReactElement {
{isDark ? <MdLightMode /> : <MdDarkMode />} {isDark ? <MdLightMode /> : <MdDarkMode />}
</Button> </Button>
<Button type="text" className={styles.loginBtn}> {/* 关键代码行注释:根据用户登录状态显示不同的按钮 */}
{userInfo && userInfo._id ? (
</Button> // 已登录状态:显示控制台按钮
<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>
</Button>
</Link>
</>
)}
</div> </div>
</div> </div>
</Header> </Header>

View 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;

View 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;

View 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;

View 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;

View File

@@ -319,7 +319,7 @@ const AddProductComponent: React.FC<AddProductProps> = ({ visible, onClose, onSu
onCancel={onClose} onCancel={onClose}
width="90%" width="90%"
footer={null} footer={null}
destroyOnClose={true} destroyOnHidden={true}
maskClosable={false} maskClosable={false}
zIndex={1100} zIndex={1100}
getContainer={false} getContainer={false}

View File

@@ -200,11 +200,11 @@ const SalesRecordPage: React.FC<SalesRecordPageProps> = ({ salesRecord, onCancel
: '销售余额抵扣', : '销售余额抵扣',
}) })
}); });
if (!transactionResponse.ok) { if (!transactionResponse.ok) {
throw new Error(`Transaction failed! status: ${transactionResponse.status}`); throw new Error(`Transaction failed! status: ${transactionResponse.status}`);
} }
const transactionData = await transactionResponse.json(); const transactionData = await transactionResponse.json();
transactionId = transactionData._id; // 获取创建的交易记录的ID transactionId = transactionData._id; // 获取创建的交易记录的ID
console.log('生成的transactionId:', transactionId); // 打印transactionId console.log('生成的transactionId:', transactionId); // 打印transactionId
@@ -228,7 +228,7 @@ const SalesRecordPage: React.FC<SalesRecordPageProps> = ({ salesRecord, onCancel
}, },
body: JSON.stringify(requestData) body: JSON.stringify(requestData)
}); });
if (!salesResponse.ok) { if (!salesResponse.ok) {
throw new Error(`Sales record failed! status: ${salesResponse.status}`); throw new Error(`Sales record failed! status: ${salesResponse.status}`);
} }
@@ -280,12 +280,12 @@ const SalesRecordPage: React.FC<SalesRecordPageProps> = ({ salesRecord, onCancel
const totalPrice = selectedProducts.reduce((acc, cur) => acc + cur., 0); const totalPrice = selectedProducts.reduce((acc, cur) => acc + cur., 0);
// 定义处理编辑客户后的回调 // 定义处理编辑客户后的回调
const handleEditCustomerSuccess = (updatedCustomer: ICustomer) => { const handleEditCustomerSuccess = (updatedCustomer: ICustomer) => {
setSelectedCustomer(updatedCustomer); // 更新选中的客户 setSelectedCustomer(updatedCustomer); // 更新选中的客户
message.success('客户信息已更新'); message.success('客户信息已更新');
}; };
return ( return (
<div > <div >
@@ -293,7 +293,7 @@ const SalesRecordPage: React.FC<SalesRecordPageProps> = ({ salesRecord, onCancel
//padding: '20px', //padding: '20px',
borderRadius: '8px', 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} > <Row gutter={24} >
<Col span={8} > <Col span={8} >
<CustomerInfoComponent <CustomerInfoComponent
@@ -403,7 +403,8 @@ const SalesRecordPage: React.FC<SalesRecordPageProps> = ({ salesRecord, onCancel
</Button> </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={form} form={form}

View File

@@ -508,7 +508,7 @@ export default function AntdUITestPage(): React.ReactElement {
<Title level={4}></Title> <Title level={4}></Title>
<Row gutter={[16, 16]} className="mt-4"> <Row gutter={[16, 16]} className="mt-4">
<Col span={8}> <Col span={8}>
<Card title="基础卡片" bordered={false}> <Card title="基础卡片" variant="outlined">
<p></p> <p></p>
<p></p> <p></p>
</Card> </Card>

2
src/theme/hooks/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export { useThemeToken } from './use-theme-token';
export { useResponsive } from './use-reponsive';

View 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,
};
}

View 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]);
}