配置后端对接

This commit is contained in:
2025-08-19 22:35:01 +08:00
parent bc13f82e41
commit 06f6cec70b
13 changed files with 1303 additions and 37 deletions

283
src/constants/statusMap.js Normal file
View File

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

View File

@@ -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 (
<div className="company-jobs-list-page-wrapper" style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '400px'
}}>
<p>加载中...</p>
</div>
);
}
return (
<ul className="company-jobs-list-page-wrapper">
<JobList data={companyJobs?.companyPositions} />
<JobList data={jobs} />
{jobs.length === 0 && !loading && (
<div style={{
textAlign: 'center',
padding: '40px',
color: '#999'
}}>
暂无岗位信息
</div>
)}
</ul>
);
};

View File

@@ -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 (
<div className="company-jobs-page-wrapper">
<div className="company-jobs-page" style={{ justifyContent: 'center', alignItems: 'center' }}>
<p>加载中...</p>
</div>
</div>
);
}
return (
<div className="company-jobs-page-wrapper">
<div className="company-jobs-page">
@@ -22,7 +87,7 @@ const CompanyJobsPage = () => {
<p className="company-jobs-page-title">企业内推岗位库</p>
<div className="company-jobs-page-left-list-wrapper">
<JobList
data={companyJobs?.companyPositions}
data={jobs}
backgroundColor="#F7F8FA"
/>
</div>
@@ -37,15 +102,15 @@ const CompanyJobsPage = () => {
>
<p className="company-jobs-page-title">内推岗位面试</p>
<ul className="company-jobs-page-interview-list">
{companyJobs?.internalPositions?.map((item) => (
{interviews.length > 0 ? interviews.map((item) => (
<li className="company-jobs-page-interview-item" key={item.id}>
<div className="company-jobs-page-interview-item-info">
<p className="company-jobs-page-interview-item-info-position">
{item.position}
</p>
{item?.tags?.length > 0 ? (
{item.job?.tags?.length > 0 ? (
<ul className="company-jobs-page-interview-item-info-tags">
{item?.tags.map((tag) => (
{item.job.tags.map((tag) => (
<li
className="company-jobs-page-interview-item-info-tag"
key={tag}
@@ -56,14 +121,14 @@ const CompanyJobsPage = () => {
</ul>
) : null}
<span className="company-jobs-page-interview-item-info-salary">
{item.salary}
{item.job?.salary || '面议'}
</span>
</div>
<div className="company-jobs-page-interview-item-btn-wrapper">
<span>{item.interviewTime}</span>
<div
className={`company-jobs-page-interview-item-btn ${
item.status !== "completed" &&
item.status !== "COMPLETED" &&
"company-jobs-page-interview-item-btn-active"
}`}
>
@@ -71,7 +136,13 @@ const CompanyJobsPage = () => {
</div>
</div>
</li>
))}
)) : (
<li className="company-jobs-page-interview-item">
<p style={{ color: '#999', textAlign: 'center', width: '100%' }}>
暂无面试安排
</p>
</li>
)}
</ul>
</div>
</div>

View File

@@ -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 (
<div className="profile-card-wrapper" style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}>
<p>加载中...</p>
</div>
);
}
return (
<div className="profile-card-wrapper">
@@ -15,9 +66,9 @@ const ProfileCard = () => {
/>
</Avatar>
<div className="profile-card-user-name">
<span className="profile-card-user-name-text">{profile.name}</span>
<span className="profile-card-user-name-text">{profile?.name}</span>
<p className="profile-card-user-name-student-id">
学号 {profile.studentId}
学号 {profile?.studentId}
</p>
</div>
</div>
@@ -25,7 +76,7 @@ const ProfileCard = () => {
<li className="profile-card-achievement-info-item">
<span className="profile-card-achievement-info-item-title">学分</span>
<span className="profile-card-achievement-info-item-text">
{profile?.badges?.credits}
{profile?.badges?.credits || 0}
</span>
</li>
<li className="profile-card-achievement-info-item">
@@ -33,13 +84,13 @@ const ProfileCard = () => {
班级排名
</span>
<span className="profile-card-achievement-info-item-text">
{profile?.badges?.classRank}
{profile?.badges?.classRank || '-'}
</span>
</li>
<li className="profile-card-achievement-info-item">
<span className="profile-card-achievement-info-item-title">MBTI</span>
<span className="profile-card-achievement-info-item-text">
{profile?.badges?.mbti}
{profile?.badges?.mbti || '-'}
</span>
</li>
</ul>
@@ -48,30 +99,30 @@ const ProfileCard = () => {
<i className="profile-card-class-info-item-icon icon-school" />
<span className="profile-card-class-info-item-title">学校</span>
<span className="profile-card-class-info-item-text">
{profile?.school}
{profile?.school || '-'}
</span>
</li>
<li className="profile-card-class-info-item">
<i className="profile-card-class-info-item-icon icon-major" />
<span className="profile-card-class-info-item-title">专业</span>
<span className="profile-card-class-info-item-text">
{profile?.major}
{profile?.major || '-'}
</span>
</li>
<li className="profile-card-class-info-item">
<i className="profile-card-class-info-item-icon icon-location" />
<span className="profile-card-class-info-item-title">
就业管家课程
班级
</span>
<span className="profile-card-class-info-item-text">
{profile?.course}
{profile?.className || '-'}
</span>
</li>
<li className="profile-card-class-info-item">
<i className="profile-card-class-info-item-icon icon-course" />
<span className="profile-card-class-info-item-title">垂直方向</span>
<span className="profile-card-class-info-item-title">学习阶段</span>
<span className="profile-card-class-info-item-text">
{profile?.course}
{profile?.stageName || '-'}
</span>
</li>
</ul>

211
src/services/api.js Normal file
View File

@@ -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'),
};

363
src/utils/dataMapper.js Normal file
View File

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

View File

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