diff --git a/package.json b/package.json index 123018d..28a9761 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "deploy:prod": "vercel --prod" }, "dependencies": { - "@arco-design/web-react": "^2.66.4", + "@arco-design/web-react": "^2.66.5", "@reduxjs/toolkit": "^2.8.2", "axios": "^1.11.0", "echarts": "^6.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e2dbc25..3c3c0b9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@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) + specifier: ^2.66.5 + version: 2.66.5(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@reduxjs/toolkit': specifier: ^2.8.2 version: 2.8.2(react-redux@9.2.0(@types/react@19.1.10)(react@19.1.1)(redux@5.0.1))(react@19.1.1) @@ -85,8 +85,8 @@ packages: '@arco-design/color@0.4.0': resolution: {integrity: sha512-s7p9MSwJgHeL8DwcATaXvWT3m2SigKpxx4JA1BGPHL4gfvaQsmQfrLBDpjOJFJuJ2jG2dMt3R3P8Pm9E65q18g==} - '@arco-design/web-react@2.66.4': - resolution: {integrity: sha512-vl7sJBLvbVyJhYRPoQ8kHc8BuXNkJIXca5h9ync2J1TuKglFMLNbQwjIvJLW3ciabqTZ5g1O7H1GQ+lLIEMsWA==} + '@arco-design/web-react@2.66.5': + resolution: {integrity: sha512-ity0kG+B6pmuJ2/Zh3wUtBV78XxWmRtGEwazL8f4KAjoQpMkisgLMXibUpAGfcqph3vycNFq4yHgHujjgwrJMQ==} peerDependencies: react: '>=16' react-dom: '>=16' @@ -1334,7 +1334,7 @@ snapshots: dependencies: color: 3.2.1 - '@arco-design/web-react@2.66.4(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@arco-design/web-react@2.66.5(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: '@arco-design/color': 0.4.0 '@babel/runtime': 7.28.3 diff --git a/src/components/Toast/README.md b/src/components/Toast/README.md new file mode 100644 index 0000000..a3e0bd5 --- /dev/null +++ b/src/components/Toast/README.md @@ -0,0 +1,147 @@ +# Toast 组件使用指南 + +## 概述 + +自定义的全局 Toast 组件,支持命令式调用,无需在组件树中添加 Provider。 + +## 特性 + +- 🚀 轻量级,零依赖 +- 📱 响应式设计 +- 🎨 支持多种类型(success, error, warning, info) +- ⚡ 命令式 API,简单易用 +- 🔧 高度可定制 +- 🌙 支持暗色主题 + +## 快速开始 + +### 1. 基本用法 + +```jsx +import toast from "@/components/Toast"; + +// 成功提示 +toast.success("操作成功!"); + +// 错误提示 +toast.error("操作失败!"); + +// 警告提示 +toast.warning("请注意!"); + +// 信息提示 +toast.info("这是一条信息"); +``` + +### 2. 高级用法 + +```jsx +// 自定义持续时间 +toast.success("3秒后消失", { duration: 3000 }); + +// 永不自动消失 +toast.info("手动关闭", { duration: 0 }); + +// 禁用关闭按钮 +toast.error("无法关闭", { closable: false }); + +// 带标题的提示 +toast.success("操作完成", { + title: "成功", + duration: 5000, +}); + +// 获取 Toast ID,手动关闭 +const toastId = toast.info("loading..."); +setTimeout(() => { + toast.remove(toastId); +}, 2000); +``` + +### 3. 在组件中使用 Hook(可选) + +如果需要在组件内部管理 Toast 状态,可以使用 Provider 方式: + +```jsx +import { ToastProvider, useToast } from "@/components/Toast"; + +function App() { + return ( + + + + ); +} + +function YourComponent() { + const { addToast, removeAllToasts } = useToast(); + + const handleClick = () => { + addToast({ + type: "success", + message: "这是通过 Hook 创建的 Toast", + }); + }; + + return ( +
+ + +
+ ); +} +``` + +## API 参考 + +### 全局方法 + +- `toast.success(message, options?)` - 显示成功提示 +- `toast.error(message, options?)` - 显示错误提示 +- `toast.warning(message, options?)` - 显示警告提示 +- `toast.info(message, options?)` - 显示信息提示 +- `toast.remove(id)` - 移除指定 Toast +- `toast.removeAll()` - 移除所有 Toast + +### Options 参数 + +```typescript +interface ToastOptions { + title?: string; // 标题 + duration?: number; // 持续时间(毫秒),0 表示不自动关闭 + closable?: boolean; // 是否显示关闭按钮,默认 true + type?: "success" | "error" | "warning" | "info"; // Toast 类型 +} +``` + +### Hook API + +- `useToast()` - 返回 Toast 管理方法 + - `addToast(options)` - 添加 Toast + - `removeToast(id)` - 移除指定 Toast + - `removeAllToasts()` - 移除所有 Toast + - `toasts` - 当前 Toast 列表 + +## 样式定制 + +可以通过修改 CSS 变量来定制样式: + +```css +.toast-item { + /* 自定义背景色 */ + background: your-color; + + /* 自定义圆角 */ + border-radius: your-radius; + + /* 自定义阴影 */ + box-shadow: your-shadow; +} +``` + +## 注意事项 + +1. Toast 容器会自动创建和销毁,无需手动管理 +2. 支持同时显示多个 Toast +3. 在移动端会自动适配屏幕宽度 +4. 支持暗色主题自动切换 diff --git a/src/components/Toast/index.css b/src/components/Toast/index.css new file mode 100644 index 0000000..87f2f30 --- /dev/null +++ b/src/components/Toast/index.css @@ -0,0 +1,178 @@ +/* Toast 容器 */ +.toast-container, +.global-toast-container { + position: fixed; + top: 20px; + right: 20px; + z-index: 9999; + pointer-events: none; +} + +.global-toast-container .toast-container { + position: static; +} + +/* Toast 项目 */ +.toast-item { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 16px; + margin-bottom: 12px; + min-width: 320px; + max-width: 480px; + background: #ffffff; + border-radius: 8px; + box-shadow: 0 6px 16px -8px rgba(0, 0, 0, 0.08), + 0 9px 28px 0 rgba(0, 0, 0, 0.05), + 0 3px 6px -4px rgba(0, 0, 0, 0.12); + border-left: 4px solid #d9d9d9; + pointer-events: auto; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); + transform: translateX(100%); + opacity: 0; +} + +/* 显示状态 */ +.toast-item.toast-visible { + transform: translateX(0); + opacity: 1; +} + +/* 移除状态 */ +.toast-item.toast-removing { + transform: translateX(100%); + opacity: 0; +} + +/* Toast 类型样式 */ +.toast-item.toast-success { + border-left-color: #52c41a; +} + +.toast-item.toast-success .toast-icon { + color: #52c41a; +} + +.toast-item.toast-error { + border-left-color: #ff4d4f; +} + +.toast-item.toast-error .toast-icon { + color: #ff4d4f; +} + +.toast-item.toast-warning { + border-left-color: #faad14; +} + +.toast-item.toast-warning .toast-icon { + color: #faad14; +} + +.toast-item.toast-info { + border-left-color: #1890ff; +} + +.toast-item.toast-info .toast-icon { + color: #1890ff; +} + + +/* Toast 内容 */ +.toast-content { + flex: 1; + min-width: 0; +} + +.toast-title { + font-size: 16px; + font-weight: 600; + color: #262626; + margin-bottom: 4px; + line-height: 1.4; +} + +.toast-message { + font-size: 14px; + color: #595959; + line-height: 1.5; + word-wrap: break-word; +} + +/* 关闭按钮 */ +.toast-close { + flex-shrink: 0; + width: 20px; + height: 20px; + border: none; + background: none; + color: #bfbfbf; + font-size: 18px; + line-height: 1; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: all 0.2s; + margin-top: 2px; + padding: 0; +} + +.toast-close:hover { + color: #8c8c8c; + background: #f5f5f5; +} + +/* 悬停效果 */ +.toast-item:hover { + box-shadow: 0 6px 16px -8px rgba(0, 0, 0, 0.12), + 0 9px 28px 0 rgba(0, 0, 0, 0.08), + 0 3px 6px -4px rgba(0, 0, 0, 0.16); +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .toast-container, + .global-toast-container { + top: 10px; + right: 10px; + left: 10px; + } + + .toast-item { + min-width: auto; + max-width: none; + margin-bottom: 8px; + } +} + +/* 暗色主题支持 */ +@media (prefers-color-scheme: dark) { + .toast-item { + background: #1f1f1f; + color: #ffffff; + box-shadow: 0 6px 16px -8px rgba(0, 0, 0, 0.32), + 0 9px 28px 0 rgba(0, 0, 0, 0.24), + 0 3px 6px -4px rgba(0, 0, 0, 0.48); + } + + .toast-title { + color: #ffffff; + } + + .toast-message { + color: #d9d9d9; + } + + .toast-close { + color: #8c8c8c; + } + + .toast-close:hover { + color: #bfbfbf; + background: #262626; + } +} \ No newline at end of file diff --git a/src/components/Toast/index.jsx b/src/components/Toast/index.jsx new file mode 100644 index 0000000..d886112 --- /dev/null +++ b/src/components/Toast/index.jsx @@ -0,0 +1,259 @@ +import React, { useState, useEffect, createContext, useContext } from "react"; +import { createRoot } from "react-dom/client"; +import "./index.css"; + +// Toast 上下文 +const ToastContext = createContext(); + +// Toast 类型 +const TOAST_TYPES = { + SUCCESS: "success", + ERROR: "error", + WARNING: "warning", + INFO: "info", +}; + +// // Toast 图标 +// const TOAST_ICONS = { +// [TOAST_TYPES.SUCCESS]: "✓", +// [TOAST_TYPES.ERROR]: "✕", +// [TOAST_TYPES.WARNING]: "⚠", +// [TOAST_TYPES.INFO]: "ℹ", +// }; + +// 单个 Toast 组件 +const ToastItem = ({ toast, onRemove }) => { + const [isVisible, setIsVisible] = useState(false); + const [isRemoving, setIsRemoving] = useState(false); + + useEffect(() => { + // 显示动画 + const showTimer = setTimeout(() => setIsVisible(true), 10); + + // 自动移除 + const removeTimer = setTimeout(() => { + handleRemove(); + }, toast.duration || 3000); + + return () => { + clearTimeout(showTimer); + clearTimeout(removeTimer); + }; + }, []); + + const handleRemove = () => { + setIsRemoving(true); + setTimeout(() => { + onRemove(toast.id); + }, 300); + }; + + return ( +
+
+ {toast.title &&
{toast.title}
} +
{toast.message}
+
+ {toast.closable !== false && ( + + )} +
+ ); +}; + +// Toast 容器组件 +const ToastContainer = () => { + const { toasts, removeToast } = useContext(ToastContext); + + return ( +
+ {toasts.map((toast) => ( + + ))} +
+ ); +}; + +// Toast Provider 组件 +export const ToastProvider = ({ children }) => { + const [toasts, setToasts] = useState([]); + + const addToast = (toast) => { + const id = Date.now() + Math.random(); + const newToast = { + id, + type: TOAST_TYPES.INFO, + duration: 3000, + closable: true, + ...toast, + }; + + setToasts((prev) => [...prev, newToast]); + return id; + }; + + const removeToast = (id) => { + setToasts((prev) => prev.filter((toast) => toast.id !== id)); + }; + + const removeAllToasts = () => { + setToasts([]); + }; + + const value = { + toasts, + addToast, + removeToast, + removeAllToasts, + }; + + return ( + + {children} + + + ); +}; + +// Hook for using toast +export const useToast = () => { + const context = useContext(ToastContext); + if (!context) { + throw new Error("useToast must be used within a ToastProvider"); + } + return context; +}; + +// 全局 Toast 管理器 +class ToastManager { + constructor() { + this.container = null; + this.root = null; + this.toasts = []; + this.listeners = []; + } + + // 初始化容器 + init() { + if (this.container) return; + + this.container = document.createElement("div"); + this.container.className = "global-toast-container"; + document.body.appendChild(this.container); + + this.root = createRoot(this.container); + this.render(); + } + + // 渲染 Toast 列表 + render() { + if (!this.root) return; + + this.root.render( +
+ {this.toasts.map((toast) => ( + + ))} +
+ ); + } + + // 添加 Toast + add(options) { + this.init(); + + const id = Date.now() + Math.random(); + const toast = { + id, + type: TOAST_TYPES.INFO, + duration: 3000, + closable: true, + ...options, + }; + + this.toasts.push(toast); + this.render(); + + // 自动移除 + if (toast.duration > 0) { + setTimeout(() => { + this.remove(id); + }, toast.duration); + } + + return id; + } + + // 移除 Toast + remove(id) { + this.toasts = this.toasts.filter((toast) => toast.id !== id); + this.render(); + + // 如果没有 Toast 了,清理容器 + if (this.toasts.length === 0) { + setTimeout(() => { + this.cleanup(); + }, 300); + } + } + + // 移除所有 Toast + removeAll() { + this.toasts = []; + this.render(); + this.cleanup(); + } + + // 清理容器 + cleanup() { + if (this.container && this.toasts.length === 0) { + this.root?.unmount(); + document.body.removeChild(this.container); + this.container = null; + this.root = null; + } + } + + // 便捷方法 + success(message, options = {}) { + return this.add({ ...options, message, type: TOAST_TYPES.SUCCESS }); + } + + error(message, options = {}) { + return this.add({ ...options, message, type: TOAST_TYPES.ERROR }); + } + + warning(message, options = {}) { + return this.add({ ...options, message, type: TOAST_TYPES.WARNING }); + } + + info(message, options = {}) { + return this.add({ ...options, message, type: TOAST_TYPES.INFO }); + } +} + +// 创建全局实例 +const toastManager = new ToastManager(); + +// 导出全局 Toast API +export const toast = { + success: (message, options) => toastManager.success(message, options), + error: (message, options) => toastManager.error(message, options), + warning: (message, options) => toastManager.warning(message, options), + info: (message, options) => toastManager.info(message, options), + remove: (id) => toastManager.remove(id), + removeAll: () => toastManager.removeAll(), +}; + +export default toast; diff --git a/src/pages/CompanyJobsPage/components/JobInfoModal/index.jsx b/src/pages/CompanyJobsPage/components/JobInfoModal/index.jsx index 57611b4..8f0b1f4 100644 --- a/src/pages/CompanyJobsPage/components/JobInfoModal/index.jsx +++ b/src/pages/CompanyJobsPage/components/JobInfoModal/index.jsx @@ -10,6 +10,7 @@ const InputSearch = Input.Search; const { userResumes } = mockData; export default ({ visible, onClose, data }) => { + console.log(data); const [resumeModalShow, setResumeModalShow] = useState(false); const [resumeInfoModalShow, setResumeInfoModalShow] = useState(false); @@ -90,7 +91,7 @@ export default ({ visible, onClose, data }) => { {data?.position} - 该岗位仅剩9人 + 该岗位仅剩{data?.remainingPositions}人 {data?.salary} @@ -105,15 +106,19 @@ export default ({ visible, onClose, data }) => { ))} )} - {data?.details?.description && ( + {data?.description && (

岗位描述

-

- {data?.details?.description} -

+

{data?.description}

)} - {data?.details?.requirements?.length > 0 && ( + {data?.requirements && ( +
+

岗位要求

+

{data?.requirements}

+
+ )} + {/* {data?.details?.requirements?.length > 0 && (

岗位要求

- )} - {data?.details?.companyInfo && ( + )} */} + {data?.company?.industry && (

公司介绍

- {data?.details?.companyInfo} + {data?.company?.industry}

)} diff --git a/src/pages/CompanyJobsPage/components/JobList/index.jsx b/src/pages/CompanyJobsPage/components/JobList/index.jsx index 9871b01..3995ee7 100644 --- a/src/pages/CompanyJobsPage/components/JobList/index.jsx +++ b/src/pages/CompanyJobsPage/components/JobList/index.jsx @@ -1,17 +1,23 @@ import { useState } from "react"; -import { useNavigate } from "react-router-dom"; +import toast from "@/components/Toast"; import JobInfoModal from "../JobInfoModal"; +import { getJobsDetail } from "@/services"; +import { mapJob } from "@/utils/dataMapper"; import "./index.css"; export default ({ className = "", data = [], backgroundColor = "#FFFFFF" }) => { - const navigate = useNavigate(); const [jobInfoData, setJobInfoData] = useState(undefined); const [jobInfoModalVisible, setJobInfoModalVisible] = useState(false); - const handleJobClick = (e, item) => { + const handleJobClick = async (e, item) => { e.stopPropagation(); - setJobInfoModalVisible(true); - setJobInfoData(item); + const res = await getJobsDetail(item.id); + if (res.success) { + setJobInfoData(mapJob(res.data)); + setJobInfoModalVisible(true); + } else { + toast.error(res.message); + } }; const onClickJobInfoModalClose = () => { diff --git a/src/pages/Dashboard/index.jsx b/src/pages/Dashboard/index.jsx index 8c6ef95..60bb30c 100644 --- a/src/pages/Dashboard/index.jsx +++ b/src/pages/Dashboard/index.jsx @@ -6,7 +6,7 @@ import StudyStatus from "./components/StudyStatus"; import Rank from "@/components/Rank"; import StageProgress from "@/components/StageProgress"; import TaskList from "./components/TaskList"; -import { getClassRanking, getLearningProgressSummary } from "@/services"; +import { getClassRanking, getStudyRecordsProgress } from "@/services"; import "./index.css"; const Dashboard = () => { @@ -14,7 +14,7 @@ const Dashboard = () => { // 获取整体学习进度 const queryLearningProgressSummary = async () => { - const res = await getLearningProgressSummary({ period: "semester" }); + const res = await getStudyRecordsProgress(); console.log("learningProgressSummary", res); }; diff --git a/src/pages/PersonalProfile/index.jsx b/src/pages/PersonalProfile/index.jsx index 07489fb..3912a77 100644 --- a/src/pages/PersonalProfile/index.jsx +++ b/src/pages/PersonalProfile/index.jsx @@ -5,7 +5,7 @@ import Rank from "@/components/Rank"; import StageProgress from "@/components/StageProgress"; import StudyStudes from "./components/StudyStudes"; import { updateStudentInfo } from "@/store/slices/studentSlice"; -import { getClassRanking, getLearningProgressSummary } from "@/services"; +import { getClassRanking, getStudyRecordsProgress } from "@/services"; import "./index.css"; const PersonalProfile = () => { @@ -13,7 +13,7 @@ const PersonalProfile = () => { const [rankData, setRankData] = useState([]); // 班级排名数据 const queryLearningProgressSummary = async () => { - const res = await getLearningProgressSummary({ period: "semester" }); + const res = await getStudyRecordsProgress(); console.log("learningProgressSummary", res); }; diff --git a/src/services/companyJobs.js b/src/services/companyJobs.js index d4b308a..354f6d0 100644 --- a/src/services/companyJobs.js +++ b/src/services/companyJobs.js @@ -8,6 +8,15 @@ export async function getJobsList(params) { params, }); } + +// 获取企业内推岗位详情 +export async function getJobsDetail(id) { + return request({ + url: `/api/jobs/${id}`, + method: "GET", + }); +} + // 获取企业内推岗位面试 export async function getInterviewsList(params) { return request({ diff --git a/src/services/dashboard.js b/src/services/dashboard.js index aaa4d68..493b50a 100644 --- a/src/services/dashboard.js +++ b/src/services/dashboard.js @@ -1,11 +1,10 @@ import request from "@/utils/request"; -// 获取当前学生的学习进度汇总 -export async function getLearningProgressSummary(queryParams = {}) { +// 获取学生的整体学习进度 +export async function getStudyRecordsProgress() { return request({ - url: `/api/dashboard/learning-summary`, + url: `/api/study-records/progress`, method: "GET", - params: queryParams, namespace: "dashboardLoading", }); } diff --git a/src/services/index.js b/src/services/index.js index 9238453..8394cc8 100644 --- a/src/services/index.js +++ b/src/services/index.js @@ -1,11 +1,11 @@ // 统一的API服务接口 - 基于当前认证用户 import { - getLearningProgressSummary, + getStudyRecordsProgress, getMyTasks, getClassRanking, } from "./dashboard"; import { getProjectsList } from "./projectLibrary"; -import { getJobsList, getInterviewsList } from "./companyJobs"; +import { getJobsList, getJobsDetail, getInterviewsList } from "./companyJobs"; import { getLoginStudentInfo } from "./global"; import { getDashboardStatistics, @@ -19,7 +19,7 @@ export { // 仪表盘相关 getMyTasks, // 获取我的任务 getDashboardStatistics, // 获取当前学生仪表盘统计 - getLearningProgressSummary, // 获取当前学生学习进度汇总 + getStudyRecordsProgress, // 获取学生的整体学习进度 // 排名相关 getClassRanking, // 获取当前学生班级排名 @@ -35,6 +35,7 @@ export { // 求职相关 getJobsList, // 获取岗位列表 + getJobsDetail, // 岗位详情 getInterviewsList, // 获取面试列表 getResumesList, // 获取简历列表 getResumesDetail, // 获取简历详情 diff --git a/src/utils/dataMapper.js b/src/utils/dataMapper.js index 96e182d..2d2881b 100644 --- a/src/utils/dataMapper.js +++ b/src/utils/dataMapper.js @@ -3,12 +3,12 @@ // 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' ? '男' : '女', + name: backendData.realName, // realName -> name + studentId: backendData.studentNo, // studentNo -> studentId + gender: backendData.gender === "MALE" ? "男" : "女", school: backendData.school, major: backendData.major, enrollDate: backendData.enrollDate, @@ -34,7 +34,7 @@ export const mapStudentList = (backendList) => { // Map course data export const mapCourse = (backendData) => { if (!backendData) return null; - + return { id: backendData.id, name: backendData.name, @@ -45,12 +45,15 @@ export const mapCourse = (backendData) => { credits: backendData.credits, hours: backendData.hours, isAiCourse: backendData.isAiCourse, - teacher: backendData.teacher ? { - id: backendData.teacher.id, - name: backendData.teacher.realName, - } : null, + teacher: backendData.teacher + ? { + id: backendData.teacher.id, + name: backendData.teacher.realName, + } + : null, stage: backendData.stage, - enrollmentCount: backendData.enrollmentCount || backendData._count?.enrollments || 0, + enrollmentCount: + backendData.enrollmentCount || backendData._count?.enrollments || 0, }; }; @@ -63,25 +66,26 @@ export const mapCourseList = (backendList) => { // Map job data export const mapJob = (backendData) => { if (!backendData) return null; - + // Format salary range - let salary = '面议'; + 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 + company: backendData.company, + position: backendData.title, // title -> position description: backendData.description, requirements: backendData.requirements, responsibilities: backendData.responsibilities, - company: backendData.company?.name || '', + companyName: backendData.company?.name || "", companyId: backendData.companyId, type: mapJobType(backendData.type), - jobType: backendData.type === 'INTERNSHIP' ? 'internship' : 'fulltime', + jobType: backendData.type === "INTERNSHIP" ? "internship" : "fulltime", level: backendData.level, location: backendData.location, salary: salary, @@ -90,9 +94,9 @@ export const mapJob = (backendData) => { benefits: backendData.benefits || [], skills: backendData.skills || [], isActive: backendData.isActive, - status: backendData.isActive ? 'available' : 'closed', + status: backendData.isActive ? "available" : "closed", remainingPositions: backendData._count?.interviews || 5, // Mock remaining positions - applicationStatus: 'not_applied', // Default status + applicationStatus: "not_applied", // Default status tags: generateJobTags(backendData), deadline: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), // 30 days from now }; @@ -107,11 +111,11 @@ export const mapJobList = (backendList) => { // Map job type const mapJobType = (type) => { const typeMap = { - 'FULLTIME': '全职', - 'PARTTIME': '兼职', - 'INTERNSHIP': '实习', - 'CONTRACT': '合同制', - 'REMOTE': '远程', + FULLTIME: "全职", + PARTTIME: "兼职", + INTERNSHIP: "实习", + CONTRACT: "合同制", + REMOTE: "远程", }; return typeMap[type] || type; }; @@ -119,21 +123,21 @@ const mapJobType = (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('弹性工作'); + 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 + companyName: backendData.name, // Alias for compatibility description: backendData.description, industry: backendData.industry, scale: mapCompanyScale(backendData.scale), @@ -155,10 +159,10 @@ export const mapCompanyList = (backendList) => { // Map company scale const mapCompanyScale = (scale) => { const scaleMap = { - 'SMALL': '50人以下', - 'MEDIUM': '50-200人', - 'LARGE': '200-1000人', - 'ENTERPRISE': '1000人以上', + SMALL: "50人以下", + MEDIUM: "50-200人", + LARGE: "200-1000人", + ENTERPRISE: "1000人以上", }; return scaleMap[scale] || scale; }; @@ -166,7 +170,7 @@ const mapCompanyScale = (scale) => { // Map resume data export const mapResume = (backendData) => { if (!backendData) return null; - + return { id: backendData.id, title: backendData.title, @@ -183,11 +187,11 @@ export const mapResume = (backendData) => { // 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'), + interviewTime: new Date(backendData.scheduledAt).toLocaleString("zh-CN"), type: backendData.type, status: backendData.status, location: backendData.location, @@ -196,8 +200,8 @@ export const mapInterview = (backendData) => { 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 || '', + company: backendData.job?.company?.name || "", + position: backendData.job?.title || "", // Map status for frontend statusText: mapInterviewStatus(backendData.status, backendData.result), }; @@ -211,16 +215,16 @@ export const mapInterviewList = (backendList) => { // Map interview status const mapInterviewStatus = (status, result) => { - if (status === 'COMPLETED') { - if (result === 'PASS' || result === 'OFFER') return '面试成功'; - if (result === 'FAIL') return '面试失败'; - return '已完成'; + if (status === "COMPLETED") { + if (result === "PASS" || result === "OFFER") return "面试成功"; + if (result === "FAIL") return "面试失败"; + return "已完成"; } - + const statusMap = { - 'SCHEDULED': '待面试', - 'CANCELLED': '已取消', - 'NO_SHOW': '未到场', + SCHEDULED: "待面试", + CANCELLED: "已取消", + NO_SHOW: "未到场", }; return statusMap[status] || status; }; @@ -228,7 +232,7 @@ const mapInterviewStatus = (status, result) => { // Map enrollment data export const mapEnrollment = (backendData) => { if (!backendData) return null; - + return { id: backendData.id, courseId: backendData.courseId, @@ -248,9 +252,9 @@ export const mapEnrollment = (backendData) => { // Map enrollment status const mapEnrollmentStatus = (status) => { const statusMap = { - 'NOT_STARTED': '未开始', - 'IN_PROGRESS': '学习中', - 'COMPLETED': '已完成', + NOT_STARTED: "未开始", + IN_PROGRESS: "学习中", + COMPLETED: "已完成", }; return statusMap[status] || status; }; @@ -258,19 +262,21 @@ const mapEnrollmentStatus = (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 + 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, + teacher: backendData.teacher + ? { + id: backendData.teacher.id, + name: backendData.teacher.realName, + } + : null, studentCount: backendData._count?.students || 0, students: backendData.students ? mapStudentList(backendData.students) : [], }; @@ -279,7 +285,7 @@ export const mapClass = (backendData) => { // Map stage data export const mapStage = (backendData) => { if (!backendData) return null; - + return { id: backendData.id, name: backendData.name, @@ -297,7 +303,7 @@ export const mapStage = (backendData) => { // Map learning record export const mapLearningRecord = (backendData) => { if (!backendData) return null; - + return { id: backendData.id, studentId: backendData.studentId, @@ -312,33 +318,35 @@ export const mapLearningRecord = (backendData) => { // 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 + 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', + mbti: studentData.mbtiType || "ENTP", }, - courses: studentData.enrollments ? - studentData.enrollments.map(e => e.course?.name).filter(Boolean) : [], - mbtiReport: studentData.mbtiReport || generateMockMBTIReport(studentData.mbtiType), + 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'], + 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"], }; }; @@ -360,4 +368,4 @@ export default { mapStage, mapLearningRecord, mapProfile, -}; \ No newline at end of file +}; diff --git a/vite.config.js b/vite.config.js index 3516d09..4d093b7 100644 --- a/vite.config.js +++ b/vite.config.js @@ -57,6 +57,7 @@ export default defineConfig({ resolve: { alias: { "@": "/src", + "@/services": "/src/services", }, }, });