解决合并冲突: 整合班级排名功能和学习进度功能

- 保留完整的班级排名显示逻辑(领奖台+列表)
- 整合学习进度查询功能
- 兼容不同的响应数据格式
- 添加加载状态和错误处理
This commit is contained in:
2025-08-25 14:32:11 +08:00
26 changed files with 951 additions and 236 deletions

View File

@@ -15,7 +15,7 @@
"deploy:prod": "vercel --prod" "deploy:prod": "vercel --prod"
}, },
"dependencies": { "dependencies": {
"@arco-design/web-react": "^2.66.4", "@arco-design/web-react": "^2.66.5",
"@reduxjs/toolkit": "^2.8.2", "@reduxjs/toolkit": "^2.8.2",
"axios": "^1.11.0", "axios": "^1.11.0",
"echarts": "^6.0.0", "echarts": "^6.0.0",

10
pnpm-lock.yaml generated
View File

@@ -9,8 +9,8 @@ importers:
.: .:
dependencies: dependencies:
'@arco-design/web-react': '@arco-design/web-react':
specifier: ^2.66.4 specifier: ^2.66.5
version: 2.66.4(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) version: 2.66.5(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@reduxjs/toolkit': '@reduxjs/toolkit':
specifier: ^2.8.2 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) 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': '@arco-design/color@0.4.0':
resolution: {integrity: sha512-s7p9MSwJgHeL8DwcATaXvWT3m2SigKpxx4JA1BGPHL4gfvaQsmQfrLBDpjOJFJuJ2jG2dMt3R3P8Pm9E65q18g==} resolution: {integrity: sha512-s7p9MSwJgHeL8DwcATaXvWT3m2SigKpxx4JA1BGPHL4gfvaQsmQfrLBDpjOJFJuJ2jG2dMt3R3P8Pm9E65q18g==}
'@arco-design/web-react@2.66.4': '@arco-design/web-react@2.66.5':
resolution: {integrity: sha512-vl7sJBLvbVyJhYRPoQ8kHc8BuXNkJIXca5h9ync2J1TuKglFMLNbQwjIvJLW3ciabqTZ5g1O7H1GQ+lLIEMsWA==} resolution: {integrity: sha512-ity0kG+B6pmuJ2/Zh3wUtBV78XxWmRtGEwazL8f4KAjoQpMkisgLMXibUpAGfcqph3vycNFq4yHgHujjgwrJMQ==}
peerDependencies: peerDependencies:
react: '>=16' react: '>=16'
react-dom: '>=16' react-dom: '>=16'
@@ -1334,7 +1334,7 @@ snapshots:
dependencies: dependencies:
color: 3.2.1 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: dependencies:
'@arco-design/color': 0.4.0 '@arco-design/color': 0.4.0
'@babel/runtime': 7.28.3 '@babel/runtime': 7.28.3

View File

@@ -1,7 +1,7 @@
/* 布局相关样式 */ /* 布局相关样式 */
.app-layout { .app-layout {
display: flex; display: flex;
min-height: 100vh; height: 100vh;
width: 100%; width: 100%;
background-color: #f2f3f5; background-color: #f2f3f5;
@@ -13,7 +13,8 @@
/* 主内容区域 */ /* 主内容区域 */
.main-content { .main-content {
flex: 1; flex: 1;
overflow: hidden; height: 100vh;
overflow-y: auto;
transition: margin-left 0.3s ease; transition: margin-left 0.3s ease;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -16,7 +16,9 @@ const Layout = ({ children }) => {
const queryLoginStudentInfo = async () => { const queryLoginStudentInfo = async () => {
const res = await getLoginStudentInfo(); const res = await getLoginStudentInfo();
dispatch(setStudentInfo(res)); if (res.success) {
dispatch(setStudentInfo(res.data));
}
}; };
// 初始化项目统一获取登录用户信息 // 初始化项目统一获取登录用户信息

View File

@@ -34,6 +34,7 @@ const Rank = ({ className, data = null, loading = false }) => {
return ( return (
<div className={`module-class-rank ${className}`}> <div className={`module-class-rank ${className}`}>
<p className="module-class-rank-title">班级排名</p> <p className="module-class-rank-title">班级排名</p>
<ul className="module-class-rank-podium"> <ul className="module-class-rank-podium">
{podiumStudents.map((student, index) => { {podiumStudents.map((student, index) => {
const positions = ["item2", "item1", "item3"]; const positions = ["item2", "item1", "item3"];

View File

@@ -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 (
<ToastProvider>
<YourComponent />
</ToastProvider>
);
}
function YourComponent() {
const { addToast, removeAllToasts } = useToast();
const handleClick = () => {
addToast({
type: "success",
message: "这是通过 Hook 创建的 Toast",
});
};
return (
<div>
<button onClick={handleClick}>显示 Toast</button>
<button onClick={removeAllToasts}>清空所有 Toast</button>
</div>
);
}
```
## 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. 支持暗色主题自动切换

View File

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

View File

@@ -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 (
<div
className={`toast-item toast-${toast.type} ${
isVisible ? "toast-visible" : ""
} ${isRemoving ? "toast-removing" : ""}`}
onClick={toast.closable !== false ? handleRemove : undefined}
>
<div className="toast-content">
{toast.title && <div className="toast-title">{toast.title}</div>}
<div className="toast-message">{toast.message}</div>
</div>
{toast.closable !== false && (
<button className="toast-close" onClick={handleRemove}>
×
</button>
)}
</div>
);
};
// Toast 容器组件
const ToastContainer = () => {
const { toasts, removeToast } = useContext(ToastContext);
return (
<div className="toast-container">
{toasts.map((toast) => (
<ToastItem key={toast.id} toast={toast} onRemove={removeToast} />
))}
</div>
);
};
// 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 (
<ToastContext.Provider value={value}>
{children}
<ToastContainer />
</ToastContext.Provider>
);
};
// 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(
<div className="toast-container">
{this.toasts.map((toast) => (
<ToastItem
key={toast.id}
toast={toast}
onRemove={this.remove.bind(this)}
/>
))}
</div>
);
}
// 添加 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;

View File

@@ -24,9 +24,14 @@
background-color: #fff; background-color: #fff;
} }
} }
.empty-data-wrapper {
width: 100%;
min-height: 555px;
display: flex;
}
.job-info-modal-user-resumes-list { .job-info-modal-user-resumes-list {
width: 100%; width: 100%;
min-height: 555px;
margin-top: 16px; margin-top: 16px;
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); /* 每行两列 */ grid-template-columns: repeat(2, 1fr); /* 每行两列 */

View File

@@ -1,23 +1,48 @@
import { useState } from "react"; import { useState } from "react";
import { useSelector } from "react-redux";
import { Input } from "@arco-design/web-react"; import { Input } from "@arco-design/web-react";
import Modal from "@/components/Modal"; import Modal from "@/components/Modal";
import { mockData } from "@/data/mockData"; import InfiniteScroll from "@/components/InfiniteScroll";
import ResumeInfoModal from "@/pages/CompanyJobsPage/components/ResumeInfoModal"; import ResumeInfoModal from "@/pages/CompanyJobsPage/components/ResumeInfoModal";
import FILEICON from "@/assets/images/CompanyJobsPage/file_icon.png"; import FILEICON from "@/assets/images/CompanyJobsPage/file_icon.png";
import { getResumesList } from "@/services";
import "./index.css"; import "./index.css";
const InputSearch = Input.Search; const InputSearch = Input.Search;
const { userResumes } = mockData; const PAGE_SIZE = 10;
export default ({ visible, onClose, data }) => { export default ({ visible, onClose, data }) => {
const studentInfo = useSelector((state) => state.student.studentInfo);
const [resumeModalShow, setResumeModalShow] = useState(false); const [resumeModalShow, setResumeModalShow] = useState(false);
const [resumeInfoModalShow, setResumeInfoModalShow] = useState(false); const [resumeInfoModalShow, setResumeInfoModalShow] = useState(false);
const [resumeList, setResumeList] = useState([]); // 简历列表
const [listPage, setListPage] = useState(1);
const [listHasMore, setListHasMore] = useState(true);
const handleCloseModal = () => { const handleCloseModal = () => {
setResumeModalShow(false); setResumeModalShow(false);
onClose(); onClose();
}; };
const queryResumeList = async () => {
const res = await getResumesList({
page: listPage,
pageSize: PAGE_SIZE,
studentId: studentInfo?.id,
});
if (res.success) {
setResumeList((prevList) => {
const newList = [...prevList, ...res.data];
if (res.total === newList?.length) {
setListHasMore(false);
} else {
setListPage((prevPage) => prevPage + 1);
}
return newList;
});
}
};
// 点击立即投递 // 点击立即投递
const handleClickDeliverBtn = (e) => { const handleClickDeliverBtn = (e) => {
e.stopPropagation(); e.stopPropagation();
@@ -52,9 +77,18 @@ export default ({ visible, onClose, data }) => {
searchButton searchButton
placeholder="搜索简历" placeholder="搜索简历"
/> />
{
<ul className="job-info-modal-user-resumes-list"> <InfiniteScroll
{userResumes.map((item) => ( loadMore={queryResumeList}
hasMore={listHasMore}
empty={resumeList.length === 0}
className={`${
resumeList.length
? "job-info-modal-user-resumes-list"
: "empty-data-wrapper"
}`}
>
{resumeList.map((item) => (
<li <li
key={item.id} key={item.id}
className="list-item" className="list-item"
@@ -64,7 +98,7 @@ export default ({ visible, onClose, data }) => {
<img src={FILEICON} className="file-icon" /> <img src={FILEICON} className="file-icon" />
<div className="file-info"> <div className="file-info">
<p className="file-info-targetPosition"> <p className="file-info-targetPosition">
{item.targetPosition} {item.title}
</p> </p>
{item?.skills?.length > 0 && ( {item?.skills?.length > 0 && (
<p className="file-info-skills"> <p className="file-info-skills">
@@ -81,7 +115,8 @@ export default ({ visible, onClose, data }) => {
</div> </div>
</li> </li>
))} ))}
</ul> </InfiniteScroll>
}
</> </>
) : ( ) : (
<> <>
@@ -90,7 +125,7 @@ export default ({ visible, onClose, data }) => {
{data?.position} {data?.position}
</span> </span>
<span className="job-info-modal-content-position-info-num"> <span className="job-info-modal-content-position-info-num">
该岗位仅剩9 该岗位仅剩{data?.remainingPositions}
</span> </span>
<span className="job-info-modal-content-position-info-salary"> <span className="job-info-modal-content-position-info-salary">
{data?.salary} {data?.salary}
@@ -105,15 +140,19 @@ export default ({ visible, onClose, data }) => {
))} ))}
</ul> </ul>
)} )}
{data?.details?.description && ( {data?.description && (
<div className="job-info-modal-content-position-info-description"> <div className="job-info-modal-content-position-info-description">
<p className="description-title">岗位描述</p> <p className="description-title">岗位描述</p>
<p className="description-content"> <p className="description-content">{data?.description}</p>
{data?.details?.description}
</p>
</div> </div>
)} )}
{data?.details?.requirements?.length > 0 && ( {data?.requirements && (
<div className="job-info-modal-content-position-info-description">
<p className="description-title">岗位要求</p>
<p className="description-content">{data?.requirements}</p>
</div>
)}
{/* {data?.details?.requirements?.length > 0 && (
<div className="job-info-modal-content-position-info-requirements"> <div className="job-info-modal-content-position-info-requirements">
<p className="requirements-title">岗位要求</p> <p className="requirements-title">岗位要求</p>
<ul className="requirements-content"> <ul className="requirements-content">
@@ -124,12 +163,12 @@ export default ({ visible, onClose, data }) => {
))} ))}
</ul> </ul>
</div> </div>
)} )} */}
{data?.details?.companyInfo && ( {data?.company?.industry && (
<div className="job-info-modal-content-position-info-companyInfo"> <div className="job-info-modal-content-position-info-companyInfo">
<p className="companyInfo-title">公司介绍</p> <p className="companyInfo-title">公司介绍</p>
<p className="companyInfo-content"> <p className="companyInfo-content">
{data?.details?.companyInfo} {data?.company?.industry}
</p> </p>
</div> </div>
)} )}

View File

@@ -1,17 +1,23 @@
import { useState } from "react"; import { useState } from "react";
import { useNavigate } from "react-router-dom"; import toast from "@/components/Toast";
import JobInfoModal from "../JobInfoModal"; import JobInfoModal from "../JobInfoModal";
import { getJobsDetail } from "@/services";
import { mapJob } from "@/utils/dataMapper";
import "./index.css"; import "./index.css";
export default ({ className = "", data = [], backgroundColor = "#FFFFFF" }) => { export default ({ className = "", data = [], backgroundColor = "#FFFFFF" }) => {
const navigate = useNavigate();
const [jobInfoData, setJobInfoData] = useState(undefined); const [jobInfoData, setJobInfoData] = useState(undefined);
const [jobInfoModalVisible, setJobInfoModalVisible] = useState(false); const [jobInfoModalVisible, setJobInfoModalVisible] = useState(false);
const handleJobClick = (e, item) => { const handleJobClick = async (e, item) => {
e.stopPropagation(); e.stopPropagation();
const res = await getJobsDetail(item.id);
if (res.success) {
setJobInfoData(mapJob(res.data));
setJobInfoModalVisible(true); setJobInfoModalVisible(true);
setJobInfoData(item); } else {
toast.error(res.message);
}
}; };
const onClickJobInfoModalClose = () => { const onClickJobInfoModalClose = () => {

View File

@@ -1,6 +1,5 @@
.company-jobs-page-wrapper { .company-jobs-page-wrapper {
width: 100%; width: 100%;
height: 100%;
box-sizing: border-box; box-sizing: border-box;
padding: 20px; padding: 20px;
position: relative; position: relative;

View File

@@ -2,9 +2,10 @@ import { useState } from "react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { mapJobList, mapInterviewList } from "@/utils/dataMapper"; import { mapJobList, mapInterviewList } from "@/utils/dataMapper";
import InfiniteScroll from "@/components/InfiniteScroll";
import toast from "@/components/Toast";
import JobList from "./components/JobList"; import JobList from "./components/JobList";
import { getJobsList, getInterviewsList } from "@/services"; import { getJobsList, getInterviewsList } from "@/services";
import InfiniteScroll from "@/components/InfiniteScroll";
import "./index.css"; import "./index.css";
const PAGE_SIZE = 10; const PAGE_SIZE = 10;
@@ -22,7 +23,6 @@ const CompanyJobsPage = () => {
// 获取面试信息 // 获取面试信息
const fetchInterviewsData = async () => { const fetchInterviewsData = async () => {
try {
if (studentInfo?.id) { if (studentInfo?.id) {
const res = await getInterviewsList({ const res = await getInterviewsList({
page: interviewsPage, page: interviewsPage,
@@ -41,11 +41,10 @@ const CompanyJobsPage = () => {
} }
return newList; return newList;
}); });
} } else {
}
} catch (error) {
console.error("Failed to fetch data:", error);
setInterviews([]); setInterviews([]);
toast.error(res.message);
}
} }
}; };

View File

@@ -1,14 +1,23 @@
import "./index.css"; import { useNavigate } from "react-router-dom";
import icon1 from "@/assets/images/Dashboard/QuickAccess/icon1.png"; import icon1 from "@/assets/images/Dashboard/QuickAccess/icon1.png";
import icon2 from "@/assets/images/Dashboard/QuickAccess/icon2.png"; import icon2 from "@/assets/images/Dashboard/QuickAccess/icon2.png";
import icon3 from "@/assets/images/Dashboard/QuickAccess/icon3.png"; import icon3 from "@/assets/images/Dashboard/QuickAccess/icon3.png";
import "./index.css";
const QuickAccess = () => { const QuickAccess = () => {
const navigate = useNavigate();
const handleClick = (path) => {
navigate(path);
};
return ( return (
<div className="module-quick-access-wrapper"> <div className="module-quick-access-wrapper">
<p className="module-quick-access-title">快捷入口</p> <p className="module-quick-access-title">快捷入口</p>
<ul className="module-quick-access-list"> <ul className="module-quick-access-list">
<li className="module-quick-access-item"> <li
className="module-quick-access-item"
onClick={() => handleClick("/expert-support")}
>
<img <img
src={icon1} src={icon1}
alt="icon1" alt="icon1"
@@ -16,7 +25,10 @@ const QuickAccess = () => {
/> />
<p className="module-quick-access-item-text">专家支持中心</p> <p className="module-quick-access-item-text">专家支持中心</p>
</li> </li>
<li className="module-quick-access-item"> <li
className="module-quick-access-item"
onClick={() => handleClick("/live")}
>
<img <img
src={icon2} src={icon2}
alt="icon1" alt="icon1"
@@ -24,7 +36,10 @@ const QuickAccess = () => {
/> />
<p className="module-quick-access-item-text">课程直播间</p> <p className="module-quick-access-item-text">课程直播间</p>
</li> </li>
<li className="module-quick-access-item"> <li
className="module-quick-access-item"
onClick={() => handleClick("/career-tree")}
>
<img <img
src={icon3} src={icon3}
alt="icon1" alt="icon1"

View File

@@ -6,7 +6,7 @@ import StudyStatus from "./components/StudyStatus";
import Rank from "@/components/Rank"; import Rank from "@/components/Rank";
import StageProgress from "@/components/StageProgress"; import StageProgress from "@/components/StageProgress";
import TaskList from "./components/TaskList"; import TaskList from "./components/TaskList";
import { getClassRanking } from "@/services/dashboard"; import { getClassRanking, getStudyRecordsProgress } from "@/services";
import "./index.css"; import "./index.css";
const Dashboard = () => { const Dashboard = () => {
@@ -15,13 +15,18 @@ const Dashboard = () => {
useEffect(() => { useEffect(() => {
fetchRankingData(); fetchRankingData();
fetchLearningProgressSummary();
}, []); }, []);
// 获取班级排名数据
const fetchRankingData = async () => { const fetchRankingData = async () => {
try { try {
setRankingLoading(true); setRankingLoading(true);
const response = await getClassRanking(); const response = await getClassRanking();
if (response) { if (response && response.success) {
setRankingData(response.data);
} else if (response) {
// 兼容直接返回数据的情况
setRankingData(response); setRankingData(response);
} }
} catch (error) { } catch (error) {
@@ -31,6 +36,16 @@ const Dashboard = () => {
} }
}; };
// 获取整体学习进度
const fetchLearningProgressSummary = async () => {
try {
const res = await getStudyRecordsProgress();
console.log("learningProgressSummary", res);
} catch (error) {
console.error('Failed to fetch learning progress:', error);
}
};
return ( return (
<div className="dashboard"> <div className="dashboard">
<StageProgress showBlockageAlert={true} /> <StageProgress showBlockageAlert={true} />

View File

@@ -27,7 +27,7 @@ const ProfileCard = () => {
<li className="profile-card-achievement-info-item"> <li className="profile-card-achievement-info-item">
<span className="profile-card-achievement-info-item-title">学分</span> <span className="profile-card-achievement-info-item-title">学分</span>
<span className="profile-card-achievement-info-item-text"> <span className="profile-card-achievement-info-item-text">
{studentInfo?.totalCredits || 0} {studentInfo?.myRank?.score || "-"}
</span> </span>
</li> </li>
<li className="profile-card-achievement-info-item"> <li className="profile-card-achievement-info-item">
@@ -35,7 +35,7 @@ const ProfileCard = () => {
班级排名 班级排名
</span> </span>
<span className="profile-card-achievement-info-item-text"> <span className="profile-card-achievement-info-item-text">
{studentInfo?.badges?.classRank || "-"} {studentInfo?.myRank?.rank || "-"}
</span> </span>
</li> </li>
<li className="profile-card-achievement-info-item"> <li className="profile-card-achievement-info-item">

View File

@@ -1,11 +1,11 @@
import * as echarts from "echarts"; import * as echarts from "echarts";
import { useState, useEffect } from "react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import StudyProgress from "../StudyProgress"; import StudyProgress from "../StudyProgress";
import ScoreRingChart from "../ScoreRingChart"; import ScoreRingChart from "../ScoreRingChart";
import AttendanceRingChart from "../AttendanceRingChart"; import AttendanceRingChart from "../AttendanceRingChart";
import { getLoginStudentProgress } from "@/services"; import { getDashboardStatistics } from "@/services";
import "./index.css"; import "./index.css";
import { useEffect } from "react";
const ringData = [ const ringData = [
{ {
@@ -54,17 +54,14 @@ const attendanceData = [
const StudyStudes = () => { const StudyStudes = () => {
const studentInfo = useSelector((state) => state.student.studentInfo); const studentInfo = useSelector((state) => state.student.studentInfo);
const [progressData, setProgressData] = useState({}); // 获取仪表盘数据
const queryDashboardStatistics = async () => {
const queryLoginStudentProgress = async () => { const res = await getDashboardStatistics();
const res = await getLoginStudentProgress(); console.log(res);
if (res.success) {
setProgressData(res.data);
}
}; };
useEffect(() => { useEffect(() => {
queryLoginStudentProgress(); queryDashboardStatistics();
}, []); }, []);
return ( return (

View File

@@ -1,10 +1,40 @@
import { useState, useEffect } from "react";
import { useDispatch } from "react-redux";
import ProfileCard from "./components/ProfileCard"; import ProfileCard from "./components/ProfileCard";
import Rank from "@/components/Rank"; import Rank from "@/components/Rank";
import StageProgress from "@/components/StageProgress"; import StageProgress from "@/components/StageProgress";
import StudyStudes from "./components/StudyStudes"; import StudyStudes from "./components/StudyStudes";
import { updateStudentInfo } from "@/store/slices/studentSlice";
import { getClassRanking, getStudyRecordsProgress } from "@/services";
import "./index.css"; import "./index.css";
const PersonalProfile = () => { const PersonalProfile = () => {
const dispatch = useDispatch();
const [rankData, setRankData] = useState([]); // 班级排名数据
const queryLearningProgressSummary = async () => {
const res = await getStudyRecordsProgress();
console.log("learningProgressSummary", res);
};
// 获取班级排名
const queryRankData = async () => {
const res = await getClassRanking();
if (res.success) {
const studentInfo = {
myRank: res.data.myRank,
classInfo: res.data.classInfo,
};
setRankData(res.data);
dispatch(updateStudentInfo(studentInfo));
}
};
useEffect(() => {
queryRankData();
queryLearningProgressSummary();
}, []);
return ( return (
<div className="personal-profile"> <div className="personal-profile">
<StageProgress /> <StageProgress />
@@ -13,7 +43,7 @@ const PersonalProfile = () => {
<div className="unified-profile-layout"> <div className="unified-profile-layout">
<div className="unified-profile-left"> <div className="unified-profile-left">
<ProfileCard /> <ProfileCard />
<Rank className="unified-profile-rank" /> <Rank className="unified-profile-rank" data={rankData} />
</div> </div>
<div className="unified-profile-right"> <div className="unified-profile-right">
<StudyStudes /> <StudyStudes />

View File

@@ -8,6 +8,15 @@ export async function getJobsList(params) {
params, params,
}); });
} }
// 获取企业内推岗位详情
export async function getJobsDetail(id) {
return request({
url: `/api/jobs/${id}`,
method: "GET",
});
}
// 获取企业内推岗位面试 // 获取企业内推岗位面试
export async function getInterviewsList(params) { export async function getInterviewsList(params) {
return request({ return request({
@@ -16,3 +25,19 @@ export async function getInterviewsList(params) {
params, params,
}); });
} }
// 获取简历列表
export async function getResumesList(params) {
return request({
url: `/api/resumes`,
method: "GET",
params: params,
});
}
// 获取简历详情
export async function getResumesDetail(id) {
return request({
url: `/api/resumes/${id}`,
method: "GET",
});
}

View File

@@ -1,20 +1,20 @@
import request from "@/utils/request"; import request from "@/utils/request";
// 获取当前学生的仪表板统计信息 // 获取学生的整体学习进度
export async function getDashboardStatistics() { export async function getStudyRecordsProgress() {
return request({ return request({
url: `/api/dashboard/stats`, url: `/api/study-records/progress`,
method: "GET", method: "GET",
namespace: "dashboardLoading", namespace: "dashboardLoading",
}); });
} }
// 获取当前学生的学习进度汇总 // 获取我的任务
export async function getLearningProgressSummary(params = {}) { export async function getMyTasks(params = {}) {
return request({ return request({
url: `/api/dashboard/learning-summary`, url: `/api/tasks/my-tasks`,
method: "GET", method: "GET",
params, params: params,
namespace: "dashboardLoading", namespace: "dashboardLoading",
}); });
} }
@@ -24,7 +24,7 @@ export async function getClassRanking(params = { limit: 6 }) {
return request({ return request({
url: `/api/rankings/class`, url: `/api/rankings/class`,
method: "GET", method: "GET",
params, params: params,
namespace: "dashboardLoading", namespace: "dashboardLoading",
}); });
} }

View File

@@ -1,19 +1,31 @@
// 统一的API服务接口 - 基于当前认证用户 // 统一的API服务接口 - 基于当前认证用户
import { import {
getDashboardStatistics, getStudyRecordsProgress,
getLearningProgressSummary, getMyTasks,
getClassRanking, getClassRanking,
} from "./dashboard"; } from "./dashboard";
import { getProjectsList } from "./projectLibrary"; import { getProjectsList } from "./projectLibrary";
import { getJobsList, getInterviewsList } from "./companyJobs"; import {
getJobsList,
getJobsDetail,
getInterviewsList,
getResumesList,
getResumesDetail,
} from "./companyJobs";
import { getLoginStudentInfo } from "./global"; import { getLoginStudentInfo } from "./global";
import { getLoginStudentProgress, getClassRank, getMyRanking } from "./personalProfile"; import {
import { getResumesList, getResumesDetail } from "./resumeInterview"; getDashboardStatistics,
getLoginStudentProgress,
getClassRank,
getMyRanking,
} from "./personalProfile";
import {} from "./resumeInterview";
export { export {
// 仪表盘相关 // 仪表盘相关
getMyTasks, // 获取我的任务
getDashboardStatistics, // 获取当前学生仪表盘统计 getDashboardStatistics, // 获取当前学生仪表盘统计
getLearningProgressSummary, // 获取当前学生学习进度汇总 getStudyRecordsProgress, // 获取学生的整体学习进度
// 排名相关 // 排名相关
getClassRanking, // 获取当前学生班级排名 getClassRanking, // 获取当前学生班级排名
@@ -29,6 +41,7 @@ export {
// 求职相关 // 求职相关
getJobsList, // 获取岗位列表 getJobsList, // 获取岗位列表
getJobsDetail, // 岗位详情
getInterviewsList, // 获取面试列表 getInterviewsList, // 获取面试列表
getResumesList, // 获取简历列表 getResumesList, // 获取简历列表
getResumesDetail, // 获取简历详情 getResumesDetail, // 获取简历详情

View File

@@ -9,12 +9,21 @@ export async function getLoginStudentProgress() {
}); });
} }
// 获取仪表板统计信息
export async function getDashboardStatistics() {
return request({
url: `/api/dashboard/stats`,
method: "GET",
namespace: "dashboardLoading",
});
}
// 获取当前学生班级排名 // 获取当前学生班级排名
export async function getClassRank(queryParams = {}) { export async function getClassRank(params = {}) {
return request({ return request({
url: `/api/rankings/class`, url: `/api/rankings/class`,
method: "GET", method: "GET",
params: queryParams, params: params,
namespace: "profileLoading", namespace: "profileLoading",
}); });
} }

View File

@@ -1,17 +1 @@
import request from "@/utils/request"; import request from "@/utils/request";
// 获取简历列表
export async function getResumesList(params) {
return request({
url: `/api/resumes/`,
method: "GET",
params,
});
}
// 获取简历详情
export async function getResumesDetail(id) {
return request({
url: `/api/resumes/${id}`,
method: "GET",
});
}

View File

@@ -8,7 +8,7 @@ export const mapStudent = (backendData) => {
id: backendData.id, id: backendData.id,
name: backendData.realName, // realName -> name name: backendData.realName, // realName -> name
studentId: backendData.studentNo, // studentNo -> studentId studentId: backendData.studentNo, // studentNo -> studentId
gender: backendData.gender === 'MALE' ? '男' : '女', gender: backendData.gender === "MALE" ? "男" : "女",
school: backendData.school, school: backendData.school,
major: backendData.major, major: backendData.major,
enrollDate: backendData.enrollDate, enrollDate: backendData.enrollDate,
@@ -45,12 +45,15 @@ export const mapCourse = (backendData) => {
credits: backendData.credits, credits: backendData.credits,
hours: backendData.hours, hours: backendData.hours,
isAiCourse: backendData.isAiCourse, isAiCourse: backendData.isAiCourse,
teacher: backendData.teacher ? { teacher: backendData.teacher
? {
id: backendData.teacher.id, id: backendData.teacher.id,
name: backendData.teacher.realName, name: backendData.teacher.realName,
} : null, }
: null,
stage: backendData.stage, stage: backendData.stage,
enrollmentCount: backendData.enrollmentCount || backendData._count?.enrollments || 0, enrollmentCount:
backendData.enrollmentCount || backendData._count?.enrollments || 0,
}; };
}; };
@@ -65,7 +68,7 @@ export const mapJob = (backendData) => {
if (!backendData) return null; if (!backendData) return null;
// Format salary range // Format salary range
let salary = '面议'; let salary = "面议";
if (backendData.salaryMin && backendData.salaryMax) { if (backendData.salaryMin && backendData.salaryMax) {
const min = Math.floor(backendData.salaryMin / 1000); const min = Math.floor(backendData.salaryMin / 1000);
const max = Math.floor(backendData.salaryMax / 1000); const max = Math.floor(backendData.salaryMax / 1000);
@@ -74,14 +77,15 @@ export const mapJob = (backendData) => {
return { return {
id: backendData.id, id: backendData.id,
company: backendData.company,
position: backendData.title, // title -> position position: backendData.title, // title -> position
description: backendData.description, description: backendData.description,
requirements: backendData.requirements, requirements: backendData.requirements,
responsibilities: backendData.responsibilities, responsibilities: backendData.responsibilities,
company: backendData.company?.name || '', companyName: backendData.company?.name || "",
companyId: backendData.companyId, companyId: backendData.companyId,
type: mapJobType(backendData.type), type: mapJobType(backendData.type),
jobType: backendData.type === 'INTERNSHIP' ? 'internship' : 'fulltime', jobType: backendData.type === "INTERNSHIP" ? "internship" : "fulltime",
level: backendData.level, level: backendData.level,
location: backendData.location, location: backendData.location,
salary: salary, salary: salary,
@@ -90,9 +94,9 @@ export const mapJob = (backendData) => {
benefits: backendData.benefits || [], benefits: backendData.benefits || [],
skills: backendData.skills || [], skills: backendData.skills || [],
isActive: backendData.isActive, isActive: backendData.isActive,
status: backendData.isActive ? 'available' : 'closed', status: backendData.isActive ? "available" : "closed",
remainingPositions: backendData._count?.interviews || 5, // Mock remaining positions remainingPositions: backendData._count?.interviews || 5, // Mock remaining positions
applicationStatus: 'not_applied', // Default status applicationStatus: "not_applied", // Default status
tags: generateJobTags(backendData), tags: generateJobTags(backendData),
deadline: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), // 30 days from now 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 // Map job type
const mapJobType = (type) => { const mapJobType = (type) => {
const typeMap = { const typeMap = {
'FULLTIME': '全职', FULLTIME: "全职",
'PARTTIME': '兼职', PARTTIME: "兼职",
'INTERNSHIP': '实习', INTERNSHIP: "实习",
'CONTRACT': '合同制', CONTRACT: "合同制",
'REMOTE': '远程', REMOTE: "远程",
}; };
return typeMap[type] || type; return typeMap[type] || type;
}; };
@@ -119,10 +123,10 @@ const mapJobType = (type) => {
// Generate job tags // Generate job tags
const generateJobTags = (job) => { const generateJobTags = (job) => {
const tags = []; const tags = [];
if (job.location) tags.push(job.location.split('市')[0] + '市'); if (job.location) tags.push(job.location.split("市")[0] + "市");
if (job.type === 'FULLTIME') tags.push('五险一金'); if (job.type === "FULLTIME") tags.push("五险一金");
if (job.benefits?.includes('双休')) tags.push('双休'); if (job.benefits?.includes("双休")) tags.push("双休");
if (job.benefits?.includes('弹性工作')) tags.push('弹性工作'); if (job.benefits?.includes("弹性工作")) tags.push("弹性工作");
return tags.slice(0, 4); // Max 4 tags return tags.slice(0, 4); // Max 4 tags
}; };
@@ -155,10 +159,10 @@ export const mapCompanyList = (backendList) => {
// Map company scale // Map company scale
const mapCompanyScale = (scale) => { const mapCompanyScale = (scale) => {
const scaleMap = { const scaleMap = {
'SMALL': '50人以下', SMALL: "50人以下",
'MEDIUM': '50-200人', MEDIUM: "50-200人",
'LARGE': '200-1000人', LARGE: "200-1000人",
'ENTERPRISE': '1000人以上', ENTERPRISE: "1000人以上",
}; };
return scaleMap[scale] || scale; return scaleMap[scale] || scale;
}; };
@@ -187,7 +191,7 @@ export const mapInterview = (backendData) => {
return { return {
id: backendData.id, id: backendData.id,
scheduledAt: backendData.scheduledAt, scheduledAt: backendData.scheduledAt,
interviewTime: new Date(backendData.scheduledAt).toLocaleString('zh-CN'), interviewTime: new Date(backendData.scheduledAt).toLocaleString("zh-CN"),
type: backendData.type, type: backendData.type,
status: backendData.status, status: backendData.status,
location: backendData.location, location: backendData.location,
@@ -196,8 +200,8 @@ export const mapInterview = (backendData) => {
result: backendData.result, result: backendData.result,
student: backendData.student ? mapStudent(backendData.student) : null, student: backendData.student ? mapStudent(backendData.student) : null,
job: backendData.job ? mapJob(backendData.job) : null, job: backendData.job ? mapJob(backendData.job) : null,
company: backendData.job?.company?.name || '', company: backendData.job?.company?.name || "",
position: backendData.job?.title || '', position: backendData.job?.title || "",
// Map status for frontend // Map status for frontend
statusText: mapInterviewStatus(backendData.status, backendData.result), statusText: mapInterviewStatus(backendData.status, backendData.result),
}; };
@@ -211,16 +215,16 @@ export const mapInterviewList = (backendList) => {
// Map interview status // Map interview status
const mapInterviewStatus = (status, result) => { const mapInterviewStatus = (status, result) => {
if (status === 'COMPLETED') { if (status === "COMPLETED") {
if (result === 'PASS' || result === 'OFFER') return '面试成功'; if (result === "PASS" || result === "OFFER") return "面试成功";
if (result === 'FAIL') return '面试失败'; if (result === "FAIL") return "面试失败";
return '已完成'; return "已完成";
} }
const statusMap = { const statusMap = {
'SCHEDULED': '待面试', SCHEDULED: "待面试",
'CANCELLED': '已取消', CANCELLED: "已取消",
'NO_SHOW': '未到场', NO_SHOW: "未到场",
}; };
return statusMap[status] || status; return statusMap[status] || status;
}; };
@@ -248,9 +252,9 @@ export const mapEnrollment = (backendData) => {
// Map enrollment status // Map enrollment status
const mapEnrollmentStatus = (status) => { const mapEnrollmentStatus = (status) => {
const statusMap = { const statusMap = {
'NOT_STARTED': '未开始', NOT_STARTED: "未开始",
'IN_PROGRESS': '学习中', IN_PROGRESS: "学习中",
'COMPLETED': '已完成', COMPLETED: "已完成",
}; };
return statusMap[status] || status; return statusMap[status] || status;
}; };
@@ -267,10 +271,12 @@ export const mapClass = (backendData) => {
startDate: backendData.startDate, startDate: backendData.startDate,
endDate: backendData.endDate, endDate: backendData.endDate,
isActive: backendData.isActive, isActive: backendData.isActive,
teacher: backendData.teacher ? { teacher: backendData.teacher
? {
id: backendData.teacher.id, id: backendData.teacher.id,
name: backendData.teacher.realName, name: backendData.teacher.realName,
} : null, }
: null,
studentCount: backendData._count?.students || 0, studentCount: backendData._count?.students || 0,
students: backendData.students ? mapStudentList(backendData.students) : [], students: backendData.students ? mapStudentList(backendData.students) : [],
}; };
@@ -317,28 +323,30 @@ export const mapProfile = (studentData) => {
return { return {
...mapped, ...mapped,
avatar: '/api/placeholder/80/80', // Default avatar avatar: "/api/placeholder/80/80", // Default avatar
badges: { badges: {
credits: 84, // Mock data, should come from backend credits: 84, // Mock data, should come from backend
classRank: 9, // 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 ? courses: studentData.enrollments
studentData.enrollments.map(e => e.course?.name).filter(Boolean) : [], ? studentData.enrollments.map((e) => e.course?.name).filter(Boolean)
mbtiReport: studentData.mbtiReport || generateMockMBTIReport(studentData.mbtiType), : [],
mbtiReport:
studentData.mbtiReport || generateMockMBTIReport(studentData.mbtiType),
}; };
}; };
// Generate mock MBTI report (temporary until backend provides) // Generate mock MBTI report (temporary until backend provides)
const generateMockMBTIReport = (type) => { const generateMockMBTIReport = (type) => {
return { return {
type: type || 'ENTP', type: type || "ENTP",
title: 'Personality Type', title: "Personality Type",
description: 'Your personality type description', description: "Your personality type description",
characteristics: ['Creative', 'Analytical', 'Strategic'], characteristics: ["Creative", "Analytical", "Strategic"],
strengths: ['Problem-solving', 'Leadership', 'Innovation'], strengths: ["Problem-solving", "Leadership", "Innovation"],
recommendations: ['Focus on execution', 'Develop patience', 'Listen more'], recommendations: ["Focus on execution", "Develop patience", "Listen more"],
careerSuggestions: ['Product Manager', 'Consultant', 'Entrepreneur'], careerSuggestions: ["Product Manager", "Consultant", "Entrepreneur"],
}; };
}; };

View File

@@ -49,24 +49,6 @@ axiosInstance.interceptors.response.use(
(response) => { (response) => {
// 处理响应数据 // 处理响应数据
const res = response.data; 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; return res;
}, },
(error) => { (error) => {

View File

@@ -57,6 +57,7 @@ export default defineConfig({
resolve: { resolve: {
alias: { alias: {
"@": "/src", "@": "/src",
"@/services": "/src/services",
}, },
}, },
}); });