mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-15 00:36:56 +08:00
refactor(web): redesign httpClient with axios and unified error handling (#1061)
* fix(web): remove duplicate PasswordChecklist in error block
- Remove duplicate PasswordChecklist component from error message area
- Keep only the real-time password validation checklist
- Error block now displays only the error message text
Bug was introduced in commit aa0bd93 (PR #872)
Co-Authored-By: tinkle-community <tinklefund@gmail.com>
* refactor(web): redesign httpClient with axios and unified error handling
Major refactoring to improve error handling architecture:
## Changes
### 1. HTTP Client Redesign (httpClient.ts)
- Replaced native fetch with axios for better interceptor support
- Implemented request/response interceptors for centralized error handling
- Added automatic Bearer token injection in request interceptor
- Network errors and system errors (404, 403, 500) now intercepted and shown via toast
- Only business logic errors (4xx except 401/403/404) returned to caller
- New ApiResponse<T> interface for type-safe responses
### 2. API Migration (api.ts)
- Migrated all 31 API methods from legacy fetch-style to new httpClient
- Updated pattern: from `res.ok/res.json()` to `result.success/result.data`
- Removed getAuthHeaders() helper (token now auto-injected)
- Added TypeScript generics for better type safety
### 3. Component Updates
- AuthContext.tsx: Updated register() to use new API
- TraderConfigModal.tsx: Migrated 3 API calls (config, templates, balance)
- RegisterPage.tsx: Simplified error display (error type handling now in API layer)
### 4. Removed Legacy Code
- Removed legacyHttpClient compatibility wrapper (~30 lines)
- Removed legacyRequest() method
- Clean separation: API layer handles all error classification
## Benefits
- Centralized error handling - no need to check network/system errors in components
- Better UX - automatic toast notifications for system errors
- Type safety - generic ApiResponse<T> provides compile-time checks
- Cleaner business components - only handle business logic errors
- Consistent error messages across the application
Co-Authored-By: tinkle-community <tinklefund@gmail.com>
---------
Co-authored-by: tinkle-community <tinklefund@gmail.com>
This commit is contained in:
93
web/package-lock.json
generated
93
web/package-lock.json
generated
@@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
|
"axios": "^1.13.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
@@ -120,6 +121,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
|
||||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.5",
|
"@babel/generator": "^7.28.5",
|
||||||
@@ -451,6 +453,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
@@ -474,6 +477,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -2033,8 +2037,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||||
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
@@ -2155,6 +2158,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz",
|
||||||
"integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==",
|
"integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
@@ -2165,6 +2169,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
||||||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
|
"peer": true,
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^18.0.0"
|
"@types/react": "^18.0.0"
|
||||||
}
|
}
|
||||||
@@ -2205,6 +2210,7 @@
|
|||||||
"integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==",
|
"integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.46.3",
|
"@typescript-eslint/scope-manager": "8.46.3",
|
||||||
"@typescript-eslint/types": "8.46.3",
|
"@typescript-eslint/types": "8.46.3",
|
||||||
@@ -2529,6 +2535,7 @@
|
|||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -2829,7 +2836,6 @@
|
|||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/autoprefixer": {
|
"node_modules/autoprefixer": {
|
||||||
@@ -2885,6 +2891,17 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.13.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/axios/-/axios-1.13.2.tgz",
|
||||||
|
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.6",
|
||||||
|
"form-data": "^4.0.4",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@@ -2952,6 +2969,7 @@
|
|||||||
"url": "https://github.com/sponsors/ai"
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.8.19",
|
"baseline-browser-mapping": "^2.8.19",
|
||||||
"caniuse-lite": "^1.0.30001751",
|
"caniuse-lite": "^1.0.30001751",
|
||||||
@@ -2999,7 +3017,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
@@ -3260,7 +3277,6 @@
|
|||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"delayed-stream": "~1.0.0"
|
"delayed-stream": "~1.0.0"
|
||||||
@@ -3632,7 +3648,6 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
@@ -3682,8 +3697,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||||
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/dom-helpers": {
|
"node_modules/dom-helpers": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
@@ -3698,7 +3712,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.1",
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
@@ -3826,7 +3839,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -3836,7 +3848,6 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -3881,7 +3892,6 @@
|
|||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0"
|
"es-errors": "^1.3.0"
|
||||||
@@ -3894,7 +3904,6 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
@@ -4006,6 +4015,7 @@
|
|||||||
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -4066,6 +4076,7 @@
|
|||||||
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
|
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"eslint-config-prettier": "bin/cli.js"
|
"eslint-config-prettier": "bin/cli.js"
|
||||||
},
|
},
|
||||||
@@ -4527,6 +4538,26 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.11",
|
||||||
|
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||||
|
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/for-each": {
|
"node_modules/for-each": {
|
||||||
"version": "0.3.5",
|
"version": "0.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
||||||
@@ -4563,7 +4594,6 @@
|
|||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
@@ -4634,7 +4664,6 @@
|
|||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
"dev": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
@@ -4706,7 +4735,6 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.2",
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
@@ -4740,7 +4768,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dunder-proto": "^1.0.1",
|
"dunder-proto": "^1.0.1",
|
||||||
@@ -4834,7 +4861,6 @@
|
|||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -4906,7 +4932,6 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -4919,7 +4944,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"has-symbols": "^1.0.3"
|
"has-symbols": "^1.0.3"
|
||||||
@@ -4935,7 +4959,6 @@
|
|||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"function-bind": "^1.1.2"
|
"function-bind": "^1.1.2"
|
||||||
},
|
},
|
||||||
@@ -5567,6 +5590,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
||||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
@@ -5595,6 +5619,7 @@
|
|||||||
"integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==",
|
"integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cssstyle": "^4.1.0",
|
"cssstyle": "^4.1.0",
|
||||||
"data-urls": "^5.0.0",
|
"data-urls": "^5.0.0",
|
||||||
@@ -5969,7 +5994,6 @@
|
|||||||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"lz-string": "bin/bin.js"
|
"lz-string": "bin/bin.js"
|
||||||
}
|
}
|
||||||
@@ -5988,7 +6012,6 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -6020,7 +6043,6 @@
|
|||||||
"version": "1.52.0",
|
"version": "1.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
@@ -6030,7 +6052,6 @@
|
|||||||
"version": "2.1.35",
|
"version": "2.1.35",
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mime-db": "1.52.0"
|
"mime-db": "1.52.0"
|
||||||
@@ -6560,6 +6581,7 @@
|
|||||||
"url": "https://github.com/sponsors/ai"
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -6713,6 +6735,7 @@
|
|||||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"prettier": "bin/prettier.cjs"
|
"prettier": "bin/prettier.cjs"
|
||||||
},
|
},
|
||||||
@@ -6742,7 +6765,6 @@
|
|||||||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-regex": "^5.0.1",
|
"ansi-regex": "^5.0.1",
|
||||||
"ansi-styles": "^5.0.0",
|
"ansi-styles": "^5.0.0",
|
||||||
@@ -6758,7 +6780,6 @@
|
|||||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@@ -6769,7 +6790,6 @@
|
|||||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
},
|
},
|
||||||
@@ -6782,8 +6802,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/prop-types": {
|
"node_modules/prop-types": {
|
||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
@@ -6800,6 +6819,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
@@ -6834,6 +6859,7 @@
|
|||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
},
|
},
|
||||||
@@ -6845,6 +6871,7 @@
|
|||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
"scheduler": "^0.23.2"
|
"scheduler": "^0.23.2"
|
||||||
@@ -8036,6 +8063,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -8252,6 +8280,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -8402,6 +8431,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.4",
|
"fdir": "^6.4.4",
|
||||||
@@ -9006,6 +9036,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -9542,6 +9573,7 @@
|
|||||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.21.3",
|
"esbuild": "^0.21.3",
|
||||||
"postcss": "^8.4.43",
|
"postcss": "^8.4.43",
|
||||||
@@ -9955,6 +9987,7 @@
|
|||||||
"integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
|
"integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
|
"axios": "^1.13.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
|||||||
@@ -76,9 +76,9 @@ export function RegisterPage() {
|
|||||||
setQrCodeURL(result.qrCodeURL || '')
|
setQrCodeURL(result.qrCodeURL || '')
|
||||||
setStep('setup-otp')
|
setStep('setup-otp')
|
||||||
} else {
|
} else {
|
||||||
|
// Only business errors reach here (system/network errors shown via toast)
|
||||||
const msg = result.message || t('registrationFailed', language)
|
const msg = result.message || t('registrationFailed', language)
|
||||||
setError(msg)
|
setError(msg)
|
||||||
toast.error(msg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
@@ -298,36 +298,7 @@ export function RegisterPage() {
|
|||||||
color: 'var(--binance-red)',
|
color: 'var(--binance-red)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
{error}
|
||||||
className="mb-1"
|
|
||||||
style={{ color: 'var(--brand-light-gray)' }}
|
|
||||||
>
|
|
||||||
{t('passwordRequirements', language)}
|
|
||||||
</div>
|
|
||||||
<PasswordChecklist
|
|
||||||
rules={[
|
|
||||||
'minLength',
|
|
||||||
'capital',
|
|
||||||
'lowercase',
|
|
||||||
'number',
|
|
||||||
'specialChar',
|
|
||||||
'match',
|
|
||||||
]}
|
|
||||||
minLength={8}
|
|
||||||
specialCharsRegex={/[@#$%!&*?]/}
|
|
||||||
value={password}
|
|
||||||
valueAgain={confirmPassword}
|
|
||||||
messages={{
|
|
||||||
minLength: t('passwordRuleMinLength', language),
|
|
||||||
capital: t('passwordRuleUppercase', language),
|
|
||||||
lowercase: t('passwordRuleLowercase', language),
|
|
||||||
number: t('passwordRuleNumber', language),
|
|
||||||
specialChar: t('passwordRuleSpecial', language),
|
|
||||||
match: t('passwordRuleMatch', language),
|
|
||||||
}}
|
|
||||||
className="space-y-1"
|
|
||||||
onChange={(isValid) => setPasswordValid(isValid)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -115,10 +115,22 @@ export function TraderConfigModal({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchConfig = async () => {
|
const fetchConfig = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await httpClient.get('/api/config')
|
const result = await httpClient.get<{ default_coins?: string[] }>(
|
||||||
const config = await response.json()
|
'/api/config'
|
||||||
if (config.default_coins) {
|
)
|
||||||
setAvailableCoins(config.default_coins)
|
if (result.success && result.data?.default_coins) {
|
||||||
|
setAvailableCoins(result.data.default_coins)
|
||||||
|
} else {
|
||||||
|
// 使用默认币种列表
|
||||||
|
setAvailableCoins([
|
||||||
|
'BTCUSDT',
|
||||||
|
'ETHUSDT',
|
||||||
|
'SOLUSDT',
|
||||||
|
'BNBUSDT',
|
||||||
|
'XRPUSDT',
|
||||||
|
'DOGEUSDT',
|
||||||
|
'ADAUSDT',
|
||||||
|
])
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch config:', error)
|
console.error('Failed to fetch config:', error)
|
||||||
@@ -141,10 +153,14 @@ export function TraderConfigModal({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchPromptTemplates = async () => {
|
const fetchPromptTemplates = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await httpClient.get('/api/prompt-templates')
|
const result = await httpClient.get<{ templates?: { name: string }[] }>(
|
||||||
const data = await response.json()
|
'/api/prompt-templates'
|
||||||
if (data.templates) {
|
)
|
||||||
setPromptTemplates(data.templates)
|
if (result.success && result.data?.templates) {
|
||||||
|
setPromptTemplates(result.data.templates)
|
||||||
|
} else {
|
||||||
|
// 使用默认模板列表
|
||||||
|
setPromptTemplates([{ name: 'default' }, { name: 'aggressive' }])
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch prompt templates:', error)
|
console.error('Failed to fetch prompt templates:', error)
|
||||||
@@ -194,30 +210,26 @@ export function TraderConfigModal({
|
|||||||
setBalanceFetchError('')
|
setBalanceFetchError('')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('auth_token')
|
const result = await httpClient.get<{
|
||||||
if (!token) {
|
total_equity?: number
|
||||||
throw new Error('未登录,请先登录')
|
balance?: number
|
||||||
|
}>(`/api/account?trader_id=${traderData.trader_id}`)
|
||||||
|
|
||||||
|
if (result.success && result.data) {
|
||||||
|
// total_equity = 当前账户净值(包含未实现盈亏)
|
||||||
|
// 这应该作为新的初始余额
|
||||||
|
const currentBalance =
|
||||||
|
result.data.total_equity || result.data.balance || 0
|
||||||
|
|
||||||
|
setFormData((prev) => ({ ...prev, initial_balance: currentBalance }))
|
||||||
|
toast.success('已获取当前余额')
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || '获取余额失败')
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await httpClient.get(
|
|
||||||
`/api/account?trader_id=${traderData.trader_id}`,
|
|
||||||
{
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
|
|
||||||
// total_equity = 当前账户净值(包含未实现盈亏)
|
|
||||||
// 这应该作为新的初始余额
|
|
||||||
const currentBalance = data.total_equity || data.balance || 0
|
|
||||||
|
|
||||||
setFormData((prev) => ({ ...prev, initial_balance: currentBalance }))
|
|
||||||
toast.success('已获取当前余额')
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取余额失败:', error)
|
console.error('获取余额失败:', error)
|
||||||
setBalanceFetchError('获取余额失败,请检查网络连接')
|
setBalanceFetchError('获取余额失败,请检查网络连接')
|
||||||
toast.error('获取余额失败,请检查网络连接')
|
// Note: Network/system errors already shown via toast by httpClient
|
||||||
} finally {
|
} finally {
|
||||||
setIsFetchingBalance(false)
|
setIsFetchingBalance(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { createContext, useContext, useState, useEffect } from 'react'
|
import React, { createContext, useContext, useState, useEffect } from 'react'
|
||||||
import { getSystemConfig } from '../lib/config'
|
import { getSystemConfig } from '../lib/config'
|
||||||
import { reset401Flag } from '../lib/httpClient'
|
import { reset401Flag, httpClient } from '../lib/httpClient'
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: string
|
id: string
|
||||||
@@ -183,39 +183,36 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
password: string,
|
password: string,
|
||||||
betaCode?: string
|
betaCode?: string
|
||||||
) => {
|
) => {
|
||||||
try {
|
const requestBody: {
|
||||||
const requestBody: {
|
email: string
|
||||||
email: string
|
password: string
|
||||||
password: string
|
beta_code?: string
|
||||||
beta_code?: string
|
} = { email, password }
|
||||||
} = { email, password }
|
if (betaCode) {
|
||||||
if (betaCode) {
|
requestBody.beta_code = betaCode
|
||||||
requestBody.beta_code = betaCode
|
}
|
||||||
|
|
||||||
|
const result = await httpClient.post<{
|
||||||
|
user_id: string
|
||||||
|
otp_secret: string
|
||||||
|
qr_code_url: string
|
||||||
|
message: string
|
||||||
|
}>('/api/register', requestBody)
|
||||||
|
|
||||||
|
if (result.success && result.data) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
userID: result.data.user_id,
|
||||||
|
otpSecret: result.data.otp_secret,
|
||||||
|
qrCodeURL: result.data.qr_code_url,
|
||||||
|
message: result.message || result.data.message,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch('/api/register', {
|
// Only business errors reach here (system/network errors were intercepted)
|
||||||
method: 'POST',
|
return {
|
||||||
headers: {
|
success: false,
|
||||||
'Content-Type': 'application/json',
|
message: result.message || 'Registration failed',
|
||||||
},
|
|
||||||
body: JSON.stringify(requestBody),
|
|
||||||
})
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
userID: data.user_id,
|
|
||||||
otpSecret: data.otp_secret,
|
|
||||||
qrCodeURL: data.qr_code_url,
|
|
||||||
message: data.message,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return { success: false, message: data.error }
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
return { success: false, message: '注册失败,请重试' }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,117 +18,92 @@ import { httpClient } from './httpClient'
|
|||||||
|
|
||||||
const API_BASE = '/api'
|
const API_BASE = '/api'
|
||||||
|
|
||||||
// Helper function to get auth headers
|
|
||||||
function getAuthHeaders(): Record<string, string> {
|
|
||||||
const token = localStorage.getItem('auth_token')
|
|
||||||
const headers: Record<string, string> = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
|
|
||||||
if (token) {
|
|
||||||
headers['Authorization'] = `Bearer ${token}`
|
|
||||||
}
|
|
||||||
|
|
||||||
return headers
|
|
||||||
}
|
|
||||||
|
|
||||||
export const api = {
|
export const api = {
|
||||||
// AI交易员管理接口
|
// AI交易员管理接口
|
||||||
async getTraders(): Promise<TraderInfo[]> {
|
async getTraders(): Promise<TraderInfo[]> {
|
||||||
const res = await httpClient.get(`${API_BASE}/my-traders`, getAuthHeaders())
|
const result = await httpClient.get<TraderInfo[]>(`${API_BASE}/my-traders`)
|
||||||
if (!res.ok) throw new Error('获取trader列表失败')
|
if (!result.success) throw new Error('获取trader列表失败')
|
||||||
return res.json()
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取公开的交易员列表(无需认证)
|
// 获取公开的交易员列表(无需认证)
|
||||||
async getPublicTraders(): Promise<any[]> {
|
async getPublicTraders(): Promise<any[]> {
|
||||||
const res = await httpClient.get(`${API_BASE}/traders`)
|
const result = await httpClient.get<any[]>(`${API_BASE}/traders`)
|
||||||
if (!res.ok) throw new Error('获取公开trader列表失败')
|
if (!result.success) throw new Error('获取公开trader列表失败')
|
||||||
return res.json()
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
async createTrader(request: CreateTraderRequest): Promise<TraderInfo> {
|
async createTrader(request: CreateTraderRequest): Promise<TraderInfo> {
|
||||||
const res = await httpClient.post(
|
const result = await httpClient.post<TraderInfo>(
|
||||||
`${API_BASE}/traders`,
|
`${API_BASE}/traders`,
|
||||||
request,
|
request
|
||||||
getAuthHeaders()
|
|
||||||
)
|
)
|
||||||
if (!res.ok) throw new Error('创建交易员失败')
|
if (!result.success) throw new Error('创建交易员失败')
|
||||||
return res.json()
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteTrader(traderId: string): Promise<void> {
|
async deleteTrader(traderId: string): Promise<void> {
|
||||||
const res = await httpClient.delete(
|
const result = await httpClient.delete(`${API_BASE}/traders/${traderId}`)
|
||||||
`${API_BASE}/traders/${traderId}`,
|
if (!result.success) throw new Error('删除交易员失败')
|
||||||
getAuthHeaders()
|
|
||||||
)
|
|
||||||
if (!res.ok) throw new Error('删除交易员失败')
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async startTrader(traderId: string): Promise<void> {
|
async startTrader(traderId: string): Promise<void> {
|
||||||
const res = await httpClient.post(
|
const result = await httpClient.post(
|
||||||
`${API_BASE}/traders/${traderId}/start`,
|
`${API_BASE}/traders/${traderId}/start`
|
||||||
undefined,
|
|
||||||
getAuthHeaders()
|
|
||||||
)
|
)
|
||||||
if (!res.ok) throw new Error('启动交易员失败')
|
if (!result.success) throw new Error('启动交易员失败')
|
||||||
},
|
},
|
||||||
|
|
||||||
async stopTrader(traderId: string): Promise<void> {
|
async stopTrader(traderId: string): Promise<void> {
|
||||||
const res = await httpClient.post(
|
const result = await httpClient.post(`${API_BASE}/traders/${traderId}/stop`)
|
||||||
`${API_BASE}/traders/${traderId}/stop`,
|
if (!result.success) throw new Error('停止交易员失败')
|
||||||
undefined,
|
|
||||||
getAuthHeaders()
|
|
||||||
)
|
|
||||||
if (!res.ok) throw new Error('停止交易员失败')
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateTraderPrompt(
|
async updateTraderPrompt(
|
||||||
traderId: string,
|
traderId: string,
|
||||||
customPrompt: string
|
customPrompt: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const res = await httpClient.put(
|
const result = await httpClient.put(
|
||||||
`${API_BASE}/traders/${traderId}/prompt`,
|
`${API_BASE}/traders/${traderId}/prompt`,
|
||||||
{ custom_prompt: customPrompt },
|
{ custom_prompt: customPrompt }
|
||||||
getAuthHeaders()
|
|
||||||
)
|
)
|
||||||
if (!res.ok) throw new Error('更新自定义策略失败')
|
if (!result.success) throw new Error('更新自定义策略失败')
|
||||||
},
|
},
|
||||||
|
|
||||||
async getTraderConfig(traderId: string): Promise<TraderConfigData> {
|
async getTraderConfig(traderId: string): Promise<TraderConfigData> {
|
||||||
const res = await httpClient.get(
|
const result = await httpClient.get<TraderConfigData>(
|
||||||
`${API_BASE}/traders/${traderId}/config`,
|
`${API_BASE}/traders/${traderId}/config`
|
||||||
getAuthHeaders()
|
|
||||||
)
|
)
|
||||||
if (!res.ok) throw new Error('获取交易员配置失败')
|
if (!result.success) throw new Error('获取交易员配置失败')
|
||||||
return res.json()
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateTrader(
|
async updateTrader(
|
||||||
traderId: string,
|
traderId: string,
|
||||||
request: CreateTraderRequest
|
request: CreateTraderRequest
|
||||||
): Promise<TraderInfo> {
|
): Promise<TraderInfo> {
|
||||||
const res = await httpClient.put(
|
const result = await httpClient.put<TraderInfo>(
|
||||||
`${API_BASE}/traders/${traderId}`,
|
`${API_BASE}/traders/${traderId}`,
|
||||||
request,
|
request
|
||||||
getAuthHeaders()
|
|
||||||
)
|
)
|
||||||
if (!res.ok) throw new Error('更新交易员失败')
|
if (!result.success) throw new Error('更新交易员失败')
|
||||||
return res.json()
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
// AI模型配置接口
|
// AI模型配置接口
|
||||||
async getModelConfigs(): Promise<AIModel[]> {
|
async getModelConfigs(): Promise<AIModel[]> {
|
||||||
const res = await httpClient.get(`${API_BASE}/models`, getAuthHeaders())
|
const result = await httpClient.get<AIModel[]>(`${API_BASE}/models`)
|
||||||
if (!res.ok) throw new Error('获取模型配置失败')
|
if (!result.success) throw new Error('获取模型配置失败')
|
||||||
return res.json()
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取系统支持的AI模型列表(无需认证)
|
// 获取系统支持的AI模型列表(无需认证)
|
||||||
async getSupportedModels(): Promise<AIModel[]> {
|
async getSupportedModels(): Promise<AIModel[]> {
|
||||||
const res = await httpClient.get(`${API_BASE}/supported-models`)
|
const result = await httpClient.get<AIModel[]>(
|
||||||
if (!res.ok) throw new Error('获取支持的模型失败')
|
`${API_BASE}/supported-models`
|
||||||
return res.json()
|
)
|
||||||
|
if (!result.success) throw new Error('获取支持的模型失败')
|
||||||
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateModelConfigs(request: UpdateModelConfigRequest): Promise<void> {
|
async updateModelConfigs(request: UpdateModelConfigRequest): Promise<void> {
|
||||||
@@ -150,37 +125,31 @@ export const api = {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 发送加密数据
|
// 发送加密数据
|
||||||
const res = await httpClient.put(
|
const result = await httpClient.put(`${API_BASE}/models`, encryptedPayload)
|
||||||
`${API_BASE}/models`,
|
if (!result.success) throw new Error('更新模型配置失败')
|
||||||
encryptedPayload,
|
|
||||||
getAuthHeaders()
|
|
||||||
)
|
|
||||||
if (!res.ok) throw new Error('更新模型配置失败')
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 交易所配置接口
|
// 交易所配置接口
|
||||||
async getExchangeConfigs(): Promise<Exchange[]> {
|
async getExchangeConfigs(): Promise<Exchange[]> {
|
||||||
const res = await httpClient.get(`${API_BASE}/exchanges`, getAuthHeaders())
|
const result = await httpClient.get<Exchange[]>(`${API_BASE}/exchanges`)
|
||||||
if (!res.ok) throw new Error('获取交易所配置失败')
|
if (!result.success) throw new Error('获取交易所配置失败')
|
||||||
return res.json()
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取系统支持的交易所列表(无需认证)
|
// 获取系统支持的交易所列表(无需认证)
|
||||||
async getSupportedExchanges(): Promise<Exchange[]> {
|
async getSupportedExchanges(): Promise<Exchange[]> {
|
||||||
const res = await httpClient.get(`${API_BASE}/supported-exchanges`)
|
const result = await httpClient.get<Exchange[]>(
|
||||||
if (!res.ok) throw new Error('获取支持的交易所失败')
|
`${API_BASE}/supported-exchanges`
|
||||||
return res.json()
|
)
|
||||||
|
if (!result.success) throw new Error('获取支持的交易所失败')
|
||||||
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateExchangeConfigs(
|
async updateExchangeConfigs(
|
||||||
request: UpdateExchangeConfigRequest
|
request: UpdateExchangeConfigRequest
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const res = await httpClient.put(
|
const result = await httpClient.put(`${API_BASE}/exchanges`, request)
|
||||||
`${API_BASE}/exchanges`,
|
if (!result.success) throw new Error('更新交易所配置失败')
|
||||||
request,
|
|
||||||
getAuthHeaders()
|
|
||||||
)
|
|
||||||
if (!res.ok) throw new Error('更新交易所配置失败')
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 使用加密传输更新交易所配置
|
// 使用加密传输更新交易所配置
|
||||||
@@ -205,12 +174,11 @@ export const api = {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 发送加密数据
|
// 发送加密数据
|
||||||
const res = await httpClient.put(
|
const result = await httpClient.put(
|
||||||
`${API_BASE}/exchanges`,
|
`${API_BASE}/exchanges`,
|
||||||
encryptedPayload,
|
encryptedPayload
|
||||||
getAuthHeaders()
|
|
||||||
)
|
)
|
||||||
if (!res.ok) throw new Error('更新交易所配置失败')
|
if (!result.success) throw new Error('更新交易所配置失败')
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取系统状态(支持trader_id)
|
// 获取系统状态(支持trader_id)
|
||||||
@@ -218,9 +186,9 @@ export const api = {
|
|||||||
const url = traderId
|
const url = traderId
|
||||||
? `${API_BASE}/status?trader_id=${traderId}`
|
? `${API_BASE}/status?trader_id=${traderId}`
|
||||||
: `${API_BASE}/status`
|
: `${API_BASE}/status`
|
||||||
const res = await httpClient.get(url, getAuthHeaders())
|
const result = await httpClient.get<SystemStatus>(url)
|
||||||
if (!res.ok) throw new Error('获取系统状态失败')
|
if (!result.success) throw new Error('获取系统状态失败')
|
||||||
return res.json()
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取账户信息(支持trader_id)
|
// 获取账户信息(支持trader_id)
|
||||||
@@ -228,17 +196,10 @@ export const api = {
|
|||||||
const url = traderId
|
const url = traderId
|
||||||
? `${API_BASE}/account?trader_id=${traderId}`
|
? `${API_BASE}/account?trader_id=${traderId}`
|
||||||
: `${API_BASE}/account`
|
: `${API_BASE}/account`
|
||||||
const res = await httpClient.request(url, {
|
const result = await httpClient.get<AccountInfo>(url)
|
||||||
cache: 'no-store',
|
if (!result.success) throw new Error('获取账户信息失败')
|
||||||
headers: {
|
console.log('Account data fetched:', result.data)
|
||||||
...getAuthHeaders(),
|
return result.data!
|
||||||
'Cache-Control': 'no-cache',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if (!res.ok) throw new Error('获取账户信息失败')
|
|
||||||
const data = await res.json()
|
|
||||||
console.log('Account data fetched:', data)
|
|
||||||
return data
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取持仓列表(支持trader_id)
|
// 获取持仓列表(支持trader_id)
|
||||||
@@ -246,9 +207,9 @@ export const api = {
|
|||||||
const url = traderId
|
const url = traderId
|
||||||
? `${API_BASE}/positions?trader_id=${traderId}`
|
? `${API_BASE}/positions?trader_id=${traderId}`
|
||||||
: `${API_BASE}/positions`
|
: `${API_BASE}/positions`
|
||||||
const res = await httpClient.get(url, getAuthHeaders())
|
const result = await httpClient.get<Position[]>(url)
|
||||||
if (!res.ok) throw new Error('获取持仓列表失败')
|
if (!result.success) throw new Error('获取持仓列表失败')
|
||||||
return res.json()
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取决策日志(支持trader_id)
|
// 获取决策日志(支持trader_id)
|
||||||
@@ -256,9 +217,9 @@ export const api = {
|
|||||||
const url = traderId
|
const url = traderId
|
||||||
? `${API_BASE}/decisions?trader_id=${traderId}`
|
? `${API_BASE}/decisions?trader_id=${traderId}`
|
||||||
: `${API_BASE}/decisions`
|
: `${API_BASE}/decisions`
|
||||||
const res = await httpClient.get(url, getAuthHeaders())
|
const result = await httpClient.get<DecisionRecord[]>(url)
|
||||||
if (!res.ok) throw new Error('获取决策日志失败')
|
if (!result.success) throw new Error('获取决策日志失败')
|
||||||
return res.json()
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取最新决策(支持trader_id和limit参数)
|
// 获取最新决策(支持trader_id和limit参数)
|
||||||
@@ -272,12 +233,11 @@ export const api = {
|
|||||||
}
|
}
|
||||||
params.append('limit', limit.toString())
|
params.append('limit', limit.toString())
|
||||||
|
|
||||||
const res = await httpClient.get(
|
const result = await httpClient.get<DecisionRecord[]>(
|
||||||
`${API_BASE}/decisions/latest?${params}`,
|
`${API_BASE}/decisions/latest?${params}`
|
||||||
getAuthHeaders()
|
|
||||||
)
|
)
|
||||||
if (!res.ok) throw new Error('获取最新决策失败')
|
if (!result.success) throw new Error('获取最新决策失败')
|
||||||
return res.json()
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取统计信息(支持trader_id)
|
// 获取统计信息(支持trader_id)
|
||||||
@@ -285,9 +245,9 @@ export const api = {
|
|||||||
const url = traderId
|
const url = traderId
|
||||||
? `${API_BASE}/statistics?trader_id=${traderId}`
|
? `${API_BASE}/statistics?trader_id=${traderId}`
|
||||||
: `${API_BASE}/statistics`
|
: `${API_BASE}/statistics`
|
||||||
const res = await httpClient.get(url, getAuthHeaders())
|
const result = await httpClient.get<Statistics>(url)
|
||||||
if (!res.ok) throw new Error('获取统计信息失败')
|
if (!result.success) throw new Error('获取统计信息失败')
|
||||||
return res.json()
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取收益率历史数据(支持trader_id)
|
// 获取收益率历史数据(支持trader_id)
|
||||||
@@ -295,32 +255,35 @@ export const api = {
|
|||||||
const url = traderId
|
const url = traderId
|
||||||
? `${API_BASE}/equity-history?trader_id=${traderId}`
|
? `${API_BASE}/equity-history?trader_id=${traderId}`
|
||||||
: `${API_BASE}/equity-history`
|
: `${API_BASE}/equity-history`
|
||||||
const res = await httpClient.get(url, getAuthHeaders())
|
const result = await httpClient.get<any[]>(url)
|
||||||
if (!res.ok) throw new Error('获取历史数据失败')
|
if (!result.success) throw new Error('获取历史数据失败')
|
||||||
return res.json()
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
// 批量获取多个交易员的历史数据(无需认证)
|
// 批量获取多个交易员的历史数据(无需认证)
|
||||||
async getEquityHistoryBatch(traderIds: string[]): Promise<any> {
|
async getEquityHistoryBatch(traderIds: string[]): Promise<any> {
|
||||||
const res = await httpClient.post(`${API_BASE}/equity-history-batch`, {
|
const result = await httpClient.post<any>(
|
||||||
trader_ids: traderIds,
|
`${API_BASE}/equity-history-batch`,
|
||||||
})
|
{ trader_ids: traderIds }
|
||||||
if (!res.ok) throw new Error('获取批量历史数据失败')
|
)
|
||||||
return res.json()
|
if (!result.success) throw new Error('获取批量历史数据失败')
|
||||||
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取前5名交易员数据(无需认证)
|
// 获取前5名交易员数据(无需认证)
|
||||||
async getTopTraders(): Promise<any[]> {
|
async getTopTraders(): Promise<any[]> {
|
||||||
const res = await httpClient.get(`${API_BASE}/top-traders`)
|
const result = await httpClient.get<any[]>(`${API_BASE}/top-traders`)
|
||||||
if (!res.ok) throw new Error('获取前5名交易员失败')
|
if (!result.success) throw new Error('获取前5名交易员失败')
|
||||||
return res.json()
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取公开交易员配置(无需认证)
|
// 获取公开交易员配置(无需认证)
|
||||||
async getPublicTraderConfig(traderId: string): Promise<any> {
|
async getPublicTraderConfig(traderId: string): Promise<any> {
|
||||||
const res = await httpClient.get(`${API_BASE}/trader/${traderId}/config`)
|
const result = await httpClient.get<any>(
|
||||||
if (!res.ok) throw new Error('获取公开交易员配置失败')
|
`${API_BASE}/trader/${traderId}/config`
|
||||||
return res.json()
|
)
|
||||||
|
if (!result.success) throw new Error('获取公开交易员配置失败')
|
||||||
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取AI学习表现分析(支持trader_id)
|
// 获取AI学习表现分析(支持trader_id)
|
||||||
@@ -328,16 +291,18 @@ export const api = {
|
|||||||
const url = traderId
|
const url = traderId
|
||||||
? `${API_BASE}/performance?trader_id=${traderId}`
|
? `${API_BASE}/performance?trader_id=${traderId}`
|
||||||
: `${API_BASE}/performance`
|
: `${API_BASE}/performance`
|
||||||
const res = await httpClient.get(url, getAuthHeaders())
|
const result = await httpClient.get<any>(url)
|
||||||
if (!res.ok) throw new Error('获取AI学习数据失败')
|
if (!result.success) throw new Error('获取AI学习数据失败')
|
||||||
return res.json()
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取竞赛数据(无需认证)
|
// 获取竞赛数据(无需认证)
|
||||||
async getCompetition(): Promise<CompetitionData> {
|
async getCompetition(): Promise<CompetitionData> {
|
||||||
const res = await httpClient.get(`${API_BASE}/competition`)
|
const result = await httpClient.get<CompetitionData>(
|
||||||
if (!res.ok) throw new Error('获取竞赛数据失败')
|
`${API_BASE}/competition`
|
||||||
return res.json()
|
)
|
||||||
|
if (!result.success) throw new Error('获取竞赛数据失败')
|
||||||
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
// 用户信号源配置接口
|
// 用户信号源配置接口
|
||||||
@@ -345,27 +310,23 @@ export const api = {
|
|||||||
coin_pool_url: string
|
coin_pool_url: string
|
||||||
oi_top_url: string
|
oi_top_url: string
|
||||||
}> {
|
}> {
|
||||||
const res = await httpClient.get(
|
const result = await httpClient.get<{
|
||||||
`${API_BASE}/user/signal-sources`,
|
coin_pool_url: string
|
||||||
getAuthHeaders()
|
oi_top_url: string
|
||||||
)
|
}>(`${API_BASE}/user/signal-sources`)
|
||||||
if (!res.ok) throw new Error('获取用户信号源配置失败')
|
if (!result.success) throw new Error('获取用户信号源配置失败')
|
||||||
return res.json()
|
return result.data!
|
||||||
},
|
},
|
||||||
|
|
||||||
async saveUserSignalSource(
|
async saveUserSignalSource(
|
||||||
coinPoolUrl: string,
|
coinPoolUrl: string,
|
||||||
oiTopUrl: string
|
oiTopUrl: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const res = await httpClient.post(
|
const result = await httpClient.post(`${API_BASE}/user/signal-sources`, {
|
||||||
`${API_BASE}/user/signal-sources`,
|
coin_pool_url: coinPoolUrl,
|
||||||
{
|
oi_top_url: oiTopUrl,
|
||||||
coin_pool_url: coinPoolUrl,
|
})
|
||||||
oi_top_url: oiTopUrl,
|
if (!result.success) throw new Error('保存用户信号源配置失败')
|
||||||
},
|
|
||||||
getAuthHeaders()
|
|
||||||
)
|
|
||||||
if (!res.ok) throw new Error('保存用户信号源配置失败')
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取服务器IP(需要认证,用于白名单配置)
|
// 获取服务器IP(需要认证,用于白名单配置)
|
||||||
@@ -373,8 +334,11 @@ export const api = {
|
|||||||
public_ip: string
|
public_ip: string
|
||||||
message: string
|
message: string
|
||||||
}> {
|
}> {
|
||||||
const res = await httpClient.get(`${API_BASE}/server-ip`, getAuthHeaders())
|
const result = await httpClient.get<{
|
||||||
if (!res.ok) throw new Error('获取服务器IP失败')
|
public_ip: string
|
||||||
return res.json()
|
message: string
|
||||||
|
}>(`${API_BASE}/server-ip`)
|
||||||
|
if (!result.success) throw new Error('获取服务器IP失败')
|
||||||
|
return result.data!
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,47 @@
|
|||||||
/**
|
/**
|
||||||
* HTTP Client with unified error handling and 401 interception
|
* HTTP Client with Axios
|
||||||
*
|
*
|
||||||
* Features:
|
* Features:
|
||||||
* - Unified fetch wrapper
|
* - Axios-based unified request wrapper
|
||||||
|
* - Automatic error interception and toast notifications
|
||||||
|
* - Network errors and system errors are intercepted and shown via toast
|
||||||
|
* - Only business logic errors are returned to the caller
|
||||||
* - Automatic 401 token expiration handling
|
* - Automatic 401 token expiration handling
|
||||||
* - Auth state cleanup on unauthorized
|
|
||||||
* - Automatic redirect to login page
|
|
||||||
* - Notification shown on login page after redirect
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import axios, { AxiosInstance, AxiosError, AxiosResponse } from 'axios'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Business response format - only business errors reach the caller
|
||||||
|
*/
|
||||||
|
export interface ApiResponse<T = any> {
|
||||||
|
success: boolean
|
||||||
|
data?: T
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP Client Class
|
||||||
|
*/
|
||||||
export class HttpClient {
|
export class HttpClient {
|
||||||
// Singleton flag to prevent duplicate 401 handling
|
private axiosInstance: AxiosInstance
|
||||||
private static isHandling401 = false
|
private static isHandling401 = false
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Create axios instance
|
||||||
|
this.axiosInstance = axios.create({
|
||||||
|
baseURL: '/',
|
||||||
|
timeout: 30000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Setup interceptors
|
||||||
|
this.setupInterceptors()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset 401 handling flag (call after successful login)
|
* Reset 401 handling flag (call after successful login)
|
||||||
*/
|
*/
|
||||||
@@ -21,137 +50,212 @@ export class HttpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Response interceptor - handles common HTTP errors
|
* Setup request and response interceptors
|
||||||
*
|
|
||||||
* @param response - Fetch Response object
|
|
||||||
* @returns Response if successful
|
|
||||||
* @throws Error with user-friendly message
|
|
||||||
*/
|
*/
|
||||||
private async handleResponse(response: Response): Promise<Response> {
|
private setupInterceptors(): void {
|
||||||
// Handle 401 Unauthorized - Token expired or invalid
|
// Request interceptor - add auth token
|
||||||
if (response.status === 401) {
|
this.axiosInstance.interceptors.request.use(
|
||||||
// Prevent duplicate 401 handling when multiple API calls fail simultaneously
|
(config) => {
|
||||||
|
const token = localStorage.getItem('auth_token')
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Response interceptor - handle errors
|
||||||
|
this.axiosInstance.interceptors.response.use(
|
||||||
|
(response: AxiosResponse) => {
|
||||||
|
// Success response - pass through
|
||||||
|
return response
|
||||||
|
},
|
||||||
|
(error: AxiosError) => {
|
||||||
|
return this.handleError(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle different types of errors
|
||||||
|
* Network and system errors are intercepted and shown via toast
|
||||||
|
* Only business errors are returned to caller
|
||||||
|
*/
|
||||||
|
private async handleError(error: AxiosError): Promise<any> {
|
||||||
|
// Network error (no response from server)
|
||||||
|
if (!error.response) {
|
||||||
|
toast.error('Network error - Please check your connection', {
|
||||||
|
description: 'Unable to reach the server',
|
||||||
|
})
|
||||||
|
throw new Error('Network error')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { status } = error.response as AxiosResponse<{
|
||||||
|
error?: string
|
||||||
|
message?: string
|
||||||
|
}>
|
||||||
|
|
||||||
|
// Handle 401 Unauthorized
|
||||||
|
if (status === 401) {
|
||||||
if (HttpClient.isHandling401) {
|
if (HttpClient.isHandling401) {
|
||||||
throw new Error('登录已过期,请重新登录')
|
throw new Error('Session expired')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set flag to prevent race conditions
|
|
||||||
HttpClient.isHandling401 = true
|
HttpClient.isHandling401 = true
|
||||||
|
|
||||||
// Clean up local storage
|
// Clean up
|
||||||
localStorage.removeItem('auth_token')
|
localStorage.removeItem('auth_token')
|
||||||
localStorage.removeItem('auth_user')
|
localStorage.removeItem('auth_user')
|
||||||
|
|
||||||
// Notify global listeners (AuthContext will react to this)
|
// Notify global listeners
|
||||||
window.dispatchEvent(new Event('unauthorized'))
|
window.dispatchEvent(new Event('unauthorized'))
|
||||||
|
|
||||||
// Only redirect if not already on login page
|
// Only redirect if not already on login page
|
||||||
if (!window.location.pathname.includes('/login')) {
|
if (!window.location.pathname.includes('/login')) {
|
||||||
// Save current location for post-login redirect
|
|
||||||
const returnUrl = window.location.pathname + window.location.search
|
const returnUrl = window.location.pathname + window.location.search
|
||||||
if (returnUrl !== '/login' && returnUrl !== '/') {
|
if (returnUrl !== '/login' && returnUrl !== '/') {
|
||||||
sessionStorage.setItem('returnUrl', returnUrl)
|
sessionStorage.setItem('returnUrl', returnUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark that user came from 401 (login page will show notification)
|
|
||||||
sessionStorage.setItem('from401', 'true')
|
sessionStorage.setItem('from401', 'true')
|
||||||
|
|
||||||
// Redirect immediately to login page
|
|
||||||
window.location.href = '/login'
|
window.location.href = '/login'
|
||||||
|
|
||||||
// Return pending promise to prevent error from being caught by SWR/React
|
// Return pending promise
|
||||||
// The notification will be shown on the login page
|
return new Promise(() => {})
|
||||||
return new Promise(() => {}) as Promise<Response>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('登录已过期,请重新登录')
|
throw new Error('Session expired')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle other common errors
|
// Handle 403 Forbidden - system error
|
||||||
if (response.status === 403) {
|
if (status === 403) {
|
||||||
throw new Error('没有权限访问此资源')
|
toast.error('Permission Denied', {
|
||||||
|
description: 'You do not have permission to access this resource',
|
||||||
|
})
|
||||||
|
throw new Error('Permission denied')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status === 404) {
|
// Handle 404 Not Found - system error
|
||||||
throw new Error('请求的资源不存在')
|
if (status === 404) {
|
||||||
|
toast.error('API Not Found', {
|
||||||
|
description: 'The requested endpoint does not exist (404)',
|
||||||
|
})
|
||||||
|
throw new Error('API not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status >= 500) {
|
// Handle 500+ Server Error - system error
|
||||||
throw new Error('服务器错误,请稍后重试')
|
if (status >= 500) {
|
||||||
|
toast.error('Server Error', {
|
||||||
|
description: 'Please try again later or contact support',
|
||||||
|
})
|
||||||
|
throw new Error('Server error')
|
||||||
}
|
}
|
||||||
|
|
||||||
return response
|
// 4xx errors (except 401/403/404) are business logic errors
|
||||||
|
// Return them to the caller for handling
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic JSON request with standardized response
|
||||||
|
* System/network errors are already intercepted and shown via toast
|
||||||
|
* Only business errors are returned
|
||||||
|
*/
|
||||||
|
async request<T = any>(
|
||||||
|
url: string,
|
||||||
|
options: {
|
||||||
|
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
|
||||||
|
data?: any
|
||||||
|
params?: any
|
||||||
|
headers?: Record<string, string>
|
||||||
|
} = {}
|
||||||
|
): Promise<ApiResponse<T>> {
|
||||||
|
try {
|
||||||
|
const response = await this.axiosInstance.request<T>({
|
||||||
|
url,
|
||||||
|
method: options.method || 'GET',
|
||||||
|
data: options.data,
|
||||||
|
params: options.params,
|
||||||
|
headers: options.headers,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Success
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: response.data,
|
||||||
|
message: (response.data as any)?.message,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// If we get here, it's a business logic error (4xx except 401/403/404)
|
||||||
|
// System errors were already intercepted and toasted
|
||||||
|
if (axios.isAxiosError(error) && error.response) {
|
||||||
|
const errorData = error.response.data as any
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: errorData?.error || errorData?.message || 'Operation failed',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Network error or other exception (already toasted)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET request
|
* GET request
|
||||||
*/
|
*/
|
||||||
async get(url: string, headers?: Record<string, string>): Promise<Response> {
|
async get<T = any>(
|
||||||
const response = await fetch(url, {
|
url: string,
|
||||||
method: 'GET',
|
params?: any,
|
||||||
headers,
|
headers?: Record<string, string>
|
||||||
})
|
): Promise<ApiResponse<T>> {
|
||||||
return this.handleResponse(response)
|
return this.request<T>(url, { method: 'GET', params, headers })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST request
|
* POST request
|
||||||
*/
|
*/
|
||||||
async post(
|
async post<T = any>(
|
||||||
url: string,
|
url: string,
|
||||||
body?: any,
|
data?: any,
|
||||||
headers?: Record<string, string>
|
headers?: Record<string, string>
|
||||||
): Promise<Response> {
|
): Promise<ApiResponse<T>> {
|
||||||
const response = await fetch(url, {
|
return this.request<T>(url, { method: 'POST', data, headers })
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...headers,
|
|
||||||
},
|
|
||||||
body: body ? JSON.stringify(body) : undefined,
|
|
||||||
})
|
|
||||||
return this.handleResponse(response)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PUT request
|
* PUT request
|
||||||
*/
|
*/
|
||||||
async put(
|
async put<T = any>(
|
||||||
url: string,
|
url: string,
|
||||||
body?: any,
|
data?: any,
|
||||||
headers?: Record<string, string>
|
headers?: Record<string, string>
|
||||||
): Promise<Response> {
|
): Promise<ApiResponse<T>> {
|
||||||
const response = await fetch(url, {
|
return this.request<T>(url, { method: 'PUT', data, headers })
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...headers,
|
|
||||||
},
|
|
||||||
body: body ? JSON.stringify(body) : undefined,
|
|
||||||
})
|
|
||||||
return this.handleResponse(response)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DELETE request
|
* DELETE request
|
||||||
*/
|
*/
|
||||||
async delete(
|
async delete<T = any>(
|
||||||
url: string,
|
url: string,
|
||||||
headers?: Record<string, string>
|
headers?: Record<string, string>
|
||||||
): Promise<Response> {
|
): Promise<ApiResponse<T>> {
|
||||||
const response = await fetch(url, {
|
return this.request<T>(url, { method: 'DELETE', headers })
|
||||||
method: 'DELETE',
|
|
||||||
headers,
|
|
||||||
})
|
|
||||||
return this.handleResponse(response)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic request method for custom configurations
|
* PATCH request
|
||||||
*/
|
*/
|
||||||
async request(url: string, options: RequestInit = {}): Promise<Response> {
|
async patch<T = any>(
|
||||||
const response = await fetch(url, options)
|
url: string,
|
||||||
return this.handleResponse(response)
|
data?: any,
|
||||||
|
headers?: Record<string, string>
|
||||||
|
): Promise<ApiResponse<T>> {
|
||||||
|
return this.request<T>(url, { method: 'PATCH', data, headers })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user