diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000..e9b8348
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,10 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(npm run lint:*)",
+ "Bash(npm run typecheck:*)"
+ ],
+ "deny": [],
+ "ask": []
+ }
+}
\ No newline at end of file
diff --git a/.env.development b/.env.development
new file mode 100644
index 0000000..aa8c980
--- /dev/null
+++ b/.env.development
@@ -0,0 +1,4 @@
+# Development environment variables
+VITE_API_BASE_URL=http://localhost:2025
+VITE_APP_TITLE=教务管理系统
+VITE_APP_ENV=development
\ No newline at end of file
diff --git a/.env.production b/.env.production
new file mode 100644
index 0000000..68fd7d8
--- /dev/null
+++ b/.env.production
@@ -0,0 +1,4 @@
+# Production environment variables
+VITE_API_BASE_URL=/api
+VITE_APP_TITLE=教务管理系统
+VITE_APP_ENV=production
\ No newline at end of file
diff --git a/package.json b/package.json
index 5b53041..a3e68f1 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
},
"dependencies": {
"@arco-design/web-react": "^2.66.4",
+ "axios": "^1.11.0",
"d3": "^7.9.0",
"echarts": "^6.0.0",
"react": "^19.1.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index cf09aae..a4f31a7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -11,6 +11,9 @@ importers:
'@arco-design/web-react':
specifier: ^2.66.4
version: 2.66.4(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ axios:
+ specifier: ^1.11.0
+ version: 1.11.0
d3:
specifier: ^7.9.0
version: 7.9.0
@@ -562,6 +565,9 @@ packages:
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+ asynckit@0.4.0:
+ resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+
autoprefixer@10.4.21:
resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==}
engines: {node: ^10 || ^12 || >=14}
@@ -569,6 +575,9 @@ packages:
peerDependencies:
postcss: ^8.1.0
+ axios@1.11.0:
+ resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==}
+
b-tween@0.3.3:
resolution: {integrity: sha512-oEHegcRpA7fAuc9KC4nktucuZn2aS8htymCPcP3qkEGPqiBH+GfqtqoG2l7LxHngg6O0HFM7hOeOYExl1Oz4ZA==}
@@ -586,6 +595,10 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
+ call-bind-apply-helpers@1.0.2:
+ resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
+ engines: {node: '>= 0.4'}
+
callsites@3.1.0:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
@@ -616,6 +629,10 @@ packages:
color@3.2.1:
resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==}
+ combined-stream@1.0.8:
+ resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+ engines: {node: '>= 0.8'}
+
commander@7.2.0:
resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
engines: {node: '>= 10'}
@@ -785,12 +802,20 @@ packages:
delaunator@5.0.1:
resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==}
+ delayed-stream@1.0.0:
+ resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
+ engines: {node: '>=0.4.0'}
+
detect-node-es@1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
dom-helpers@5.2.1:
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
+ dunder-proto@1.0.1:
+ resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+ engines: {node: '>= 0.4'}
+
echarts@3.8.5:
resolution: {integrity: sha512-E+nnROMfCeiLeoT/fZyX8SE8mKzwkTjyemyoBF543oqjRtjTSKQAVDEihMXy4oC6pJS0tYGdMqCA2ATk8onyRg==}
@@ -800,6 +825,22 @@ packages:
electron-to-chromium@1.5.200:
resolution: {integrity: sha512-rFCxROw7aOe4uPTfIAx+rXv9cEcGx+buAF4npnhtTqCJk5KDFRnh3+KYj7rdVh6lsFt5/aPs+Irj9rZ33WMA7w==}
+ es-define-property@1.0.1:
+ resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
+ engines: {node: '>= 0.4'}
+
+ es-errors@1.3.0:
+ resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+ engines: {node: '>= 0.4'}
+
+ es-object-atoms@1.1.1:
+ resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
+ engines: {node: '>= 0.4'}
+
+ es-set-tostringtag@2.1.0:
+ resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
+ engines: {node: '>= 0.4'}
+
esbuild@0.25.9:
resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==}
engines: {node: '>=18'}
@@ -902,6 +943,19 @@ packages:
resolution: {integrity: sha512-Ik/6OCk9RQQ0T5Xw+hKNLWrjSMtv51dD4GRmJjbD5a58TIEpI5a5iXagKVl3Z5UuyslMCA8Xwnu76jQob62Yhg==}
engines: {node: '>=10'}
+ follow-redirects@1.15.11:
+ resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
+ engines: {node: '>=4.0'}
+ peerDependencies:
+ debug: '*'
+ peerDependenciesMeta:
+ debug:
+ optional: true
+
+ form-data@4.0.4:
+ resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
+ engines: {node: '>= 6'}
+
fraction.js@4.3.7:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
@@ -910,10 +964,21 @@ packages:
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
+ function-bind@1.1.2:
+ resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+
gensync@1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
+ get-intrinsic@1.3.0:
+ resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
+ engines: {node: '>= 0.4'}
+
+ get-proto@1.0.1:
+ resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
+ engines: {node: '>= 0.4'}
+
glob-parent@6.0.2:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'}
@@ -926,10 +991,26 @@ packages:
resolution: {integrity: sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==}
engines: {node: '>=18'}
+ gopd@1.2.0:
+ resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
+ engines: {node: '>= 0.4'}
+
has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
+ has-symbols@1.1.0:
+ resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
+ engines: {node: '>= 0.4'}
+
+ has-tostringtag@1.0.2:
+ resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
+ engines: {node: '>= 0.4'}
+
+ hasown@2.0.2:
+ resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
+ engines: {node: '>= 0.4'}
+
iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
@@ -1014,6 +1095,18 @@ packages:
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+ math-intrinsics@1.1.0:
+ resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+ engines: {node: '>= 0.4'}
+
+ mime-db@1.52.0:
+ resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
+ engines: {node: '>= 0.6'}
+
+ mime-types@2.1.35:
+ resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
+ engines: {node: '>= 0.6'}
+
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
@@ -1090,6 +1183,9 @@ packages:
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
+ proxy-from-env@1.1.0:
+ resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
+
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
@@ -1741,6 +1837,8 @@ snapshots:
argparse@2.0.1: {}
+ asynckit@0.4.0: {}
+
autoprefixer@10.4.21(postcss@8.5.6):
dependencies:
browserslist: 4.25.2
@@ -1751,6 +1849,14 @@ snapshots:
postcss: 8.5.6
postcss-value-parser: 4.2.0
+ axios@1.11.0:
+ dependencies:
+ follow-redirects: 1.15.11
+ form-data: 4.0.4
+ proxy-from-env: 1.1.0
+ transitivePeerDependencies:
+ - debug
+
b-tween@0.3.3: {}
b-validate@1.5.3: {}
@@ -1769,6 +1875,11 @@ snapshots:
node-releases: 2.0.19
update-browserslist-db: 1.1.3(browserslist@4.25.2)
+ call-bind-apply-helpers@1.0.2:
+ dependencies:
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+
callsites@3.1.0: {}
caniuse-lite@1.0.30001734: {}
@@ -1800,6 +1911,10 @@ snapshots:
color-convert: 1.9.3
color-string: 1.9.1
+ combined-stream@1.0.8:
+ dependencies:
+ delayed-stream: 1.0.0
+
commander@7.2.0: {}
compute-scroll-into-view@1.0.20: {}
@@ -1982,6 +2097,8 @@ snapshots:
dependencies:
robust-predicates: 3.0.2
+ delayed-stream@1.0.0: {}
+
detect-node-es@1.1.0: {}
dom-helpers@5.2.1:
@@ -1989,6 +2106,12 @@ snapshots:
'@babel/runtime': 7.28.2
csstype: 3.1.3
+ dunder-proto@1.0.1:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
echarts@3.8.5:
dependencies:
zrender: 3.7.4
@@ -2000,6 +2123,21 @@ snapshots:
electron-to-chromium@1.5.200: {}
+ es-define-property@1.0.1: {}
+
+ es-errors@1.3.0: {}
+
+ es-object-atoms@1.1.1:
+ dependencies:
+ es-errors: 1.3.0
+
+ es-set-tostringtag@2.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ has-tostringtag: 1.0.2
+ hasown: 2.0.2
+
esbuild@0.25.9:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.9
@@ -2138,13 +2276,43 @@ snapshots:
dependencies:
tslib: 2.8.1
+ follow-redirects@1.15.11: {}
+
+ form-data@4.0.4:
+ dependencies:
+ asynckit: 0.4.0
+ combined-stream: 1.0.8
+ es-set-tostringtag: 2.1.0
+ hasown: 2.0.2
+ mime-types: 2.1.35
+
fraction.js@4.3.7: {}
fsevents@2.3.3:
optional: true
+ function-bind@1.1.2: {}
+
gensync@1.0.0-beta.2: {}
+ get-intrinsic@1.3.0:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ function-bind: 1.1.2
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ has-symbols: 1.1.0
+ hasown: 2.0.2
+ math-intrinsics: 1.1.0
+
+ get-proto@1.0.1:
+ dependencies:
+ dunder-proto: 1.0.1
+ es-object-atoms: 1.1.1
+
glob-parent@6.0.2:
dependencies:
is-glob: 4.0.3
@@ -2153,8 +2321,20 @@ snapshots:
globals@16.3.0: {}
+ gopd@1.2.0: {}
+
has-flag@4.0.0: {}
+ has-symbols@1.1.0: {}
+
+ has-tostringtag@1.0.2:
+ dependencies:
+ has-symbols: 1.1.0
+
+ hasown@2.0.2:
+ dependencies:
+ function-bind: 1.1.2
+
iconv-lite@0.6.3:
dependencies:
safer-buffer: 2.1.2
@@ -2221,6 +2401,14 @@ snapshots:
dependencies:
yallist: 3.1.1
+ math-intrinsics@1.1.0: {}
+
+ mime-db@1.52.0: {}
+
+ mime-types@2.1.35:
+ dependencies:
+ mime-db: 1.52.0
+
minimatch@3.1.2:
dependencies:
brace-expansion: 1.1.12
@@ -2289,6 +2477,8 @@ snapshots:
object-assign: 4.1.1
react-is: 16.13.1
+ proxy-from-env@1.1.0: {}
+
punycode@2.3.1: {}
react-clientside-effect@1.2.8(react@19.1.1):
diff --git a/src/constants/statusMap.js b/src/constants/statusMap.js
new file mode 100644
index 0000000..0af42e7
--- /dev/null
+++ b/src/constants/statusMap.js
@@ -0,0 +1,283 @@
+// Status mapping constants for converting between frontend and backend values
+
+// Application status mapping
+export const APPLICATION_STATUS_MAP = {
+ // Backend -> Frontend
+ toFrontend: {
+ 'SCHEDULED': 'applied',
+ 'COMPLETED': 'interview_success',
+ 'CANCELLED': 'interview_failed',
+ 'NO_SHOW': 'not_applied',
+ },
+ // Frontend -> Backend
+ toBackend: {
+ 'not_applied': null,
+ 'applied': 'SCHEDULED',
+ 'interview_success': 'COMPLETED',
+ 'accepted': 'COMPLETED',
+ 'interview_failed': 'CANCELLED',
+ },
+};
+
+// Enrollment status mapping
+export const ENROLLMENT_STATUS_MAP = {
+ // Backend values
+ NOT_STARTED: {
+ text: '未开始',
+ color: '#999999',
+ progress: 0,
+ },
+ IN_PROGRESS: {
+ text: '学习中',
+ color: '#3b82f6',
+ progress: 50,
+ },
+ COMPLETED: {
+ text: '已完成',
+ color: '#10b981',
+ progress: 100,
+ },
+};
+
+// Interview status mapping
+export const INTERVIEW_STATUS_MAP = {
+ // Backend values
+ SCHEDULED: {
+ text: '待面试',
+ color: '#f59e0b',
+ icon: 'clock',
+ },
+ COMPLETED: {
+ text: '已完成',
+ color: '#10b981',
+ icon: 'check',
+ },
+ CANCELLED: {
+ text: '已取消',
+ color: '#ef4444',
+ icon: 'close',
+ },
+ NO_SHOW: {
+ text: '未到场',
+ color: '#6b7280',
+ icon: 'warning',
+ },
+};
+
+// Interview result mapping
+export const INTERVIEW_RESULT_MAP = {
+ PASS: {
+ text: '通过',
+ color: '#10b981',
+ applicationStatus: 'interview_success',
+ },
+ FAIL: {
+ text: '未通过',
+ color: '#ef4444',
+ applicationStatus: 'interview_failed',
+ },
+ PENDING: {
+ text: '待定',
+ color: '#f59e0b',
+ applicationStatus: 'applied',
+ },
+ OFFER: {
+ text: '已发Offer',
+ color: '#10b981',
+ applicationStatus: 'accepted',
+ },
+};
+
+// Job type mapping
+export const JOB_TYPE_MAP = {
+ // Backend -> Frontend display
+ FULLTIME: {
+ text: '全职',
+ value: 'fulltime',
+ color: '#3b82f6',
+ },
+ PARTTIME: {
+ text: '兼职',
+ value: 'parttime',
+ color: '#8b5cf6',
+ },
+ INTERNSHIP: {
+ text: '实习',
+ value: 'internship',
+ color: '#ec4899',
+ },
+ CONTRACT: {
+ text: '合同制',
+ value: 'contract',
+ color: '#f59e0b',
+ },
+ REMOTE: {
+ text: '远程',
+ value: 'remote',
+ color: '#10b981',
+ },
+};
+
+// Job level mapping
+export const JOB_LEVEL_MAP = {
+ JUNIOR: {
+ text: '初级',
+ minExperience: 0,
+ maxExperience: 2,
+ },
+ MID: {
+ text: '中级',
+ minExperience: 2,
+ maxExperience: 5,
+ },
+ SENIOR: {
+ text: '高级',
+ minExperience: 5,
+ maxExperience: 10,
+ },
+ LEAD: {
+ text: '资深',
+ minExperience: 8,
+ maxExperience: 15,
+ },
+ MANAGER: {
+ text: '管理',
+ minExperience: 5,
+ maxExperience: null,
+ },
+};
+
+// Course category mapping
+export const COURSE_CATEGORY_MAP = {
+ GENERAL: {
+ text: '通识课程',
+ color: '#6b7280',
+ icon: 'book',
+ },
+ PROFESSIONAL: {
+ text: '专业课程',
+ color: '#3b82f6',
+ icon: 'desktop',
+ },
+ PRACTICAL: {
+ text: '实践课程',
+ color: '#10b981',
+ icon: 'tool',
+ },
+ COMPREHENSIVE: {
+ text: '综合课程',
+ color: '#8b5cf6',
+ icon: 'layers',
+ },
+};
+
+// Course type mapping
+export const COURSE_TYPE_MAP = {
+ LIVE: {
+ text: '直播课',
+ color: '#ef4444',
+ icon: 'video',
+ },
+ RECORDED: {
+ text: '录播课',
+ color: '#3b82f6',
+ icon: 'play',
+ },
+ HYBRID: {
+ text: '混合式',
+ color: '#8b5cf6',
+ icon: 'mix',
+ },
+ OFFLINE: {
+ text: '线下课',
+ color: '#10b981',
+ icon: 'location',
+ },
+};
+
+// Gender mapping
+export const GENDER_MAP = {
+ MALE: '男',
+ FEMALE: '女',
+};
+
+// Company scale mapping
+export const COMPANY_SCALE_MAP = {
+ SMALL: {
+ text: '50人以下',
+ min: 0,
+ max: 50,
+ },
+ MEDIUM: {
+ text: '50-200人',
+ min: 50,
+ max: 200,
+ },
+ LARGE: {
+ text: '200-1000人',
+ min: 200,
+ max: 1000,
+ },
+ ENTERPRISE: {
+ text: '1000人以上',
+ min: 1000,
+ max: null,
+ },
+};
+
+// Interview type mapping
+export const INTERVIEW_TYPE_MAP = {
+ PHONE: {
+ text: '电话面试',
+ icon: 'phone',
+ },
+ VIDEO: {
+ text: '视频面试',
+ icon: 'video',
+ },
+ ONSITE: {
+ text: '现场面试',
+ icon: 'location',
+ },
+ TECHNICAL: {
+ text: '技术面试',
+ icon: 'code',
+ },
+ HR: {
+ text: 'HR面试',
+ icon: 'user',
+ },
+};
+
+// Helper functions
+export const getStatusText = (status, map) => {
+ return map[status]?.text || status;
+};
+
+export const getStatusColor = (status, map) => {
+ return map[status]?.color || '#6b7280';
+};
+
+export const mapApplicationStatus = (backendStatus, result = null) => {
+ if (result && INTERVIEW_RESULT_MAP[result]) {
+ return INTERVIEW_RESULT_MAP[result].applicationStatus;
+ }
+ return APPLICATION_STATUS_MAP.toFrontend[backendStatus] || 'not_applied';
+};
+
+export default {
+ APPLICATION_STATUS_MAP,
+ ENROLLMENT_STATUS_MAP,
+ INTERVIEW_STATUS_MAP,
+ INTERVIEW_RESULT_MAP,
+ JOB_TYPE_MAP,
+ JOB_LEVEL_MAP,
+ COURSE_CATEGORY_MAP,
+ COURSE_TYPE_MAP,
+ GENDER_MAP,
+ COMPANY_SCALE_MAP,
+ INTERVIEW_TYPE_MAP,
+ getStatusText,
+ getStatusColor,
+ mapApplicationStatus,
+};
\ No newline at end of file
diff --git a/src/pages/CompanyJobsListPage/index.jsx b/src/pages/CompanyJobsListPage/index.jsx
index c59f257..c201f61 100644
--- a/src/pages/CompanyJobsListPage/index.jsx
+++ b/src/pages/CompanyJobsListPage/index.jsx
@@ -1,17 +1,67 @@
-import { useState } from "react";
+import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
-import { mockData } from "@/data/mockData";
+import { jobAPI } from "@/services/api";
+import { mapJobList } from "@/utils/dataMapper";
import JobList from "@/pages/CompanyJobsPage/components/JobList";
import "./index.css";
const CompanyJobsListPage = () => {
- const { companyJobs } = mockData;
+ const [jobs, setJobs] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [page, setPage] = useState(1);
+ const [total, setTotal] = useState(0);
const navigate = useNavigate();
+ useEffect(() => {
+ fetchJobs();
+ }, [page]);
+
+ const fetchJobs = async () => {
+ try {
+ setLoading(true);
+ const response = await jobAPI.getList({
+ page,
+ pageSize: 20,
+ isActive: true
+ });
+
+ const mappedJobs = mapJobList(response.data || response);
+ setJobs(mappedJobs);
+ setTotal(response.total || mappedJobs.length);
+ } catch (error) {
+ console.error("Failed to fetch jobs:", error);
+ setJobs([]);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (loading && jobs.length === 0) {
+ return (
+
+ );
+ }
+
return (
-
+
+ {jobs.length === 0 && !loading && (
+
+ 暂无岗位信息
+
+ )}
);
};
diff --git a/src/pages/CompanyJobsPage/index.jsx b/src/pages/CompanyJobsPage/index.jsx
index 28796ac..96bf696 100644
--- a/src/pages/CompanyJobsPage/index.jsx
+++ b/src/pages/CompanyJobsPage/index.jsx
@@ -1,20 +1,85 @@
-import { useState } from "react";
+import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
-import { mockData } from "@/data/mockData";
+import { jobAPI, interviewAPI, studentAPI } from "@/services/api";
+import { mapJobList, mapInterviewList } from "@/utils/dataMapper";
import JobList from "./components/JobList";
import "./index.css";
const CompanyJobsPage = () => {
- const { companyJobs } = mockData;
+ const [jobs, setJobs] = useState([]);
+ const [interviews, setInterviews] = useState([]);
+ const [loading, setLoading] = useState(true);
const [isExpand, setIsExpand] = useState(false); // 是否展开
const navigate = useNavigate();
+ useEffect(() => {
+ fetchData();
+ }, []);
+
+ const fetchData = async () => {
+ try {
+ setLoading(true);
+
+ // Get current user's student ID from API
+ let studentId = null;
+ try {
+ const currentStudent = await studentAPI.getCurrentStudent();
+ studentId = currentStudent?.id;
+ } catch (err) {
+ console.log('Could not get current student:', err);
+ }
+
+ // Fetch jobs (and interviews if we have a student ID)
+ const jobsData = await jobAPI.getList({
+ page: 1,
+ pageSize: 10,
+ isActive: true
+ });
+
+ let interviewsData = { data: [] };
+ if (studentId) {
+ try {
+ interviewsData = await interviewAPI.getList({
+ studentId: studentId,
+ status: 'SCHEDULED'
+ });
+ } catch (err) {
+ console.log('No interviews found or API error');
+ }
+ }
+
+ // Map data to frontend format
+ const mappedJobs = mapJobList(jobsData.data || jobsData);
+ const mappedInterviews = mapInterviewList(interviewsData.data || interviewsData);
+
+ setJobs(mappedJobs);
+ setInterviews(mappedInterviews);
+ } catch (error) {
+ console.error("Failed to fetch data:", error);
+ // Fallback to empty data
+ setJobs([]);
+ setInterviews([]);
+ } finally {
+ setLoading(false);
+ }
+ };
+
const handleJobWrapperClick = () => {
navigate("/company-jobs-list");
};
+ if (loading) {
+ return (
+
+ );
+ }
+
return (
@@ -22,7 +87,7 @@ const CompanyJobsPage = () => {
企业内推岗位库
@@ -37,15 +102,15 @@ const CompanyJobsPage = () => {
>
内推岗位面试
- {companyJobs?.internalPositions?.map((item) => (
+ {interviews.length > 0 ? interviews.map((item) => (
-
{item.position}
- {item?.tags?.length > 0 ? (
+ {item.job?.tags?.length > 0 ? (
- {item?.tags.map((tag) => (
+ {item.job.tags.map((tag) => (
- {
) : null}
- {item.salary}
+ {item.job?.salary || '面议'}
{item.interviewTime}
@@ -71,7 +136,13 @@ const CompanyJobsPage = () => {
- ))}
+ )) : (
+ -
+
+ 暂无面试安排
+
+
+ )}
diff --git a/src/pages/PersonalProfile/components/ProfileCard/index.jsx b/src/pages/PersonalProfile/components/ProfileCard/index.jsx
index 6a5f6a6..c58a8fb 100644
--- a/src/pages/PersonalProfile/components/ProfileCard/index.jsx
+++ b/src/pages/PersonalProfile/components/ProfileCard/index.jsx
@@ -1,9 +1,60 @@
import { Avatar } from "@arco-design/web-react";
-import { mockData } from "@/data/mockData";
+import { useState, useEffect } from "react";
+import { studentAPI } from "@/services/api";
+import { mapProfile } from "@/utils/dataMapper";
import "./index.css";
const ProfileCard = () => {
- const { profile } = mockData;
+ const [profile, setProfile] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ fetchProfile();
+ }, []);
+
+ const fetchProfile = async () => {
+ try {
+ setLoading(true);
+ // Get current logged-in student information
+ const studentData = await studentAPI.getCurrentStudent();
+
+ if (studentData) {
+ const mappedProfile = mapProfile(studentData);
+ setProfile(mappedProfile);
+ } else {
+ throw new Error("Failed to get current student data");
+ }
+ } catch (error) {
+ console.error("Failed to fetch profile:", error);
+ // Show error message instead of fake data
+ setProfile({
+ name: "数据加载失败",
+ studentId: "请检查后端服务",
+ school: error.message || "后端未运行",
+ major: "请确保数据库已初始化",
+ badges: {
+ credits: 0,
+ classRank: 0,
+ mbti: "-"
+ },
+ courses: []
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
return (
@@ -15,9 +66,9 @@ const ProfileCard = () => {
/>
-
{profile.name}
+
{profile?.name}
- 学号: {profile.studentId}
+ 学号: {profile?.studentId}
@@ -25,7 +76,7 @@ const ProfileCard = () => {
学分
- {profile?.badges?.credits}
+ {profile?.badges?.credits || 0}
@@ -33,13 +84,13 @@ const ProfileCard = () => {
班级排名
- {profile?.badges?.classRank}
+ {profile?.badges?.classRank || '-'}
MBTI
- {profile?.badges?.mbti}
+ {profile?.badges?.mbti || '-'}
@@ -48,30 +99,30 @@ const ProfileCard = () => {
学校
- {profile?.school}
+ {profile?.school || '-'}
专业
- {profile?.major}
+ {profile?.major || '-'}
- 就业管家课程
+ 班级
- {profile?.course}
+ {profile?.className || '-'}
- 垂直方向
+ 学习阶段
- {profile?.course}
+ {profile?.stageName || '-'}
diff --git a/src/services/api.js b/src/services/api.js
new file mode 100644
index 0000000..35c94da
--- /dev/null
+++ b/src/services/api.js
@@ -0,0 +1,211 @@
+import request from '@/utils/request';
+
+// Student API
+export const studentAPI = {
+ // Get current logged-in student
+ getCurrentStudent: () => request.get('/api/students/me'),
+
+ // Get student list
+ getList: (params) => request.get('/api/students', { params }),
+
+ // Get student detail
+ getDetail: (id) => request.get(`/api/students/${id}`),
+
+ // Create student
+ create: (data) => request.post('/api/students', data),
+
+ // Update student
+ update: (id, data) => request.put(`/api/students/${id}`, data),
+
+ // Get student progress
+ getProgress: (id) => request.get(`/api/students/${id}/progress`),
+};
+
+// Course API
+export const courseAPI = {
+ // Get course list
+ getList: (params) => request.get('/api/courses', { params }),
+
+ // Get course detail
+ getDetail: (id) => request.get(`/api/courses/${id}`),
+
+ // Create course
+ create: (data) => request.post('/api/courses', data),
+
+ // Update course
+ update: (id, data) => request.put(`/api/courses/${id}`, data),
+
+ // Enroll student in course
+ enroll: (courseId, studentId) =>
+ request.post(`/api/courses/${courseId}/enroll`, { studentId }),
+
+ // Update enrollment progress
+ updateEnrollment: (courseId, enrollmentId, data) =>
+ request.put(`/api/courses/${courseId}/enrollment/${enrollmentId}`, data),
+
+ // Get course students
+ getStudents: (id) => request.get(`/api/courses/${id}/students`),
+};
+
+// Job API
+export const jobAPI = {
+ // Get job list
+ getList: (params) => request.get('/api/jobs', { params }),
+
+ // Get job detail
+ getDetail: (id) => request.get(`/api/jobs/${id}`),
+
+ // Create job
+ create: (data) => request.post('/api/jobs', data),
+
+ // Update job
+ update: (id, data) => request.put(`/api/jobs/${id}`, data),
+
+ // Get recommended jobs for student
+ getRecommended: (studentId) => request.get(`/api/jobs/recommend/${studentId}`),
+};
+
+// Company API
+export const companyAPI = {
+ // Get company list
+ getList: (params) => request.get('/api/companies', { params }),
+
+ // Get company detail
+ getDetail: (id) => request.get(`/api/companies/${id}`),
+
+ // Create company
+ create: (data) => request.post('/api/companies', data),
+
+ // Update company
+ update: (id, data) => request.put(`/api/companies/${id}`, data),
+
+ // Get company jobs
+ getJobs: (id) => request.get(`/api/companies/${id}/jobs`),
+};
+
+// Resume API
+export const resumeAPI = {
+ // Get resume list
+ getList: (params) => request.get('/api/resumes', { params }),
+
+ // Get resume detail
+ getDetail: (id) => request.get(`/api/resumes/${id}`),
+
+ // Create resume
+ create: (data) => request.post('/api/resumes', data),
+
+ // Update resume
+ update: (id, data) => request.put(`/api/resumes/${id}`, data),
+
+ // Delete resume
+ delete: (id) => request.delete(`/api/resumes/${id}`),
+
+ // Get student's active resume
+ getStudentActive: (studentId) =>
+ request.get(`/api/resumes/student/${studentId}/active`),
+};
+
+// Interview API
+export const interviewAPI = {
+ // Get interview list
+ getList: (params) => request.get('/api/interviews', { params }),
+
+ // Get interview detail
+ getDetail: (id) => request.get(`/api/interviews/${id}`),
+
+ // Schedule interview
+ schedule: (data) => request.post('/api/interviews', data),
+
+ // Update interview
+ update: (id, data) => request.put(`/api/interviews/${id}`, data),
+
+ // Cancel interview
+ cancel: (id, reason) =>
+ request.post(`/api/interviews/${id}/cancel`, { reason }),
+
+ // Submit feedback
+ submitFeedback: (id, data) =>
+ request.post(`/api/interviews/${id}/feedback`, data),
+
+ // Get student interview history
+ getStudentHistory: (studentId) =>
+ request.get(`/api/interviews/student/${studentId}/history`),
+};
+
+// Class API
+export const classAPI = {
+ // Get class list
+ getList: (params) => request.get('/api/classes', { params }),
+
+ // Get class detail
+ getDetail: (id) => request.get(`/api/classes/${id}`),
+
+ // Create class
+ create: (data) => request.post('/api/classes', data),
+
+ // Update class
+ update: (id, data) => request.put(`/api/classes/${id}`, data),
+
+ // Get class students
+ getStudents: (id) => request.get(`/api/classes/${id}/students`),
+
+ // Add student to class
+ addStudent: (classId, studentId) =>
+ request.post(`/api/classes/${classId}/students`, { studentId }),
+
+ // Remove student from class
+ removeStudent: (classId, studentId) =>
+ request.delete(`/api/classes/${classId}/students/${studentId}`),
+
+ // Get class statistics
+ getStats: (id) => request.get(`/api/classes/${id}/stats`),
+};
+
+// Learning Stage API
+export const stageAPI = {
+ // Get all stages
+ getList: () => request.get('/api/stages'),
+
+ // Get stage detail
+ getDetail: (id) => request.get(`/api/stages/${id}`),
+
+ // Create stage
+ create: (data) => request.post('/api/stages', data),
+
+ // Update stage
+ update: (id, data) => request.put(`/api/stages/${id}`, data),
+
+ // Delete stage
+ delete: (id) => request.delete(`/api/stages/${id}`),
+
+ // Get stage courses
+ getCourses: (id) => request.get(`/api/stages/${id}/courses`),
+
+ // Get stage students
+ getStudents: (id) => request.get(`/api/stages/${id}/students`),
+
+ // Advance student to next stage
+ advanceStudent: (stageId, studentId) =>
+ request.post(`/api/stages/${stageId}/advance/${studentId}`),
+};
+
+// Auth API
+export const authAPI = {
+ // Login
+ login: (data) => request.post('/api/auth/login', data),
+
+ // Register
+ register: (data) => request.post('/api/auth/register', data),
+
+ // Logout
+ logout: () => request.post('/api/auth/logout'),
+
+ // Get current user
+ getCurrentUser: () => request.get('/api/auth/me'),
+};
+
+// Health Check
+export const healthAPI = {
+ check: () => request.get('/health'),
+ checkDB: () => request.get('/health/db'),
+};
\ No newline at end of file
diff --git a/src/utils/dataMapper.js b/src/utils/dataMapper.js
new file mode 100644
index 0000000..96e182d
--- /dev/null
+++ b/src/utils/dataMapper.js
@@ -0,0 +1,363 @@
+// Data mapping utilities for converting backend data to frontend format
+
+// Map student data from backend to frontend format
+export const mapStudent = (backendData) => {
+ if (!backendData) return null;
+
+ return {
+ id: backendData.id,
+ name: backendData.realName, // realName -> name
+ studentId: backendData.studentNo, // studentNo -> studentId
+ gender: backendData.gender === 'MALE' ? '男' : '女',
+ school: backendData.school,
+ major: backendData.major,
+ enrollDate: backendData.enrollDate,
+ mbtiType: backendData.mbtiType,
+ className: backendData.class?.name,
+ classId: backendData.classId,
+ stageName: backendData.currentStage?.name,
+ stageId: backendData.currentStageId,
+ // User info
+ email: backendData.user?.email,
+ phone: backendData.user?.phone,
+ username: backendData.user?.username,
+ lastLogin: backendData.user?.lastLogin,
+ };
+};
+
+// Map student list
+export const mapStudentList = (backendList) => {
+ if (!Array.isArray(backendList)) return [];
+ return backendList.map(mapStudent);
+};
+
+// Map course data
+export const mapCourse = (backendData) => {
+ if (!backendData) return null;
+
+ return {
+ id: backendData.id,
+ name: backendData.name,
+ code: backendData.code,
+ description: backendData.description,
+ category: backendData.category,
+ type: backendData.type,
+ credits: backendData.credits,
+ hours: backendData.hours,
+ isAiCourse: backendData.isAiCourse,
+ teacher: backendData.teacher ? {
+ id: backendData.teacher.id,
+ name: backendData.teacher.realName,
+ } : null,
+ stage: backendData.stage,
+ enrollmentCount: backendData.enrollmentCount || backendData._count?.enrollments || 0,
+ };
+};
+
+// Map course list
+export const mapCourseList = (backendList) => {
+ if (!Array.isArray(backendList)) return [];
+ return backendList.map(mapCourse);
+};
+
+// Map job data
+export const mapJob = (backendData) => {
+ if (!backendData) return null;
+
+ // Format salary range
+ let salary = '面议';
+ if (backendData.salaryMin && backendData.salaryMax) {
+ const min = Math.floor(backendData.salaryMin / 1000);
+ const max = Math.floor(backendData.salaryMax / 1000);
+ salary = `${min}K-${max}K`;
+ }
+
+ return {
+ id: backendData.id,
+ position: backendData.title, // title -> position
+ description: backendData.description,
+ requirements: backendData.requirements,
+ responsibilities: backendData.responsibilities,
+ company: backendData.company?.name || '',
+ companyId: backendData.companyId,
+ type: mapJobType(backendData.type),
+ jobType: backendData.type === 'INTERNSHIP' ? 'internship' : 'fulltime',
+ level: backendData.level,
+ location: backendData.location,
+ salary: salary,
+ salaryMin: backendData.salaryMin,
+ salaryMax: backendData.salaryMax,
+ benefits: backendData.benefits || [],
+ skills: backendData.skills || [],
+ isActive: backendData.isActive,
+ status: backendData.isActive ? 'available' : 'closed',
+ remainingPositions: backendData._count?.interviews || 5, // Mock remaining positions
+ applicationStatus: 'not_applied', // Default status
+ tags: generateJobTags(backendData),
+ deadline: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), // 30 days from now
+ };
+};
+
+// Map job list
+export const mapJobList = (backendList) => {
+ if (!Array.isArray(backendList)) return [];
+ return backendList.map(mapJob);
+};
+
+// Map job type
+const mapJobType = (type) => {
+ const typeMap = {
+ 'FULLTIME': '全职',
+ 'PARTTIME': '兼职',
+ 'INTERNSHIP': '实习',
+ 'CONTRACT': '合同制',
+ 'REMOTE': '远程',
+ };
+ return typeMap[type] || type;
+};
+
+// Generate job tags
+const generateJobTags = (job) => {
+ const tags = [];
+ if (job.location) tags.push(job.location.split('市')[0] + '市');
+ if (job.type === 'FULLTIME') tags.push('五险一金');
+ if (job.benefits?.includes('双休')) tags.push('双休');
+ if (job.benefits?.includes('弹性工作')) tags.push('弹性工作');
+ return tags.slice(0, 4); // Max 4 tags
+};
+
+// Map company data
+export const mapCompany = (backendData) => {
+ if (!backendData) return null;
+
+ return {
+ id: backendData.id,
+ name: backendData.name,
+ companyName: backendData.name, // Alias for compatibility
+ description: backendData.description,
+ industry: backendData.industry,
+ scale: mapCompanyScale(backendData.scale),
+ location: backendData.location,
+ website: backendData.website,
+ logo: backendData.logo,
+ contact: backendData.contact,
+ jobCount: backendData._count?.jobs || 0,
+ jobs: backendData.jobs ? mapJobList(backendData.jobs) : [],
+ };
+};
+
+// Map company list
+export const mapCompanyList = (backendList) => {
+ if (!Array.isArray(backendList)) return [];
+ return backendList.map(mapCompany);
+};
+
+// Map company scale
+const mapCompanyScale = (scale) => {
+ const scaleMap = {
+ 'SMALL': '50人以下',
+ 'MEDIUM': '50-200人',
+ 'LARGE': '200-1000人',
+ 'ENTERPRISE': '1000人以上',
+ };
+ return scaleMap[scale] || scale;
+};
+
+// Map resume data
+export const mapResume = (backendData) => {
+ if (!backendData) return null;
+
+ return {
+ id: backendData.id,
+ title: backendData.title,
+ content: backendData.content,
+ isActive: backendData.isActive,
+ version: backendData.version,
+ student: backendData.student ? mapStudent(backendData.student) : null,
+ studentId: backendData.studentId,
+ createdAt: backendData.createdAt,
+ updatedAt: backendData.updatedAt,
+ };
+};
+
+// Map interview data
+export const mapInterview = (backendData) => {
+ if (!backendData) return null;
+
+ return {
+ id: backendData.id,
+ scheduledAt: backendData.scheduledAt,
+ interviewTime: new Date(backendData.scheduledAt).toLocaleString('zh-CN'),
+ type: backendData.type,
+ status: backendData.status,
+ location: backendData.location,
+ notes: backendData.notes,
+ feedback: backendData.feedback,
+ result: backendData.result,
+ student: backendData.student ? mapStudent(backendData.student) : null,
+ job: backendData.job ? mapJob(backendData.job) : null,
+ company: backendData.job?.company?.name || '',
+ position: backendData.job?.title || '',
+ // Map status for frontend
+ statusText: mapInterviewStatus(backendData.status, backendData.result),
+ };
+};
+
+// Map interview list
+export const mapInterviewList = (backendList) => {
+ if (!Array.isArray(backendList)) return [];
+ return backendList.map(mapInterview);
+};
+
+// Map interview status
+const mapInterviewStatus = (status, result) => {
+ if (status === 'COMPLETED') {
+ if (result === 'PASS' || result === 'OFFER') return '面试成功';
+ if (result === 'FAIL') return '面试失败';
+ return '已完成';
+ }
+
+ const statusMap = {
+ 'SCHEDULED': '待面试',
+ 'CANCELLED': '已取消',
+ 'NO_SHOW': '未到场',
+ };
+ return statusMap[status] || status;
+};
+
+// Map enrollment data
+export const mapEnrollment = (backendData) => {
+ if (!backendData) return null;
+
+ return {
+ id: backendData.id,
+ courseId: backendData.courseId,
+ studentId: backendData.studentId,
+ status: backendData.status,
+ progress: backendData.progress || 0,
+ score: backendData.score,
+ enrolledAt: backendData.enrolledAt,
+ completedAt: backendData.completedAt,
+ course: backendData.course ? mapCourse(backendData.course) : null,
+ student: backendData.student ? mapStudent(backendData.student) : null,
+ // Map status for display
+ statusText: mapEnrollmentStatus(backendData.status),
+ };
+};
+
+// Map enrollment status
+const mapEnrollmentStatus = (status) => {
+ const statusMap = {
+ 'NOT_STARTED': '未开始',
+ 'IN_PROGRESS': '学习中',
+ 'COMPLETED': '已完成',
+ };
+ return statusMap[status] || status;
+};
+
+// Map class data
+export const mapClass = (backendData) => {
+ if (!backendData) return null;
+
+ return {
+ id: backendData.id,
+ name: backendData.name,
+ className: backendData.name, // Alias for compatibility
+ description: backendData.description,
+ startDate: backendData.startDate,
+ endDate: backendData.endDate,
+ isActive: backendData.isActive,
+ teacher: backendData.teacher ? {
+ id: backendData.teacher.id,
+ name: backendData.teacher.realName,
+ } : null,
+ studentCount: backendData._count?.students || 0,
+ students: backendData.students ? mapStudentList(backendData.students) : [],
+ };
+};
+
+// Map stage data
+export const mapStage = (backendData) => {
+ if (!backendData) return null;
+
+ return {
+ id: backendData.id,
+ name: backendData.name,
+ description: backendData.description,
+ order: backendData.order,
+ duration: backendData.duration,
+ requirements: backendData.requirements,
+ courseCount: backendData._count?.courses || 0,
+ studentCount: backendData._count?.students || 0,
+ courses: backendData.courses ? mapCourseList(backendData.courses) : [],
+ students: backendData.students ? mapStudentList(backendData.students) : [],
+ };
+};
+
+// Map learning record
+export const mapLearningRecord = (backendData) => {
+ if (!backendData) return null;
+
+ return {
+ id: backendData.id,
+ studentId: backendData.studentId,
+ courseId: backendData.courseId,
+ date: backendData.date,
+ duration: backendData.duration,
+ progress: backendData.progress,
+ content: backendData.content,
+ };
+};
+
+// Map profile data (for personal profile page)
+export const mapProfile = (studentData) => {
+ if (!studentData) return null;
+
+ const mapped = mapStudent(studentData);
+
+ return {
+ ...mapped,
+ avatar: '/api/placeholder/80/80', // Default avatar
+ badges: {
+ credits: 84, // Mock data, should come from backend
+ classRank: 9, // Mock data, should come from backend
+ mbti: studentData.mbtiType || 'ENTP',
+ },
+ courses: studentData.enrollments ?
+ studentData.enrollments.map(e => e.course?.name).filter(Boolean) : [],
+ mbtiReport: studentData.mbtiReport || generateMockMBTIReport(studentData.mbtiType),
+ };
+};
+
+// Generate mock MBTI report (temporary until backend provides)
+const generateMockMBTIReport = (type) => {
+ return {
+ type: type || 'ENTP',
+ title: 'Personality Type',
+ description: 'Your personality type description',
+ characteristics: ['Creative', 'Analytical', 'Strategic'],
+ strengths: ['Problem-solving', 'Leadership', 'Innovation'],
+ recommendations: ['Focus on execution', 'Develop patience', 'Listen more'],
+ careerSuggestions: ['Product Manager', 'Consultant', 'Entrepreneur'],
+ };
+};
+
+// Export all mappers
+export default {
+ mapStudent,
+ mapStudentList,
+ mapCourse,
+ mapCourseList,
+ mapJob,
+ mapJobList,
+ mapCompany,
+ mapCompanyList,
+ mapResume,
+ mapInterview,
+ mapInterviewList,
+ mapEnrollment,
+ mapClass,
+ mapStage,
+ mapLearningRecord,
+ mapProfile,
+};
\ No newline at end of file
diff --git a/src/utils/request.js b/src/utils/request.js
index a01bf32..117dd6d 100644
--- a/src/utils/request.js
+++ b/src/utils/request.js
@@ -3,8 +3,8 @@ import axios from "axios";
// 创建axios实例
const service = axios.create({
- baseURL: "", // 基础URL,根据实际项目配置
- timeout: 5000, // 请求超时时间
+ baseURL: import.meta.env.VITE_API_BASE_URL || "http://localhost:3000", // 基础URL
+ timeout: 10000, // 请求超时时间
headers: {
"Content-Type": "application/json;charset=utf-8",
},
@@ -13,11 +13,15 @@ const service = axios.create({
// 请求拦截器
service.interceptors.request.use(
(config) => {
- // 可以在这里添加token等信息
- const token = localStorage.getItem("token");
- if (token) {
- config.headers["Authorization"] = `Bearer ${token}`;
- }
+ // 开发阶段使用固定的 x-user-id
+ // 这个ID对应种子数据中的开发默认用户
+ config.headers["x-user-id"] = "dev-user-id";
+
+ // 后续对接飞书后使用token
+ // const token = localStorage.getItem("token");
+ // if (token) {
+ // config.headers["Authorization"] = `Bearer ${token}`;
+ // }
return config;
},
(error) => {
@@ -30,12 +34,31 @@ service.interceptors.response.use(
(response) => {
// 处理响应数据
const res = response.data;
+
+ // 后端统一返回格式 {success, data, message}
+ if (res.success !== undefined) {
+ if (res.success) {
+ // 如果有分页信息,保留完整结构
+ if (res.total !== undefined) {
+ return res;
+ }
+ // 否则只返回data
+ return res.data || res;
+ } else {
+ // 处理业务错误
+ console.error("业务错误:", res.message);
+ return Promise.reject(new Error(res.message || "请求失败"));
+ }
+ }
+
+ // 兼容直接返回数据的情况
return res;
},
(error) => {
// 处理响应错误
console.error("请求错误:", error);
- return Promise.reject(error);
+ const message = error.response?.data?.message || error.message || "网络错误";
+ return Promise.reject(new Error(message));
}
);
diff --git a/vite.config.js b/vite.config.js
index 11b3dc8..46e03b5 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -38,12 +38,17 @@ export default defineConfig({
// 开发服务器配置
server: {
host: "0.0.0.0",
- port: 3000,
+ port: 5173, // Frontend port, different from backend
strictPort: true,
// API代理配置
proxy: {
"/api": {
- target: "http://localhost:3000",
+ target: "http://localhost:2025", // Backend server
+ changeOrigin: true,
+ rewrite: (path) => path,
+ },
+ "/health": {
+ target: "http://localhost:2025",
changeOrigin: true,
},
},