feat: 🎸 对接了企业岗位的部分信息
This commit is contained in:
@@ -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",
|
||||
|
||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
147
src/components/Toast/README.md
Normal file
147
src/components/Toast/README.md
Normal 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. 支持暗色主题自动切换
|
||||
178
src/components/Toast/index.css
Normal file
178
src/components/Toast/index.css
Normal 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;
|
||||
}
|
||||
}
|
||||
259
src/components/Toast/index.jsx
Normal file
259
src/components/Toast/index.jsx
Normal 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;
|
||||
@@ -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}
|
||||
</span>
|
||||
<span className="job-info-modal-content-position-info-num">
|
||||
该岗位仅剩9人
|
||||
该岗位仅剩{data?.remainingPositions}人
|
||||
</span>
|
||||
<span className="job-info-modal-content-position-info-salary">
|
||||
{data?.salary}
|
||||
@@ -105,15 +106,19 @@ export default ({ visible, onClose, data }) => {
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{data?.details?.description && (
|
||||
{data?.description && (
|
||||
<div className="job-info-modal-content-position-info-description">
|
||||
<p className="description-title">岗位描述</p>
|
||||
<p className="description-content">
|
||||
{data?.details?.description}
|
||||
</p>
|
||||
<p className="description-content">{data?.description}</p>
|
||||
</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">
|
||||
<p className="requirements-title">岗位要求</p>
|
||||
<ul className="requirements-content">
|
||||
@@ -124,12 +129,12 @@ export default ({ visible, onClose, data }) => {
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{data?.details?.companyInfo && (
|
||||
)} */}
|
||||
{data?.company?.industry && (
|
||||
<div className="job-info-modal-content-position-info-companyInfo">
|
||||
<p className="companyInfo-title">公司介绍</p>
|
||||
<p className="companyInfo-content">
|
||||
{data?.details?.companyInfo}
|
||||
{data?.company?.industry}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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();
|
||||
const res = await getJobsDetail(item.id);
|
||||
if (res.success) {
|
||||
setJobInfoData(mapJob(res.data));
|
||||
setJobInfoModalVisible(true);
|
||||
setJobInfoData(item);
|
||||
} else {
|
||||
toast.error(res.message);
|
||||
}
|
||||
};
|
||||
|
||||
const onClickJobInfoModalClose = () => {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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, // 获取简历详情
|
||||
|
||||
@@ -8,7 +8,7 @@ export const mapStudent = (backendData) => {
|
||||
id: backendData.id,
|
||||
name: backendData.realName, // realName -> name
|
||||
studentId: backendData.studentNo, // studentNo -> studentId
|
||||
gender: backendData.gender === 'MALE' ? '男' : '女',
|
||||
gender: backendData.gender === "MALE" ? "男" : "女",
|
||||
school: backendData.school,
|
||||
major: backendData.major,
|
||||
enrollDate: backendData.enrollDate,
|
||||
@@ -45,12 +45,15 @@ export const mapCourse = (backendData) => {
|
||||
credits: backendData.credits,
|
||||
hours: backendData.hours,
|
||||
isAiCourse: backendData.isAiCourse,
|
||||
teacher: backendData.teacher ? {
|
||||
teacher: backendData.teacher
|
||||
? {
|
||||
id: backendData.teacher.id,
|
||||
name: backendData.teacher.realName,
|
||||
} : null,
|
||||
}
|
||||
: null,
|
||||
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;
|
||||
|
||||
// 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);
|
||||
@@ -74,14 +77,15 @@ export const mapJob = (backendData) => {
|
||||
|
||||
return {
|
||||
id: backendData.id,
|
||||
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,10 +123,10 @@ 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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -187,7 +191,7 @@ export const mapInterview = (backendData) => {
|
||||
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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -267,10 +271,12 @@ export const mapClass = (backendData) => {
|
||||
startDate: backendData.startDate,
|
||||
endDate: backendData.endDate,
|
||||
isActive: backendData.isActive,
|
||||
teacher: backendData.teacher ? {
|
||||
teacher: backendData.teacher
|
||||
? {
|
||||
id: backendData.teacher.id,
|
||||
name: backendData.teacher.realName,
|
||||
} : null,
|
||||
}
|
||||
: null,
|
||||
studentCount: backendData._count?.students || 0,
|
||||
students: backendData.students ? mapStudentList(backendData.students) : [],
|
||||
};
|
||||
@@ -317,28 +323,30 @@ export const mapProfile = (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"],
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": "/src",
|
||||
"@/services": "/src/services",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user