完整的教务系统前端项目 - 包含所有修复和9月份数据

This commit is contained in:
KQL
2025-09-03 13:26:13 +08:00
commit 87b06d3176
270 changed files with 116169 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
// 贝塞尔曲线路径生成工具
export const createLinePathForCurve = (
fx, // 起始点 x 坐标
fy, // 起始点 y 坐标
tx, // 终点 x 坐标
ty, // 终点 y 坐标
toLeft = false // 是否向左连接
) => {
const __buff_x = tx - fx;
const __buff_y = ty - fy;
const startDirection_x = toLeft ? -100 : 100;
const startDirection_y = 0;
const endDirection_x = toLeft ? 100 : -100;
const endDirection_y = 0;
const forceX = Math.min(200, Math.max(100, Math.abs(__buff_x / 2)));
const forceY = Math.min(200, Math.max(100, Math.abs(__buff_y / 2)));
const startForceX = startDirection_x / (Math.abs(startDirection_x) + Math.abs(startDirection_y)) * forceX;
const startForceY = startDirection_y / (Math.abs(startDirection_x) + Math.abs(startDirection_y)) * forceY;
const ctrl1 = {x: startForceX, y: startForceY};
const endForceX = endDirection_x / (Math.abs(endDirection_x) + Math.abs(endDirection_y)) * forceX + __buff_x;
const endForceY = endDirection_y / (Math.abs(endDirection_x) + Math.abs(endDirection_y)) * forceY + __buff_y;
const ctrl2 = {x: endForceX, y: endForceY};
const path = `M ${Math.round(fx)},${Math.round(fy)} c ${Math.round(ctrl1.x)},${Math.round(ctrl1.y)} ${Math.round(ctrl2.x)},${Math.round(ctrl2.y)} ${Math.round(__buff_x)},${Math.round(__buff_y)}`;
return path;
};

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

@@ -0,0 +1,376 @@
// 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,
company: backendData.company,
position: backendData.title, // title -> position
description: backendData.description,
requirements: backendData.requirements,
responsibilities: backendData.responsibilities,
companyName: 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) : {
// Provide default job object when no job relation exists
salary: "面议",
tags: [],
company: { name: backendData.company || "" }
},
company: backendData.company || backendData.job?.company?.name || "",
position: backendData.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,
};

84
src/utils/request.js Normal file
View File

@@ -0,0 +1,84 @@
// 引入axios
import axios from "axios";
import store from "@/store/index";
import {
showGlobalLoading,
hideGlobalLoading,
} from "@/store/slices/loadingSlice";
const baseURL = import.meta.env.VITE_API_BASE_URL || "http://localhost:3000";
// 全局加载状态loading基于redux
const handleGlobalLoading = (namespace, type) => {
if (!namespace) return;
store.dispatch(
type === "show"
? showGlobalLoading({ namespace })
: hideGlobalLoading({ namespace })
);
};
// 创建axios实例
const axiosInstance = axios.create({
baseURL, // 基础URL
timeout: 10000, // 请求超时时间
headers: {
"Content-Type": "application/json;charset=utf-8",
},
});
// 请求拦截器
axiosInstance.interceptors.request.use(
(config) => {
// 开发阶段使用固定的 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) => {
return Promise.reject(error);
}
);
// 响应拦截器
axiosInstance.interceptors.response.use(
(response) => {
// 处理响应数据
const res = response.data;
return res;
},
(error) => {
// 处理响应错误
console.error("请求错误:", error);
const message =
error.response?.data?.message || error.message || "网络错误";
return Promise.reject(new Error(message));
}
);
// 导出请求方法
export default function request({
url,
apiUrl,
namespace,
method = "get",
data,
params,
headers = {},
}) {
handleGlobalLoading(namespace, "show");
// 返回Promise对象
return axiosInstance({
method,
url: `${apiUrl ? apiUrl : baseURL}${url}`,
data,
params,
headers,
}).finally(() => {
handleGlobalLoading(namespace, "hide");
});
}