chore: 迁移项目到新仓库并整理代码

- 更新多个组件的功能优化
- 整理简历映射数据
- 优化视频播放和面试模拟相关组件
- 更新就业策略和公司职位页面

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
KQL
2025-10-24 18:42:25 +08:00
parent 1b964b3886
commit 63f8cf2e7d
43 changed files with 3937 additions and 792 deletions

View File

@@ -4,7 +4,7 @@ const AgentPage = () => {
return (
<div className="agent-page-wrapper">
<iframe
src="http://192.168.2.33:4173"
src="http://127.0.0.1:4173"
className="agent-page-iframe"
title="Agent"
frameBorder="0"

View File

@@ -1,44 +1,15 @@
import { useState, useCallback } from "react";
import { Empty } from "@arco-design/web-react";
import { useNavigate } from "react-router-dom";
import JobList from "@/pages/CompanyJobsPage/components/JobList";
import InfiniteScroll from "@/components/InfiniteScroll";
import { getJobsList } from "@/services";
import "./index.css";
const PAGE_SIZE = 20;
const CompanyJobsListPage = () => {
const navigate = useNavigate();
const [jobs, setJobs] = useState([]);
const [listPage, setListPage] = useState(1);
const [listHasMore, setListHasMore] = useState(true);
// 返回到企业内推岗位页面
const handleBack = () => {
navigate("/company-jobs");
};
const fetchJobs = useCallback(async () => {
const res = await getJobsList({
page: listPage,
pageSize: PAGE_SIZE,
isActive: true,
});
if (res.success) {
// Mock数据已经是前端格式不需要映射
setJobs((prevList) => {
const newList = [...prevList, ...res.data];
if (res.total === newList?.length) {
setListHasMore(false);
} else {
setListPage((prevPage) => prevPage + 1);
}
return newList;
});
}
}, [listPage]);
return (
<div className="company-jobs-list-page">
{/* 返回按钮 */}
@@ -49,14 +20,16 @@ const CompanyJobsListPage = () => {
</button>
</div>
<InfiniteScroll
loadMore={fetchJobs}
hasMore={listHasMore}
empty={jobs.length === 0}
className="company-jobs-list-page-wrapper"
>
<JobList data={jobs} className="jobs-list-margin" />
</InfiniteScroll>
<div className="company-jobs-list-page-wrapper">
<Empty
description="暂无岗位数据"
style={{
paddingTop: '200px',
fontSize: '16px',
color: '#86909c'
}}
/>
</div>
</div>
);
};

View File

@@ -0,0 +1,64 @@
import { useState, useCallback } from "react";
import { useNavigate } from "react-router-dom";
import JobList from "@/pages/CompanyJobsPage/components/JobList";
import InfiniteScroll from "@/components/InfiniteScroll";
import { getJobsList } from "@/services";
import "./index.css";
const PAGE_SIZE = 20;
const CompanyJobsListPage = () => {
const navigate = useNavigate();
const [jobs, setJobs] = useState([]);
const [listPage, setListPage] = useState(1);
const [listHasMore, setListHasMore] = useState(true);
// 返回到企业内推岗位页面
const handleBack = () => {
navigate("/company-jobs");
};
const fetchJobs = useCallback(async () => {
const res = await getJobsList({
page: listPage,
pageSize: PAGE_SIZE,
isActive: true,
});
if (res.success) {
// Mock数据已经是前端格式不需要映射
setJobs((prevList) => {
const newList = [...prevList, ...res.data];
if (res.total === newList?.length) {
setListHasMore(false);
} else {
setListPage((prevPage) => prevPage + 1);
}
return newList;
});
}
}, [listPage]);
return (
<div className="company-jobs-list-page">
{/* 返回按钮 */}
<div className="back-button-wrapper">
<button className="back-button" onClick={handleBack}>
<span className="back-icon">←</span>
<span className="back-text">返回</span>
</button>
</div>
<InfiniteScroll
loadMore={fetchJobs}
hasMore={listHasMore}
empty={jobs.length === 0}
className="company-jobs-list-page-wrapper"
>
<JobList data={jobs} className="jobs-list-margin" />
</InfiniteScroll>
</div>
);
};
export default CompanyJobsListPage;

View File

@@ -448,4 +448,14 @@
}
}
}
.company-jobs-page-empty-state {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: #ffffff;
border-radius: 8px;
}
}

View File

@@ -1,375 +1,79 @@
import { useState, useEffect } from "react";
import { useSelector } from "react-redux";
import { Empty } from "@arco-design/web-react";
import { useNavigate } from "react-router-dom";
import { Spin, Empty } from "@arco-design/web-react";
// import { mapJobList, mapInterviewList } from "@/utils/dataMapper"; // 不再需要映射器mock数据已经是前端格式
import InfiniteScroll from "@/components/InfiniteScroll";
import toast from "@/components/Toast";
import JobList from "./components/JobList";
import InterviewStatusAnimation from "./components/InterviewStatusAnimation";
import JobInfoModal from "./components/JobInfoModal";
import {
getCompanyJobsPageData,
getJobsList,
getInterviewsList,
getJobsDetail,
} from "@/services";
import { getJobByPosition } from "@/services/companyJobsNew";
import "./index.css";
const PAGE_SIZE = 10;
const CompanyJobsPage = () => {
const studentInfo = useSelector((state) => state.student.studentInfo);
const [jobs, setJobs] = useState([]);
const [jobsListPage, setJobsListPage] = useState(1);
const [jobsListHasMore, setJobsListHasMore] = useState(true);
const [interviews, setInterviews] = useState([]);
const [interviewsPage, setInterviewsPage] = useState(1);
const [interviewsHasMore, setInterviewsHasMore] = useState(true);
const [initialDataLoaded, setInitialDataLoaded] = useState(false);
const [loading, setLoading] = useState(true);
const [expandedItemId, setExpandedItemId] = useState(null);
const [jobDetailVisible, setJobDetailVisible] = useState(false);
const [selectedJob, setSelectedJob] = useState(null);
const [isFromInterview, setIsFromInterview] = useState(false); // 标识是否从面试状态卡片点击
const navigate = useNavigate();
// 初始化页面数据 - 使用聚合接口
useEffect(() => {
const fetchInitialData = async () => {
try {
setLoading(true);
const res = await getCompanyJobsPageData({
studentId: studentInfo?.id,
});
if (res?.success) {
// 设置面试数据
let interviewsData = [];
if (res.data?.interviews) {
// Mock数据已经是前端格式直接使用不需要映射
interviewsData = res.data.interviews.list || [];
setInterviews(interviewsData);
setInterviewsHasMore(res.data.interviews.hasMore);
if (interviewsData.length > 0) {
setInterviewsPage(2); // 下次从第2页开始
}
}
// 设置岗位数据 - 包含已投递的岗位
if (res.data?.jobs) {
// Mock数据已经是前端格式直接使用不需要映射
const jobsList = res.data.jobs.list || [];
// 从面试数据中提取已投递的岗位信息
const deliveredJobs = interviewsData.map(interview => {
// 确保有完整的岗位数据
const jobData = interview.job || {};
return {
id: `delivered-${interview.id}`, // 使用特殊的ID标识已投递岗位
position: interview.position,
isDelivered: true, // 标记为已投递
interviewTime: interview.interviewTime,
interviewStatus: interview.statusText,
originalInterviewId: interview.id,
// 从job对象中提取所有必要字段
salary: jobData.salary || "面议",
tags: jobData.tags || [],
location: jobData.location || "待定",
education: jobData.education || "待定",
jobCategory: jobData.jobCategory || "专业相关岗位",
remainingPositions: jobData.remainingPositions || 5,
deadline: jobData.deadline || "2025-12-31",
jobType: jobData.jobType || "job",
requirements: jobData.requirements || "",
description: jobData.description || "",
welfare: jobData.welfare || [],
companyInfo: jobData.companyInfo || "",
companyImages: jobData.companyImages || []
};
}).filter(job => job.position); // 过滤掉没有岗位信息的项
// 分离未投递和已过期的岗位
const activeJobs = jobsList.filter(job => !job.isExpired && job.status !== 'expired');
const expiredJobs = jobsList.filter(job => job.isExpired || job.status === 'expired');
// 按照顺序合并:未投递 -> 已投递 -> 已过期
const allJobs = [...activeJobs, ...deliveredJobs, ...expiredJobs];
setJobs(allJobs);
setJobsListHasMore(res.data.jobs.hasMore);
if (allJobs.length > 0) {
setJobsListPage(2); // 下次从第2页开始
}
}
setInitialDataLoaded(true);
}
} catch (error) {
console.error("Failed to fetch initial page data:", error);
// 如果聚合接口失败,回退到原来的方式
setInitialDataLoaded(true);
// 显示错误信息给用户
if (toast && toast.error) {
toast.error("加载数据失败,请刷新重试");
}
} finally {
setLoading(false);
}
};
fetchInitialData();
}, [studentInfo?.id]);
// 获取面试信息 - 用于分页加载更多
const fetchInterviewsData = async () => {
// 如果初始数据还没加载完成,或者是第一页且已有初始数据,则跳过
if (!initialDataLoaded || (interviewsPage === 1 && interviews.length > 0)) {
return;
}
if (studentInfo?.id) {
const res = await getInterviewsList({
page: interviewsPage,
pageSize: PAGE_SIZE,
studentId: studentInfo?.id,
status: "SCHEDULED",
});
if (res.success) {
// Mock数据已经是前端格式直接使用
const interviews = res.data || [];
setInterviews((prevList) => {
// 去重处理:过滤掉已存在的数据
const existingIds = new Set(
prevList.map((interview) => interview.id)
);
const newInterviews = interviews.filter(
(interview) => !existingIds.has(interview.id)
);
const newList = [...prevList, ...newInterviews];
if (res.total <= newList?.length) {
setInterviewsHasMore(false);
} else {
setInterviewsPage((prevPage) => prevPage + 1);
}
return newList;
});
} else {
if (interviewsPage === 1) {
setInterviews([]);
}
toast.error(res.message);
}
}
};
// 处理面试状态点击
const handleStatusClick = (e, item) => {
e.stopPropagation();
// 如果点击的是已展开的项,则收起;否则展开新项
if (expandedItemId === item.id) {
setExpandedItemId(null);
} else {
setExpandedItemId(item.id);
}
};
// 获取企业内推岗位 - 用于分页加载更多
const fetchJobsList = async () => {
// 如果初始数据还没加载完成,或者是第一页且已有初始数据,则跳过
if (!initialDataLoaded || (jobsListPage === 1 && jobs.length > 0)) {
return;
}
// 防止重复请求
if (jobsListPage === 1 && jobs.length === 0) {
return; // 初始数据应该通过聚合接口加载
}
try {
const res = await getJobsList({
page: jobsListPage,
pageSize: PAGE_SIZE,
isActive: true,
});
if (res?.success) {
// Mock数据已经是前端格式直接使用
const jobs = res.data;
setJobs((prevList) => {
// 去重处理:过滤掉已存在的数据
const existingIds = new Set(prevList.map((job) => job.id));
const newJobs = jobs.filter((job) => !existingIds.has(job.id));
const newList = [...prevList, ...newJobs];
if (res.total <= newList?.length) {
setJobsListHasMore(false);
} else {
setJobsListPage((prevPage) => prevPage + 1);
}
return newList;
});
}
} catch (error) {
console.error("Failed to fetch data:", error);
if (jobsListPage === 1) {
setJobs([]);
}
}
};
const handleJobWrapperClick = () => {
navigate("/company-jobs-list");
};
// 处理岗位卡片点击,显示岗位详情
const handleJobCardClick = async (item) => {
// 直接从企业内推岗位库中查找对应的岗位
if (item.position) {
const jobData = getJobByPosition(item.position);
if (jobData) {
setSelectedJob(jobData);
setIsFromInterview(true); // 标记是从面试状态卡片点击的
setJobDetailVisible(true);
} else {
toast.error("未找到对应的岗位详情");
}
} else {
toast.error("无法获取岗位详情");
}
};
return (
<div className="company-jobs-page-wrapper">
<div className="company-jobs-page">
{loading ? (
<Spin size={80} className="company-jobs-page-spin" />
) : (
<>
<div
className="company-jobs-page-left"
<div className="company-jobs-page-left">
<div className="company-jobs-page-header">
<p className="company-jobs-page-title">
<img
src="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuUY5w4Kcw4H.png"
alt="icon"
style={{
width: '24px',
height: '24px',
marginRight: '8px',
verticalAlign: 'middle'
}}
/>
企业内推岗位库
</p>
<button
className="view-all-jobs-btn"
onClick={handleJobWrapperClick}
>
<div className="company-jobs-page-header">
<p className="company-jobs-page-title">
<img
src="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuUY5w4Kcw4H.png"
alt="icon"
style={{
width: '24px',
height: '24px',
marginRight: '8px',
verticalAlign: 'middle'
}}
/>
企业内推岗位库
</p>
<button
className="view-all-jobs-btn"
onClick={handleJobWrapperClick}
>
查看全部岗位
</button>
</div>
<InfiniteScroll
loadMore={fetchJobsList}
hasMore={jobsListHasMore}
className="company-jobs-page-left-list-wrapper"
>
<JobList data={jobs} />
</InfiniteScroll>
</div>
<div className="company-jobs-page-interview-wrapper">
<div
className="company-jobs-page-interview"
>
<p className="company-jobs-page-title">
<img
src="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuUY5wqNngw9.png"
alt="icon"
style={{
width: '24px',
height: '24px',
marginRight: '8px',
verticalAlign: 'middle'
}}
/>
岗位面试状态
</p>
<InfiniteScroll
loadMore={fetchInterviewsData}
hasMore={interviewsHasMore}
empty={interviews.length === 0}
className="company-jobs-page-interview-list"
>
{interviews.map((item) => (
<div key={item.id} className="interview-item-wrapper">
<li
className="company-jobs-page-interview-item"
onClick={() => handleJobCardClick(item)}
style={{ cursor: 'pointer' }}
>
<div className="company-jobs-page-interview-item-info">
<p className="company-jobs-page-interview-item-info-position">
{item.position}
</p>
{item.job?.tags?.length > 0 ? (
<ul className="company-jobs-page-interview-item-info-tags">
{item.job.tags.map((tag) => (
<li
className="company-jobs-page-interview-item-info-tag"
key={tag}
>
{tag}
</li>
))}
</ul>
) : null}
<span className="company-jobs-page-interview-item-info-salary">
{item.job?.salary || "面议"}
</span>
</div>
<div className="company-jobs-page-interview-item-btn-wrapper">
<span>{item.interviewTime}</span>
<div
className={`company-jobs-page-interview-item-btn ${
item.status !== "COMPLETED" &&
"company-jobs-page-interview-item-btn-active"
}`}
onClick={(e) => handleStatusClick(e, item)}
style={{ cursor: 'pointer' }}
>
{item.statusText}
</div>
</div>
</li>
<InterviewStatusAnimation
statusText={item.statusText}
isOpen={expandedItemId === item.id}
stageDate={item.stageDate}
/>
</div>
))}
</InfiniteScroll>
</div>
</div>
</>
)}
</div>
查看全部岗位
</button>
</div>
<div className="company-jobs-page-left-list-wrapper">
<Empty
description="暂无岗位数据"
style={{
paddingTop: '100px',
fontSize: '14px',
color: '#86909c'
}}
/>
</div>
</div>
{/* 岗位详情弹窗 */}
<JobInfoModal
visible={jobDetailVisible}
onClose={() => {
setJobDetailVisible(false);
setSelectedJob(null);
setIsFromInterview(false); // 重置标志
}}
data={selectedJob}
directToResume={false}
hideDeliverButton={isFromInterview} // 传递是否隐藏投递按钮的标志
/>
<div className="company-jobs-page-interview-wrapper">
<div className="company-jobs-page-interview">
<p className="company-jobs-page-title">
<img
src="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuUY5wqNngw9.png"
alt="icon"
style={{
width: '24px',
height: '24px',
marginRight: '8px',
verticalAlign: 'middle'
}}
/>
岗位面试状态
</p>
<div className="company-jobs-page-interview-list">
<Empty
description="暂无面试数据"
style={{
paddingTop: '100px',
fontSize: '14px',
color: '#86909c'
}}
/>
</div>
</div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,377 @@
import { useState, useEffect } from "react";
import { useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import { Spin, Empty } from "@arco-design/web-react";
// import { mapJobList, mapInterviewList } from "@/utils/dataMapper"; // 不再需要映射器mock数据已经是前端格式
import InfiniteScroll from "@/components/InfiniteScroll";
import toast from "@/components/Toast";
import JobList from "./components/JobList";
import InterviewStatusAnimation from "./components/InterviewStatusAnimation";
import JobInfoModal from "./components/JobInfoModal";
import {
getCompanyJobsPageData,
getJobsList,
getInterviewsList,
getJobsDetail,
} from "@/services";
import { getJobByPosition } from "@/services/companyJobsNew";
import "./index.css";
const PAGE_SIZE = 10;
const CompanyJobsPage = () => {
const studentInfo = useSelector((state) => state.student.studentInfo);
const [jobs, setJobs] = useState([]);
const [jobsListPage, setJobsListPage] = useState(1);
const [jobsListHasMore, setJobsListHasMore] = useState(true);
const [interviews, setInterviews] = useState([]);
const [interviewsPage, setInterviewsPage] = useState(1);
const [interviewsHasMore, setInterviewsHasMore] = useState(true);
const [initialDataLoaded, setInitialDataLoaded] = useState(false);
const [loading, setLoading] = useState(true);
const [expandedItemId, setExpandedItemId] = useState(null);
const [jobDetailVisible, setJobDetailVisible] = useState(false);
const [selectedJob, setSelectedJob] = useState(null);
const [isFromInterview, setIsFromInterview] = useState(false); // 标识是否从面试状态卡片点击
const navigate = useNavigate();
// 初始化页面数据 - 使用聚合接口
useEffect(() => {
const fetchInitialData = async () => {
try {
setLoading(true);
const res = await getCompanyJobsPageData({
studentId: studentInfo?.id,
});
if (res?.success) {
// 设置面试数据
let interviewsData = [];
if (res.data?.interviews) {
// Mock数据已经是前端格式直接使用不需要映射
interviewsData = res.data.interviews.list || [];
setInterviews(interviewsData);
setInterviewsHasMore(res.data.interviews.hasMore);
if (interviewsData.length > 0) {
setInterviewsPage(2); // 下次从第2页开始
}
}
// 设置岗位数据 - 包含已投递的岗位
if (res.data?.jobs) {
// Mock数据已经是前端格式直接使用不需要映射
const jobsList = res.data.jobs.list || [];
// 从面试数据中提取已投递的岗位信息
const deliveredJobs = interviewsData.map(interview => {
// 确保有完整的岗位数据
const jobData = interview.job || {};
return {
id: `delivered-${interview.id}`, // 使用特殊的ID标识已投递岗位
position: interview.position,
isDelivered: true, // 标记为已投递
interviewTime: interview.interviewTime,
interviewStatus: interview.statusText,
originalInterviewId: interview.id,
// 从job对象中提取所有必要字段
salary: jobData.salary || "面议",
tags: jobData.tags || [],
location: jobData.location || "待定",
education: jobData.education || "待定",
jobCategory: jobData.jobCategory || "专业相关岗位",
remainingPositions: jobData.remainingPositions || 5,
deadline: jobData.deadline || "2025-12-31",
jobType: jobData.jobType || "job",
requirements: jobData.requirements || "",
description: jobData.description || "",
welfare: jobData.welfare || [],
companyInfo: jobData.companyInfo || "",
companyImages: jobData.companyImages || []
};
}).filter(job => job.position); // 过滤掉没有岗位信息的项
// 分离未投递和已过期的岗位
const activeJobs = jobsList.filter(job => !job.isExpired && job.status !== 'expired');
const expiredJobs = jobsList.filter(job => job.isExpired || job.status === 'expired');
// 按照顺序合并:未投递 -> 已投递 -> 已过期
const allJobs = [...activeJobs, ...deliveredJobs, ...expiredJobs];
setJobs(allJobs);
setJobsListHasMore(res.data.jobs.hasMore);
if (allJobs.length > 0) {
setJobsListPage(2); // 下次从第2页开始
}
}
setInitialDataLoaded(true);
}
} catch (error) {
console.error("Failed to fetch initial page data:", error);
// 如果聚合接口失败,回退到原来的方式
setInitialDataLoaded(true);
// 显示错误信息给用户
if (toast && toast.error) {
toast.error("加载数据失败,请刷新重试");
}
} finally {
setLoading(false);
}
};
fetchInitialData();
}, [studentInfo?.id]);
// 获取面试信息 - 用于分页加载更多
const fetchInterviewsData = async () => {
// 如果初始数据还没加载完成,或者是第一页且已有初始数据,则跳过
if (!initialDataLoaded || (interviewsPage === 1 && interviews.length > 0)) {
return;
}
if (studentInfo?.id) {
const res = await getInterviewsList({
page: interviewsPage,
pageSize: PAGE_SIZE,
studentId: studentInfo?.id,
status: "SCHEDULED",
});
if (res.success) {
// Mock数据已经是前端格式直接使用
const interviews = res.data || [];
setInterviews((prevList) => {
// 去重处理:过滤掉已存在的数据
const existingIds = new Set(
prevList.map((interview) => interview.id)
);
const newInterviews = interviews.filter(
(interview) => !existingIds.has(interview.id)
);
const newList = [...prevList, ...newInterviews];
if (res.total <= newList?.length) {
setInterviewsHasMore(false);
} else {
setInterviewsPage((prevPage) => prevPage + 1);
}
return newList;
});
} else {
if (interviewsPage === 1) {
setInterviews([]);
}
toast.error(res.message);
}
}
};
// 处理面试状态点击
const handleStatusClick = (e, item) => {
e.stopPropagation();
// 如果点击的是已展开的项,则收起;否则展开新项
if (expandedItemId === item.id) {
setExpandedItemId(null);
} else {
setExpandedItemId(item.id);
}
};
// 获取企业内推岗位 - 用于分页加载更多
const fetchJobsList = async () => {
// 如果初始数据还没加载完成,或者是第一页且已有初始数据,则跳过
if (!initialDataLoaded || (jobsListPage === 1 && jobs.length > 0)) {
return;
}
// 防止重复请求
if (jobsListPage === 1 && jobs.length === 0) {
return; // 初始数据应该通过聚合接口加载
}
try {
const res = await getJobsList({
page: jobsListPage,
pageSize: PAGE_SIZE,
isActive: true,
});
if (res?.success) {
// Mock数据已经是前端格式直接使用
const jobs = res.data;
setJobs((prevList) => {
// 去重处理:过滤掉已存在的数据
const existingIds = new Set(prevList.map((job) => job.id));
const newJobs = jobs.filter((job) => !existingIds.has(job.id));
const newList = [...prevList, ...newJobs];
if (res.total <= newList?.length) {
setJobsListHasMore(false);
} else {
setJobsListPage((prevPage) => prevPage + 1);
}
return newList;
});
}
} catch (error) {
console.error("Failed to fetch data:", error);
if (jobsListPage === 1) {
setJobs([]);
}
}
};
const handleJobWrapperClick = () => {
navigate("/company-jobs-list");
};
// 处理岗位卡片点击,显示岗位详情
const handleJobCardClick = async (item) => {
// 直接从企业内推岗位库中查找对应的岗位
if (item.position) {
const jobData = getJobByPosition(item.position);
if (jobData) {
setSelectedJob(jobData);
setIsFromInterview(true); // 标记是从面试状态卡片点击的
setJobDetailVisible(true);
} else {
toast.error("未找到对应的岗位详情");
}
} else {
toast.error("无法获取岗位详情");
}
};
return (
<div className="company-jobs-page-wrapper">
<div className="company-jobs-page">
{loading ? (
<Spin size={80} className="company-jobs-page-spin" />
) : (
<>
<div
className="company-jobs-page-left"
>
<div className="company-jobs-page-header">
<p className="company-jobs-page-title">
<img
src="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuUY5w4Kcw4H.png"
alt="icon"
style={{
width: '24px',
height: '24px',
marginRight: '8px',
verticalAlign: 'middle'
}}
/>
企业内推岗位库
</p>
<button
className="view-all-jobs-btn"
onClick={handleJobWrapperClick}
>
查看全部岗位
</button>
</div>
<InfiniteScroll
loadMore={fetchJobsList}
hasMore={jobsListHasMore}
className="company-jobs-page-left-list-wrapper"
>
<JobList data={jobs} />
</InfiniteScroll>
</div>
<div className="company-jobs-page-interview-wrapper">
<div
className="company-jobs-page-interview"
>
<p className="company-jobs-page-title">
<img
src="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuUY5wqNngw9.png"
alt="icon"
style={{
width: '24px',
height: '24px',
marginRight: '8px',
verticalAlign: 'middle'
}}
/>
岗位面试状态
</p>
<InfiniteScroll
loadMore={fetchInterviewsData}
hasMore={interviewsHasMore}
empty={interviews.length === 0}
className="company-jobs-page-interview-list"
>
{interviews.map((item) => (
<div key={item.id} className="interview-item-wrapper">
<li
className="company-jobs-page-interview-item"
onClick={() => handleJobCardClick(item)}
style={{ cursor: 'pointer' }}
>
<div className="company-jobs-page-interview-item-info">
<p className="company-jobs-page-interview-item-info-position">
{item.position}
</p>
{item.job?.tags?.length > 0 ? (
<ul className="company-jobs-page-interview-item-info-tags">
{item.job.tags.map((tag) => (
<li
className="company-jobs-page-interview-item-info-tag"
key={tag}
>
{tag}
</li>
))}
</ul>
) : null}
<span className="company-jobs-page-interview-item-info-salary">
{item.job?.salary || "面议"}
</span>
</div>
<div className="company-jobs-page-interview-item-btn-wrapper">
<span>{item.interviewTime}</span>
<div
className={`company-jobs-page-interview-item-btn ${
item.status !== "COMPLETED" &&
"company-jobs-page-interview-item-btn-active"
}`}
onClick={(e) => handleStatusClick(e, item)}
style={{ cursor: 'pointer' }}
>
{item.statusText}
</div>
</div>
</li>
<InterviewStatusAnimation
statusText={item.statusText}
isOpen={expandedItemId === item.id}
stageDate={item.stageDate}
/>
</div>
))}
</InfiniteScroll>
</div>
</div>
</>
)}
</div>
{/* 岗位详情弹窗 */}
<JobInfoModal
visible={jobDetailVisible}
onClose={() => {
setJobDetailVisible(false);
setSelectedJob(null);
setIsFromInterview(false); // 重置标志
}}
data={selectedJob}
directToResume={false}
hideDeliverButton={isFromInterview} // 传递是否隐藏投递按钮的标志
/>
</div>
);
};
export default CompanyJobsPage;

View File

@@ -23,156 +23,32 @@ export default ({ selectedItem = "面试初体验" }) => {
}
};
// 判断是否是锁定的面试模拟项目
// 所有视频都显示锁定样式
const isLockedItem = () => {
return selectedItem === "第一次线下面试模拟" ||
selectedItem === "第二次线下面试模拟" ||
selectedItem === "第三次线下面试模拟";
return true;
};
// 根据选中项目获取评价数据
// 根据选中项目获取评价数据 - 所有数据改为0或空
const getEvaluationData = () => {
if (selectedItem === "未来的自己") {
return {
totalScore: 97, // 根据JSON计算的真实总分
professionalScore: 57, // (9+10+10+9+10+9)/6*10*0.6 = 57
performanceScore: 40, // (10+10+10+10)/4*10*0.4 = 40
radarData: [9, 10, 10, 9, 10, 9], // 六项专业能力指标来自JSON
radarData2: [10, 10, 10, 10], // 四项现场表现指标来自JSON
title: "面试评价",
content: `# 专业能力
return {
totalScore: 0,
professionalScore: 0,
performanceScore: 0,
radarData: [0, 0, 0, 0, 0, 0],
radarData2: [0, 0, 0, 0],
title: "面试评价",
content: `# 专业能力
1. 专业知识学得特别扎实,讲核心概念的时候又准又清楚,还能随手举例子把道理说透,不光能把知识点讲明白,还能用实例帮人理解,一看就是专业底子特别厚,也很会用学到的知识;
2. 对行业里的产业链、还有未来的发展趋势摸得特别透,不光能说清产业链各个环节是怎么连起来的,还能具体讲明白这些趋势会影响到岗位工作、业务方向,聊到行业相关的话题,总能说出有想法的观点,跟行业里的人交流完全没障碍;
3. 对企业从头到尾的工作流程门儿清,知道业务怎么推进、关键环节在哪,还有跟其他部门怎么配合,既能说清自己进了企业要干些啥,也能找准自己在流程里的位置,跟不同部门合作的关键点也拎得很清,跟实际工作场景特别适配;
4. 碰到具体任务或问题,拆解得特别有条理,分析思路也很系统,能从实际需要出发理出解决办法,想的方案既靠谱能落地,又有新点子,还能用具体数据说明能达到啥效果,不管是想办法还是实际操作,都做得挺到位;
5. 对目标岗位的工作职责、要干的活、需要啥能力都摸得很清楚,聊职业规划和自己跟岗位合不合适的时候,能精准说清自己能发挥啥作用,还能把自己的优势、经历跟岗位需求、业务目标绑在一起说,一看就跟岗位特别对路;
6. 做过的项目又多又完整,说项目的时候能讲清背景、自己具体干了啥、负责哪些环节,还能好好总结从里面学到了啥、能力咋提升的,通过这些项目,能实实在在看出专业能力和动手做事的本事;
暂无评价数据
# 现场表现力
1. 说话表达能力特别好,嘴皮子利索还挺有劲儿,说事儿的时候结构很清楚,每个信息都能说到点子上,既能让人准确 get 到核心观点,又能高效把关键信息传出去,沟通起来又快又准;
2. 心态特别稳,不管是被提问、有工作压力,还是碰到突发情况,一直都很自信、不慌不乱,思路也很清晰,既能把工作或回答做好,还能巧妙化解压力,扛事儿能力和临场应变都挺强;
3. 跟人交流、做展示的时候,姿态特别专业,眼神交流自然又真诚,手势动作跟说话配合得刚好,既显得得体专业,又能通过动作让自己的观点更有说服力,让人觉得特别靠谱;
4. 对时间和流程把控得特别好,不管是做事、聊天还是做展示,每个环节的时间都掐得准,环节之间过渡也很顺,还会留时间做总结,能保证整个过程顺畅推进,干活、沟通的效率都很高。
暂无评价数据
# 综合评价
总的来说,这学生在专业基础、行业认知、懂企业流程、解决问题、适配岗位、项目经验、说话表达、心态调整、职业仪态还有时间管理这些关键方面,都表现得特别优秀,综合能力和职业素养都很突出。这些优点不光能让专业交流更顺畅、解决问题更合理,还能看出他特别有职业潜力,跟岗位也特别匹配,是个很有发展前途的好苗子。`
};
} else if (selectedItem === "第一次线下面试模拟") {
return {
totalScore: 42, // 根据JSON计算的真实总分
professionalScore: 28, // (4+6+5+4+6+3)/6*10*0.6 = 28
performanceScore: 14, // (2+4+5+3)/4*10*0.4 = 14
radarData: [4, 6, 5, 4, 6, 3], // 六项专业能力指标来自JSON
radarData2: [2, 4, 5, 3], // 四项现场表现指标来自JSON
title: "面试评价",
content: `# 专业能力
1. 对知识点的概念总弄混,回答问题也只停在表面,没法往深了说 —— 比如问个核心概念,要么跟别的概念搅在一起,要么就说几句皮毛,根本挖不出背后的门道,能看出来对知识的理解还差得远;
2. 知道的行业知识都是零零散散的,没形成系统,尤其说不明白行业趋势跟岗位、业务的关系 —— 比如问某个趋势会影响工作内容不,他就答不上来,对行业的认知特别散,没串起来;
3. 对工作流程的概念特别模糊,连自己该干啥都搞不清 —— 比如问企业里某个业务流程怎么走,他说不明白,再问他在里面要承担啥角色,更是没头绪,完全没找准自己的位置;
4. 分析问题的时候特别局限,想的方案也很片面,连怎么落地的步骤都没有 —— 比如让他给个解决办法,只能说个大概方向,至于需要哪些资源、分几步做、怎么推进,根本没考虑,这样的方案根本没法用;
5. 对目标岗位的认知特别模糊,连岗位的核心工作是啥、该干到啥程度、哪些活不归自己管,都弄不明白 —— 问他这个岗位主要负责啥,他说的颠三倒四,工作边界更是完全没概念,明显没搞懂岗位到底是干啥的;
6. 做过的项目特别少,就算有一两个,也说不明白情况 —— 要么讲不清项目是做什么的,要么说不出自己在里面具体干了啥,连自己到底是啥角色都模模糊糊,根本没法用项目证明自己有能力;
# 现场表现力
1. 说话特别散乱,抓不住重点,逻辑还老跳 —— 比如跟他聊个事儿,他东说一句西说一句,关键信息没几句,还经常突然从一个话题跳到另一个,听的人根本跟不上,半天搞不清他想表达啥;
2. 情绪波动特别大,一会儿好一会儿坏,特别影响沟通 —— 可能刚开始聊得好好的,稍微有点问题就慌了,或者没耐心了,跟他交流的时候,很容易因为他的情绪受影响,沟通效果特别差;
3. 跟人说话或者坐着的时候,小动作特别多,坐姿也不稳 —— 一会儿摸笔、一会儿挠头,身子还总晃来晃去,这些动作特别容易分散别人的注意力,让人没法专心听他说话,印象分也会打折扣;
4. 不管是做事、做展示还是跟人聊天,时间把控得特别差 —— 要么说起来没个完,严重超时;要么没说几句就结束了,整个过程一点条理都没有,结构乱得很,完全没规划。
# 综合评价
总的来说,这学生在知识理解、行业认知、流程和岗位把握、方案设计、项目经验、表达逻辑,还有情绪管理、行为仪态、时间把控这些方面,都有挺明显的问题。这些问题不光让日常沟通和解决问题受影响,也能看出来他现在还不太能适应实际工作的要求,之后得重点补补知识的深度、多了解行业和岗位,再好好练练说话的逻辑和心态,慢慢把综合能力提上来才行。`
};
} else if (selectedItem === "第二次线下面试模拟") {
return {
totalScore: 67, // 根据JSON计算的真实总分
professionalScore: 41, // (7+7+6+6+7+8)/6*10*0.6 = 41
performanceScore: 26, // (8+7+6+5)/4*10*0.4 = 26
radarData: [7, 7, 6, 6, 7, 8], // 六项专业能力指标来自JSON
radarData2: [8, 7, 6, 5], // 四项现场表现指标来自JSON
title: "面试评价",
content: `# 专业能力
1. 关键知识掌握得挺全面的,大部分内容都能抓准,就是偶尔在小细节上有点马虎,比如个别知识点的细微区别会记混,但整体来看,知识的准确性还是不错的,核心内容都能掌握到位;
2. 对市场上的主要动态有了解,比如行业里近期的热门方向、大家关注的重点,都能说出个大概,而且能简单讲讲这些动态对业务开展有啥意义,虽然说得不算深入,但至少能把 "动态和业务" 的关联点到,有基本的认知;
3. 明白工作的主要流程是啥,比如一项业务从开始到结束要经过哪些关键步骤,都能说清楚,也知道自己在流程里负责哪个环节、要干些啥,但在细节上就有点粗糙了,比如环节之间怎么衔接、遇到小问题该怎么处理,就说不太细;
4. 面对问题或者任务时,能有个初步的思路雏形,比如想解决某个问题,能先提出一两个大概的方向,但思路不够系统,没有把 "为什么这么做、步骤是啥、需要啥支持" 串成完整的逻辑,论证的时候也缺乏足够的理由或者例子支撑,说服力还差了点;
5. 知道目标岗位的主要任务是啥,能说出大概的工作范围,比如日常要处理哪些事、负责哪些板块,但没法深入剖析岗位 —— 像岗位的核心价值是啥、不同任务之间的优先级怎么排、需要具备哪些隐藏技能,这些深入的内容就说不出来了;
6. 也参与过一定数量的项目,不是没经验的,聊项目的时候能大体描述自己在里面做了啥任务,比如负责过数据整理、协助过方案讨论,但说到项目成果就有点笼统了,比如只说 "完成了任务",没说清 "任务带来了啥效果、自己的贡献让项目有啥提升",成果没法具体体现;
# 现场表现力
1. 说话的逻辑基本能让人听明白,不会让人抓不着重点,但偶尔会有重复的情况,比如同一句话换个说法又说一遍,或者讲到一半会停顿一下,想不起来下一句该说啥,不过整体的表达节奏还能跟上,不影响理解;
2. 面对交流或者任务时,基本能保持镇定,不会慌慌张张的,就算偶尔有点紧张,比如说话声音稍微变小、语速变快,也能自己调整过来,很快恢复平稳的状态,不会让紧张影响整体表现;
3. 平时的体态看起来挺得体的,坐姿、站姿都比较规范,跟人交流时也不会有太随意的动作,就是偶尔会有点僵硬,比如坐着的时候身体绷得太紧、手势不太自然,但这些小问题不影响整体的印象,还是显得比较专业的;
4. 不管是做事、做展示还是跟人沟通,基本能在规定时间内完成,不会出现严重超时或者没做完的情况,就是偶尔会有点小偏差 —— 要么比规定时间多花个几分钟,要么为了赶时间稍微省略一点内容,但整体的进度和完整性还是有保障的。
# 综合评价
总的来说,这学生在知识掌握、市场认知、流程理解、思路形成、岗位认知、项目经验、表达逻辑、心态调整、体态和时间把控上,都有基础的能力,没有特别明显的短板,但在 "细节、深度、系统性" 上还有提升空间。之后可以重点补补细节知识、多深入思考岗位和项目的核心价值、把思路梳理得更系统,这样综合能力就能再上一个台阶,也会更适配实际工作的要求。`
};
} else if (selectedItem === "第三次线下面试模拟") {
return {
totalScore: 91, // 根据JSON计算的真实总分
professionalScore: 54, // (8+10+9+8+10+9)/6*10*0.6 = 54
performanceScore: 37, // (10+8+10+10)/4*10*0.4 = 38 (约37)
radarData: [8, 10, 9, 8, 10, 9], // 六项专业能力指标来自JSON
radarData2: [10, 8, 10, 10], // 四项现场表现指标来自JSON
title: "面试评价",
content: `# 专业能力
1. 关键知识掌握得特别全面,不管是核心考点还是重要内容,都能稳稳抓住,就是偶尔在小细节上会有点疏漏,比如个别细碎知识点记不太准,但整体来看,知识的准确性特别好,不会出大差错;
2. 对行业里的产业链和发展趋势摸得很透,不光能说清产业链各个环节怎么联动,还能具体讲明白这些趋势会给岗位工作、业务开展带来啥影响,比如哪种趋势会让岗位多些新任务,哪种趋势能帮业务找新方向,分析得特别实在;
3. 能把企业从头到尾的工作流程说得明明白白,哪个环节该干啥、流程里的关键节点是啥,都门儿清,而且能找准自己在流程里的角色,就连跟其他部门怎么配合、配合的关键点是啥,也能说得很到位,完全不像没接触过实际工作的;
4. 就算单说具体的主要流程,也能讲清楚自己负责的环节要做啥,比如流程里的资料整理、对接沟通这些活儿,都能说透,就是在细节上稍微有点粗糙,比如环节之间怎么交接更顺畅、遇到小问题怎么快速处理,说得没那么细;
5. 对目标岗位的职责了解得特别全面,岗位要干的活儿、承担的责任都能说全,还能精准找到自己在岗位上的价值 —— 比如自己能帮岗位解决啥问题、能给团队带来啥助力,更厉害的是,能结合实际例子说明这些职责和价值怎么跟业务目标挂钩,比如做好某项工作能帮业务完成多少指标,逻辑特别顺;
6. 做过的项目又多又完整,不管是校园里的实践项目,还是外面的实习项目,都有涉及,聊项目的时候,能清清楚楚说清自己在里面扮演啥角色、过程中具体做了哪些贡献,就连最后项目拿到啥成果、带来啥效果,也能说得明明白白,不会含糊其辞;
# 现场表现力
1. 说话特别流畅,而且很有劲儿,不管是回答问题还是分享想法,表达的结构都很严谨,不会东拉西扯,每个信息点都能精准说到点子上,让人一听就懂,还能快速 get 到核心内容,沟通效率特别高;
2. 面对提问或者展示这些场景,基本能保持镇定,不会慌里慌张的,就算偶尔有点紧张,比如语速稍微变快、声音有点抖,也能自己快速调整过来,很快就恢复平稳状态,不会让紧张影响整体发挥;
3. 跟人交流的时候,目光交流特别自然,不会躲躲闪闪,肢体动作也跟说话内容配合得刚好,比如讲重点的时候会配合手势强调,坐着的时候姿态也很放松,这些细节让说的话更有说服力,让人觉得特别靠谱;
4. 不管是做展示、答问题,还是走流程,每个环节的时间都控制得特别准,不会出现超时或者没说完的情况,环节之间衔接得也很自然,不会有生硬的停顿,更难得的是,还会特意留时间做总结,把核心内容再梳理一遍,让人印象更深刻。
# 综合评价
总的来说,这学生在知识掌握、行业认知、流程理解、岗位适配、项目经验、表达能力、心态调整、沟通仪态和时间把控上,都表现得特别出色,基础扎实还懂实际应用,就算偶尔有小瑕疵也不影响整体实力。这样的学生不管是继续学习还是去工作,都能快速适应,后续再把流程细节打磨打磨,综合能力还能再上一个大台阶,绝对是个好苗子。`
};
} else {
return {
totalScore: 14, // 根据JSON计算的真实总分 (面试初体验)
professionalScore: 7, // (2+1+1+1+1+1)/6*10*0.6 = 7
performanceScore: 7, // (2+1+2+2)/4*10*0.4 = 7
radarData: [2, 1, 1, 1, 1, 1], // 六项专业能力指标来自JSON
radarData2: [2, 1, 2, 2], // 四项现场表现指标来自JSON
title: "面试评价",
content: `# 专业能力
1. 基础概念掌握得很不好,经常犯错误,连最基本的知识点都拎不清,这样一来根本没办法展开有效的交流,说出来的内容因为概念错了,也没什么参考价值;
2. 对行业基本情况几乎一无所知,不管问什么跟行业相关的问题,都答不上来,完全没接触过行业里的常识,聊起行业话题根本插不上话;
3. 一点都不理解企业的工作流程,不知道一项业务是怎么推进的,也说不清楚如果自己进了企业,该在哪个环节做事、要干些什么,对实际工作场景完全没概念;
4. 碰到问题或任务的时候,一点清晰的思路都没有,东想西想没章法,想出来的方案要么不切实际、没法落地,要么根本没解决核心问题,完全拿不出能用的办法;
5. 对目标岗位的工作职责彻底不了解,不知道岗位要干哪些活、需要什么能力,聊到跟岗位相关的内容,根本说不出有用的信息,也没法表达自己跟岗位的关联;
6. 要么就没做过什么项目,要么就是有项目经历也说不明白 —— 既讲不清项目背景,也说不出自己在里面干了啥,更总结不出从项目里学到了什么、能力有没有提升,完全没法用项目证明自己的能力;
# 现场表现力
1. 说话特别不连贯,一句完整的话都说不利索,想表达的观点颠三倒四,听的人得费劲猜才能勉强懂一点,信息传递特别低效,很容易让人误解;
2. 心态特别差,一碰到提问或者有点压力的情况,就慌慌张张的,要么说不出话,要么越说越乱,根本没办法持续把问题答完,稍微有点压力就扛不住;
3. 跟人交流或者做展示的时候,姿态特别不专业,要么不敢抬头看人、眼神躲躲闪闪,要么肢体动作很僵硬、很别扭,显得特别紧张、不靠谱,根本没法让人信服;
4. 完全没有时间概念,不管是做事、聊天还是做展示,都把控不好时间 —— 要么一个环节拖半天,要么节奏乱得一塌糊涂,严重影响整个流程的推进,让整体效率特别低。
# 综合评价
总的来说,这学生在专业基础、行业认知、企业流程理解、问题解决、岗位认知、项目经验、表达能力、心态调整、职业仪态和时间管理这些关键方面,都存在比较明显的不足。这些问题不光让专业交流没法顺利进行、碰到问题没法有效解决,也能看出目前他还不太适配实际工作场景,职业潜力还需要花很多功夫去挖掘和提升,得好好补补基础、多积累实践经验才行。`
};
}
暂无评价数据`
};
};
// 判断是否应该显示评价内容
@@ -199,44 +75,13 @@ export default ({ selectedItem = "面试初体验" }) => {
position: 'relative',
width: '100%',
height: '100%',
overflow: 'hidden'
backgroundColor: '#ffffff',
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}>
{/* 背景图片 */}
<div style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
backgroundImage: `url(${
selectedItem === "第一次线下面试模拟"
? "/src/assets/images/InterviewSimulationPage/第一次线下面试模拟.jpg"
: selectedItem === "第二次线下面试模拟"
? "/src/assets/images/InterviewSimulationPage/第二次线下面试模拟.jpg"
: "/src/assets/images/InterviewSimulationPage/第三次线下面试模拟.jpg"
})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
filter: 'blur(20px)',
transform: 'scale(1.1)'
}} />
{/* 半透明遮罩 */}
<div style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
backgroundColor: 'rgba(0, 0, 0, 0.4)'
}} />
{/* 锁图标和文字 */}
<div style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
@@ -248,15 +93,17 @@ export default ({ selectedItem = "面试初体验" }) => {
style={{ width: '280px', height: '280px' }}
/>
<span style={{
color: '#fff',
color: '#000',
fontSize: '16px',
fontWeight: '500',
textAlign: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.6)',
padding: '8px 16px',
borderRadius: '4px'
backgroundColor: '#fff',
padding: '12px 24px',
borderRadius: '8px',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
whiteSpace: 'pre-line'
}}>
DEMO演示非学员无查看权限
该板块将于垂直能力提升阶段启动后开放{"\n"}届时请留意教务系统通知您可在线下站点进行线下面试模拟
</span>
</div>
</div>

View File

@@ -0,0 +1,415 @@
import { useState, useRef, useEffect } from "react";
import ReactMarkdown from "react-markdown";
import ScoreChart from "../ScoreChart";
import RadarChart from "../RadarChart";
import "./index.css";
export default ({ selectedItem = "面试初体验" }) => {
// 根据选中项目获取对应的视频URL
const getVideoUrl = () => {
switch(selectedItem) {
case "面试初体验":
return "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/video/teach_sys/interview_simulation/3years_ago.mov";
case "未来的自己":
return "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/video/teach_sys/interview_simulation/3years_later.mov";
case "第一次线下面试模拟":
return "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/video/teach_sys/interview_offline_vedio/recuUpJSOKoqAm.mov"; // 使用相同视频作为示例
case "第二次线下面试模拟":
return "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/video/teach_sys/interview_offline_vedio/recuUpJT02CMM5.mp4"; // 使用相同视频作为示例
case "第三次线下面试模拟":
return "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/video/teach_sys/interview_offline_vedio/recuUpJSOKoqAm.mov"; // 使用相同视频作为示例
default:
return "";
}
};
// 判断是否是锁定的面试模拟项目
const isLockedItem = () => {
return selectedItem === "第一次线下面试模拟" ||
selectedItem === "第二次线下面试模拟" ||
selectedItem === "第三次线下面试模拟";
};
// 根据选中项目获取评价数据
const getEvaluationData = () => {
if (selectedItem === "未来的自己") {
return {
totalScore: 97, // 根据JSON计算的真实总分
professionalScore: 57, // (9+10+10+9+10+9)/6*10*0.6 = 57
performanceScore: 40, // (10+10+10+10)/4*10*0.4 = 40
radarData: [9, 10, 10, 9, 10, 9], // 六项专业能力指标来自JSON
radarData2: [10, 10, 10, 10], // 四项现场表现指标来自JSON
title: "面试评价",
content: `# 专业能力
1. 专业知识学得特别扎实,讲核心概念的时候又准又清楚,还能随手举例子把道理说透,不光能把知识点讲明白,还能用实例帮人理解,一看就是专业底子特别厚,也很会用学到的知识;
2. 对行业里的产业链、还有未来的发展趋势摸得特别透,不光能说清产业链各个环节是怎么连起来的,还能具体讲明白这些趋势会影响到岗位工作、业务方向,聊到行业相关的话题,总能说出有想法的观点,跟行业里的人交流完全没障碍;
3. 对企业从头到尾的工作流程门儿清,知道业务怎么推进、关键环节在哪,还有跟其他部门怎么配合,既能说清自己进了企业要干些啥,也能找准自己在流程里的位置,跟不同部门合作的关键点也拎得很清,跟实际工作场景特别适配;
4. 碰到具体任务或问题,拆解得特别有条理,分析思路也很系统,能从实际需要出发理出解决办法,想的方案既靠谱能落地,又有新点子,还能用具体数据说明能达到啥效果,不管是想办法还是实际操作,都做得挺到位;
5. 对目标岗位的工作职责、要干的活、需要啥能力都摸得很清楚,聊职业规划和自己跟岗位合不合适的时候,能精准说清自己能发挥啥作用,还能把自己的优势、经历跟岗位需求、业务目标绑在一起说,一看就跟岗位特别对路;
6. 做过的项目又多又完整,说项目的时候能讲清背景、自己具体干了啥、负责哪些环节,还能好好总结从里面学到了啥、能力咋提升的,通过这些项目,能实实在在看出专业能力和动手做事的本事;
# 现场表现力
1. 说话表达能力特别好,嘴皮子利索还挺有劲儿,说事儿的时候结构很清楚,每个信息都能说到点子上,既能让人准确 get 到核心观点,又能高效把关键信息传出去,沟通起来又快又准;
2. 心态特别稳,不管是被提问、有工作压力,还是碰到突发情况,一直都很自信、不慌不乱,思路也很清晰,既能把工作或回答做好,还能巧妙化解压力,扛事儿能力和临场应变都挺强;
3. 跟人交流、做展示的时候,姿态特别专业,眼神交流自然又真诚,手势动作跟说话配合得刚好,既显得得体专业,又能通过动作让自己的观点更有说服力,让人觉得特别靠谱;
4. 对时间和流程把控得特别好,不管是做事、聊天还是做展示,每个环节的时间都掐得准,环节之间过渡也很顺,还会留时间做总结,能保证整个过程顺畅推进,干活、沟通的效率都很高。
# 综合评价
总的来说,这学生在专业基础、行业认知、懂企业流程、解决问题、适配岗位、项目经验、说话表达、心态调整、职业仪态还有时间管理这些关键方面,都表现得特别优秀,综合能力和职业素养都很突出。这些优点不光能让专业交流更顺畅、解决问题更合理,还能看出他特别有职业潜力,跟岗位也特别匹配,是个很有发展前途的好苗子。`
};
} else if (selectedItem === "第一次线下面试模拟") {
return {
totalScore: 42, // 根据JSON计算的真实总分
professionalScore: 28, // (4+6+5+4+6+3)/6*10*0.6 = 28
performanceScore: 14, // (2+4+5+3)/4*10*0.4 = 14
radarData: [4, 6, 5, 4, 6, 3], // 六项专业能力指标来自JSON
radarData2: [2, 4, 5, 3], // 四项现场表现指标来自JSON
title: "面试评价",
content: `# 专业能力
1. 对知识点的概念总弄混,回答问题也只停在表面,没法往深了说 —— 比如问个核心概念,要么跟别的概念搅在一起,要么就说几句皮毛,根本挖不出背后的门道,能看出来对知识的理解还差得远;
2. 知道的行业知识都是零零散散的,没形成系统,尤其说不明白行业趋势跟岗位、业务的关系 —— 比如问某个趋势会影响工作内容不,他就答不上来,对行业的认知特别散,没串起来;
3. 对工作流程的概念特别模糊,连自己该干啥都搞不清 —— 比如问企业里某个业务流程怎么走,他说不明白,再问他在里面要承担啥角色,更是没头绪,完全没找准自己的位置;
4. 分析问题的时候特别局限,想的方案也很片面,连怎么落地的步骤都没有 —— 比如让他给个解决办法,只能说个大概方向,至于需要哪些资源、分几步做、怎么推进,根本没考虑,这样的方案根本没法用;
5. 对目标岗位的认知特别模糊,连岗位的核心工作是啥、该干到啥程度、哪些活不归自己管,都弄不明白 —— 问他这个岗位主要负责啥,他说的颠三倒四,工作边界更是完全没概念,明显没搞懂岗位到底是干啥的;
6. 做过的项目特别少,就算有一两个,也说不明白情况 —— 要么讲不清项目是做什么的,要么说不出自己在里面具体干了啥,连自己到底是啥角色都模模糊糊,根本没法用项目证明自己有能力;
# 现场表现力
1. 说话特别散乱,抓不住重点,逻辑还老跳 —— 比如跟他聊个事儿,他东说一句西说一句,关键信息没几句,还经常突然从一个话题跳到另一个,听的人根本跟不上,半天搞不清他想表达啥;
2. 情绪波动特别大,一会儿好一会儿坏,特别影响沟通 —— 可能刚开始聊得好好的,稍微有点问题就慌了,或者没耐心了,跟他交流的时候,很容易因为他的情绪受影响,沟通效果特别差;
3. 跟人说话或者坐着的时候,小动作特别多,坐姿也不稳 —— 一会儿摸笔、一会儿挠头,身子还总晃来晃去,这些动作特别容易分散别人的注意力,让人没法专心听他说话,印象分也会打折扣;
4. 不管是做事、做展示还是跟人聊天,时间把控得特别差 —— 要么说起来没个完,严重超时;要么没说几句就结束了,整个过程一点条理都没有,结构乱得很,完全没规划。
# 综合评价
总的来说,这学生在知识理解、行业认知、流程和岗位把握、方案设计、项目经验、表达逻辑,还有情绪管理、行为仪态、时间把控这些方面,都有挺明显的问题。这些问题不光让日常沟通和解决问题受影响,也能看出来他现在还不太能适应实际工作的要求,之后得重点补补知识的深度、多了解行业和岗位,再好好练练说话的逻辑和心态,慢慢把综合能力提上来才行。`
};
} else if (selectedItem === "第二次线下面试模拟") {
return {
totalScore: 67, // 根据JSON计算的真实总分
professionalScore: 41, // (7+7+6+6+7+8)/6*10*0.6 = 41
performanceScore: 26, // (8+7+6+5)/4*10*0.4 = 26
radarData: [7, 7, 6, 6, 7, 8], // 六项专业能力指标来自JSON
radarData2: [8, 7, 6, 5], // 四项现场表现指标来自JSON
title: "面试评价",
content: `# 专业能力
1. 关键知识掌握得挺全面的,大部分内容都能抓准,就是偶尔在小细节上有点马虎,比如个别知识点的细微区别会记混,但整体来看,知识的准确性还是不错的,核心内容都能掌握到位;
2. 对市场上的主要动态有了解,比如行业里近期的热门方向、大家关注的重点,都能说出个大概,而且能简单讲讲这些动态对业务开展有啥意义,虽然说得不算深入,但至少能把 "动态和业务" 的关联点到,有基本的认知;
3. 明白工作的主要流程是啥,比如一项业务从开始到结束要经过哪些关键步骤,都能说清楚,也知道自己在流程里负责哪个环节、要干些啥,但在细节上就有点粗糙了,比如环节之间怎么衔接、遇到小问题该怎么处理,就说不太细;
4. 面对问题或者任务时,能有个初步的思路雏形,比如想解决某个问题,能先提出一两个大概的方向,但思路不够系统,没有把 "为什么这么做、步骤是啥、需要啥支持" 串成完整的逻辑,论证的时候也缺乏足够的理由或者例子支撑,说服力还差了点;
5. 知道目标岗位的主要任务是啥,能说出大概的工作范围,比如日常要处理哪些事、负责哪些板块,但没法深入剖析岗位 —— 像岗位的核心价值是啥、不同任务之间的优先级怎么排、需要具备哪些隐藏技能,这些深入的内容就说不出来了;
6. 也参与过一定数量的项目,不是没经验的,聊项目的时候能大体描述自己在里面做了啥任务,比如负责过数据整理、协助过方案讨论,但说到项目成果就有点笼统了,比如只说 "完成了任务",没说清 "任务带来了啥效果、自己的贡献让项目有啥提升",成果没法具体体现;
# 现场表现力
1. 说话的逻辑基本能让人听明白,不会让人抓不着重点,但偶尔会有重复的情况,比如同一句话换个说法又说一遍,或者讲到一半会停顿一下,想不起来下一句该说啥,不过整体的表达节奏还能跟上,不影响理解;
2. 面对交流或者任务时,基本能保持镇定,不会慌慌张张的,就算偶尔有点紧张,比如说话声音稍微变小、语速变快,也能自己调整过来,很快恢复平稳的状态,不会让紧张影响整体表现;
3. 平时的体态看起来挺得体的,坐姿、站姿都比较规范,跟人交流时也不会有太随意的动作,就是偶尔会有点僵硬,比如坐着的时候身体绷得太紧、手势不太自然,但这些小问题不影响整体的印象,还是显得比较专业的;
4. 不管是做事、做展示还是跟人沟通,基本能在规定时间内完成,不会出现严重超时或者没做完的情况,就是偶尔会有点小偏差 —— 要么比规定时间多花个几分钟,要么为了赶时间稍微省略一点内容,但整体的进度和完整性还是有保障的。
# 综合评价
总的来说,这学生在知识掌握、市场认知、流程理解、思路形成、岗位认知、项目经验、表达逻辑、心态调整、体态和时间把控上,都有基础的能力,没有特别明显的短板,但在 "细节、深度、系统性" 上还有提升空间。之后可以重点补补细节知识、多深入思考岗位和项目的核心价值、把思路梳理得更系统,这样综合能力就能再上一个台阶,也会更适配实际工作的要求。`
};
} else if (selectedItem === "第三次线下面试模拟") {
return {
totalScore: 91, // 根据JSON计算的真实总分
professionalScore: 54, // (8+10+9+8+10+9)/6*10*0.6 = 54
performanceScore: 37, // (10+8+10+10)/4*10*0.4 = 38 (约37)
radarData: [8, 10, 9, 8, 10, 9], // 六项专业能力指标来自JSON
radarData2: [10, 8, 10, 10], // 四项现场表现指标来自JSON
title: "面试评价",
content: `# 专业能力
1. 关键知识掌握得特别全面,不管是核心考点还是重要内容,都能稳稳抓住,就是偶尔在小细节上会有点疏漏,比如个别细碎知识点记不太准,但整体来看,知识的准确性特别好,不会出大差错;
2. 对行业里的产业链和发展趋势摸得很透,不光能说清产业链各个环节怎么联动,还能具体讲明白这些趋势会给岗位工作、业务开展带来啥影响,比如哪种趋势会让岗位多些新任务,哪种趋势能帮业务找新方向,分析得特别实在;
3. 能把企业从头到尾的工作流程说得明明白白,哪个环节该干啥、流程里的关键节点是啥,都门儿清,而且能找准自己在流程里的角色,就连跟其他部门怎么配合、配合的关键点是啥,也能说得很到位,完全不像没接触过实际工作的;
4. 就算单说具体的主要流程,也能讲清楚自己负责的环节要做啥,比如流程里的资料整理、对接沟通这些活儿,都能说透,就是在细节上稍微有点粗糙,比如环节之间怎么交接更顺畅、遇到小问题怎么快速处理,说得没那么细;
5. 对目标岗位的职责了解得特别全面,岗位要干的活儿、承担的责任都能说全,还能精准找到自己在岗位上的价值 —— 比如自己能帮岗位解决啥问题、能给团队带来啥助力,更厉害的是,能结合实际例子说明这些职责和价值怎么跟业务目标挂钩,比如做好某项工作能帮业务完成多少指标,逻辑特别顺;
6. 做过的项目又多又完整,不管是校园里的实践项目,还是外面的实习项目,都有涉及,聊项目的时候,能清清楚楚说清自己在里面扮演啥角色、过程中具体做了哪些贡献,就连最后项目拿到啥成果、带来啥效果,也能说得明明白白,不会含糊其辞;
# 现场表现力
1. 说话特别流畅,而且很有劲儿,不管是回答问题还是分享想法,表达的结构都很严谨,不会东拉西扯,每个信息点都能精准说到点子上,让人一听就懂,还能快速 get 到核心内容,沟通效率特别高;
2. 面对提问或者展示这些场景,基本能保持镇定,不会慌里慌张的,就算偶尔有点紧张,比如语速稍微变快、声音有点抖,也能自己快速调整过来,很快就恢复平稳状态,不会让紧张影响整体发挥;
3. 跟人交流的时候,目光交流特别自然,不会躲躲闪闪,肢体动作也跟说话内容配合得刚好,比如讲重点的时候会配合手势强调,坐着的时候姿态也很放松,这些细节让说的话更有说服力,让人觉得特别靠谱;
4. 不管是做展示、答问题,还是走流程,每个环节的时间都控制得特别准,不会出现超时或者没说完的情况,环节之间衔接得也很自然,不会有生硬的停顿,更难得的是,还会特意留时间做总结,把核心内容再梳理一遍,让人印象更深刻。
# 综合评价
总的来说,这学生在知识掌握、行业认知、流程理解、岗位适配、项目经验、表达能力、心态调整、沟通仪态和时间把控上,都表现得特别出色,基础扎实还懂实际应用,就算偶尔有小瑕疵也不影响整体实力。这样的学生不管是继续学习还是去工作,都能快速适应,后续再把流程细节打磨打磨,综合能力还能再上一个大台阶,绝对是个好苗子。`
};
} else {
return {
totalScore: 14, // 根据JSON计算的真实总分 (面试初体验)
professionalScore: 7, // (2+1+1+1+1+1)/6*10*0.6 = 7
performanceScore: 7, // (2+1+2+2)/4*10*0.4 = 7
radarData: [2, 1, 1, 1, 1, 1], // 六项专业能力指标来自JSON
radarData2: [2, 1, 2, 2], // 四项现场表现指标来自JSON
title: "面试评价",
content: `# 专业能力
1. 基础概念掌握得很不好,经常犯错误,连最基本的知识点都拎不清,这样一来根本没办法展开有效的交流,说出来的内容因为概念错了,也没什么参考价值;
2. 对行业基本情况几乎一无所知,不管问什么跟行业相关的问题,都答不上来,完全没接触过行业里的常识,聊起行业话题根本插不上话;
3. 一点都不理解企业的工作流程,不知道一项业务是怎么推进的,也说不清楚如果自己进了企业,该在哪个环节做事、要干些什么,对实际工作场景完全没概念;
4. 碰到问题或任务的时候,一点清晰的思路都没有,东想西想没章法,想出来的方案要么不切实际、没法落地,要么根本没解决核心问题,完全拿不出能用的办法;
5. 对目标岗位的工作职责彻底不了解,不知道岗位要干哪些活、需要什么能力,聊到跟岗位相关的内容,根本说不出有用的信息,也没法表达自己跟岗位的关联;
6. 要么就没做过什么项目,要么就是有项目经历也说不明白 —— 既讲不清项目背景,也说不出自己在里面干了啥,更总结不出从项目里学到了什么、能力有没有提升,完全没法用项目证明自己的能力;
# 现场表现力
1. 说话特别不连贯,一句完整的话都说不利索,想表达的观点颠三倒四,听的人得费劲猜才能勉强懂一点,信息传递特别低效,很容易让人误解;
2. 心态特别差,一碰到提问或者有点压力的情况,就慌慌张张的,要么说不出话,要么越说越乱,根本没办法持续把问题答完,稍微有点压力就扛不住;
3. 跟人交流或者做展示的时候,姿态特别不专业,要么不敢抬头看人、眼神躲躲闪闪,要么肢体动作很僵硬、很别扭,显得特别紧张、不靠谱,根本没法让人信服;
4. 完全没有时间概念,不管是做事、聊天还是做展示,都把控不好时间 —— 要么一个环节拖半天,要么节奏乱得一塌糊涂,严重影响整个流程的推进,让整体效率特别低。
# 综合评价
总的来说,这学生在专业基础、行业认知、企业流程理解、问题解决、岗位认知、项目经验、表达能力、心态调整、职业仪态和时间管理这些关键方面,都存在比较明显的不足。这些问题不光让专业交流没法顺利进行、碰到问题没法有效解决,也能看出目前他还不太适配实际工作场景,职业潜力还需要花很多功夫去挖掘和提升,得好好补补基础、多积累实践经验才行。`
};
}
};
// 判断是否应该显示评价内容
const shouldShowEvaluation = () => {
return selectedItem === "面试初体验" ||
selectedItem === "未来的自己" ||
selectedItem === "第一次线下面试模拟" ||
selectedItem === "第二次线下面试模拟" ||
selectedItem === "第三次线下面试模拟";
};
return (
<div className="interview-rating-wrapper">
{/* 视频播放器区域 */}
<div className="interview-rating-video-section">
<div className="interview-rating-header">
<span className="interview-rating-header-title">
{selectedItem}
</span>
</div>
<div className="interview-rating-video">
{isLockedItem() ? (
<div className="locked-video-container" style={{
position: 'relative',
width: '100%',
height: '100%',
overflow: 'hidden'
}}>
{/* 背景图片 */}
<div style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
backgroundImage: `url(${
selectedItem === "第一次线下面试模拟"
? "/src/assets/images/InterviewSimulationPage/第一次线下面试模拟.jpg"
: selectedItem === "第二次线下面试模拟"
? "/src/assets/images/InterviewSimulationPage/第二次线下面试模拟.jpg"
: "/src/assets/images/InterviewSimulationPage/第三次线下面试模拟.jpg"
})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
filter: 'blur(20px)',
transform: 'scale(1.1)'
}} />
{/* 半透明遮罩 */}
<div style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
backgroundColor: 'rgba(0, 0, 0, 0.4)'
}} />
{/* 锁图标和文字 */}
<div style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '16px'
}}>
<img
src="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuVOrz2GnJdK.png"
alt="lock"
style={{ width: '280px', height: '280px' }}
/>
<span style={{
color: '#fff',
fontSize: '16px',
fontWeight: '500',
textAlign: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.6)',
padding: '8px 16px',
borderRadius: '4px'
}}>
DEMO演示非学员无查看权限
</span>
</div>
</div>
) : (
<video src={getVideoUrl()} controls></video>
)}
</div>
</div>
{/* 评价内容区域 - 仅在特定项目时显示 */}
{shouldShowEvaluation() && (
<>
{/* 面试评分区域 */}
<div className="interview-evaluation-charts-wrapper">
<div className="interview-rating-header">
<img src="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuUY5uY7Ek50.png" alt="icon" style={{ width: '28px', height: '28px', marginRight: '10px' }} />
<span className="interview-rating-header-title">面试评分</span>
</div>
<div className="charts-content">
<div className="charts-content-top">
<ScoreChart className="score-chart" value={getEvaluationData().totalScore} />
<div className="score-info">
<div className="score-info-item item1">
<p className="score-info-item-title">
专业能力评分
<span className="score-info-item-value">{getEvaluationData().professionalScore}</span>
</p>
<p className="score-info-line">
<i
style={{
width: `${(getEvaluationData().professionalScore / 60) * 100}%`,
}}
/>
<span className="score-info-line-total-value">60</span>
</p>
</div>
<div className="score-info-item item2">
<p className="score-info-item-title">
现场表现评分
<span className="score-info-item-value">{getEvaluationData().performanceScore}</span>
</p>
<p className="score-info-line">
<i
style={{
width: `${(getEvaluationData().performanceScore / 40) * 100}%`,
}}
/>
<span className="score-info-line-total-value">40</span>
</p>
</div>
</div>
</div>
<div className="charts-content-bottom">
<RadarChart
className="radar-chart"
data={getEvaluationData().radarData}
indicator={[
{ name: "基础知识\n掌握水平", max: 10 },
{ name: "产业链\n认知程度", max: 10 },
{ name: "企业生产\n体系了解", max: 10 },
{ name: "典型问题\n解决能力", max: 10 },
{ name: "岗位职责\n理解程度", max: 10 },
{ name: "项目经历\n丰富程度", max: 10 },
]}
/>
<RadarChart
className="radar-chart"
data={getEvaluationData().radarData2 || [7, 8, 6, 7]}
indicator={[
{ name: "语言表达与逻辑", max: 10 },
{ name: "自信与情绪管理", max: 10 },
{ name: "仪表与职场礼仪", max: 10 },
{ name: "时间管理与条理性", max: 10 },
]}
lineClolr="#E8F5E9"
areaColor="#C8E6C9"
areaBorderColor="#66BB6A"
isGreenTheme={true}
/>
</div>
</div>
</div>
{/* 面试评价三板块区域 */}
<div className="interview-evaluation-sections">
{/* 专业能力板块 */}
<div className="evaluation-section">
<div className="section-header">
<div className="section-icon professional-icon">
<img src="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuUY5vlvUj2X.png" alt="专业能力" />
</div>
<h3 className="section-title">专业能力</h3>
<div className="section-score">
<span className="score-label">得分</span>
<span className="score-value">{getEvaluationData().professionalScore}</span>
<span className="score-total">/60</span>
</div>
</div>
<div className="section-content">
<ReactMarkdown>
{(() => {
const content = getEvaluationData().content;
const sections = content.split('#');
if (sections.length > 1) {
const professionalContent = sections[1].split('\n\n');
return professionalContent.slice(1, -1).join('\n\n');
}
return '';
})()}
</ReactMarkdown>
</div>
</div>
{/* 现场表现力板块 */}
<div className="evaluation-section">
<div className="section-header">
<div className="section-icon performance-icon">
<img src="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuUY5uY7Ek50.png" alt="现场表现力" />
</div>
<h3 className="section-title">现场表现力</h3>
<div className="section-score">
<span className="score-label">得分</span>
<span className="score-value">{getEvaluationData().performanceScore}</span>
<span className="score-total">/40</span>
</div>
</div>
<div className="section-content">
<ReactMarkdown>
{getEvaluationData().content.split('# 现场表现力')[1].split('# 综合评价')[0].trim()}
</ReactMarkdown>
</div>
</div>
{/* 综合评价板块 */}
<div className="evaluation-section comprehensive-section">
<div className="section-header">
<h3 className="section-title">综合评价</h3>
<div className="section-badge">
{getEvaluationData().totalScore >= 80 ? '优秀' :
getEvaluationData().totalScore >= 60 ? '良好' :
getEvaluationData().totalScore >= 40 ? '及格' : '需努力'}
</div>
</div>
<div className="section-content">
<ReactMarkdown>
{getEvaluationData().content.split('# 综合评价')[1].trim()}
</ReactMarkdown>
</div>
</div>
</div>
</>
)}
</div>
);
};

View File

@@ -0,0 +1,294 @@
import { useState, useRef, useEffect } from "react";
import ReactMarkdown from "react-markdown";
import ScoreChart from "../ScoreChart";
import RadarChart from "../RadarChart";
import "./index.css";
export default ({ selectedItem = "面试初体验" }) => {
// 根据选中项目获取对应的视频URL
const getVideoUrl = () => {
switch(selectedItem) {
case "面试初体验":
return "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/video/teach_sys/interview_simulation/3years_ago.mov";
case "未来的自己":
return "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/video/teach_sys/interview_simulation/3years_later.mov";
case "第一次线下面试模拟":
return "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/video/teach_sys/interview_offline_vedio/recuUpJSOKoqAm.mov"; // 使用相同视频作为示例
case "第二次线下面试模拟":
return "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/video/teach_sys/interview_offline_vedio/recuUpJT02CMM5.mp4"; // 使用相同视频作为示例
case "第三次线下面试模拟":
return "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/video/teach_sys/interview_offline_vedio/recuUpJSOKoqAm.mov"; // 使用相同视频作为示例
default:
return "";
}
};
// 判断是否是锁定的面试模拟项目
const isLockedItem = () => {
return selectedItem === "第一次线下面试模拟" ||
selectedItem === "第二次线下面试模拟" ||
selectedItem === "第三次线下面试模拟";
};
// 根据选中项目获取评价数据
const getEvaluationData = () => {
// 所有评分数据改为0,评价改为空
return {
totalScore: 0,
professionalScore: 0,
performanceScore: 0,
radarData: [0, 0, 0, 0, 0, 0],
radarData2: [0, 0, 0, 0],
title: "面试评价",
content: `# 专业能力
暂无评价数据
# 现场表现力
暂无评价数据
# 综合评价
暂无评价数据`
};
};
// 判断是否应该显示评价内容
const shouldShowEvaluation = () => {
return selectedItem === "面试初体验" ||
selectedItem === "未来的自己" ||
selectedItem === "第一次线下面试模拟" ||
selectedItem === "第二次线下面试模拟" ||
selectedItem === "第三次线下面试模拟";
};
return (
<div className="interview-rating-wrapper">
{/* 视频播放器区域 */}
<div className="interview-rating-video-section">
<div className="interview-rating-header">
<span className="interview-rating-header-title">
{selectedItem}
</span>
</div>
<div className="interview-rating-video">
{isLockedItem() ? (
<div className="locked-video-container" style={{
position: 'relative',
width: '100%',
height: '100%',
overflow: 'hidden'
}}>
{/* 背景图片 */}
<div style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
backgroundImage: `url(${
selectedItem === "第一次线下面试模拟"
? "/src/assets/images/InterviewSimulationPage/第一次线下面试模拟.jpg"
: selectedItem === "第二次线下面试模拟"
? "/src/assets/images/InterviewSimulationPage/第二次线下面试模拟.jpg"
: "/src/assets/images/InterviewSimulationPage/第三次线下面试模拟.jpg"
})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
filter: 'blur(20px)',
transform: 'scale(1.1)'
}} />
{/* 半透明遮罩 */}
<div style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
backgroundColor: 'rgba(0, 0, 0, 0.4)'
}} />
{/* 锁图标和文字 */}
<div style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '16px'
}}>
<img
src="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuVOrz2GnJdK.png"
alt="lock"
style={{ width: '280px', height: '280px' }}
/>
<span style={{
color: '#fff',
fontSize: '16px',
fontWeight: '500',
textAlign: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.6)',
padding: '8px 16px',
borderRadius: '4px'
}}>
DEMO演示非学员无查看权限
</span>
</div>
</div>
) : (
<video src={getVideoUrl()} controls></video>
)}
</div>
</div>
{/* 评价内容区域 - 仅在特定项目时显示 */}
{shouldShowEvaluation() && (
<>
{/* 面试评分区域 */}
<div className="interview-evaluation-charts-wrapper">
<div className="interview-rating-header">
<img src="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuUY5uY7Ek50.png" alt="icon" style={{ width: '28px', height: '28px', marginRight: '10px' }} />
<span className="interview-rating-header-title">面试评分</span>
</div>
<div className="charts-content">
<div className="charts-content-top">
<ScoreChart className="score-chart" value={getEvaluationData().totalScore} />
<div className="score-info">
<div className="score-info-item item1">
<p className="score-info-item-title">
专业能力评分
<span className="score-info-item-value">{getEvaluationData().professionalScore}</span>
</p>
<p className="score-info-line">
<i
style={{
width: `${(getEvaluationData().professionalScore / 60) * 100}%`,
}}
/>
<span className="score-info-line-total-value">60</span>
</p>
</div>
<div className="score-info-item item2">
<p className="score-info-item-title">
现场表现评分
<span className="score-info-item-value">{getEvaluationData().performanceScore}</span>
</p>
<p className="score-info-line">
<i
style={{
width: `${(getEvaluationData().performanceScore / 40) * 100}%`,
}}
/>
<span className="score-info-line-total-value">40</span>
</p>
</div>
</div>
</div>
<div className="charts-content-bottom">
<RadarChart
className="radar-chart"
data={getEvaluationData().radarData}
indicator={[
{ name: "基础知识\n掌握水平", max: 10 },
{ name: "产业链\n认知程度", max: 10 },
{ name: "企业生产\n体系了解", max: 10 },
{ name: "典型问题\n解决能力", max: 10 },
{ name: "岗位职责\n理解程度", max: 10 },
{ name: "项目经历\n丰富程度", max: 10 },
]}
/>
<RadarChart
className="radar-chart"
data={getEvaluationData().radarData2 || [7, 8, 6, 7]}
indicator={[
{ name: "语言表达与逻辑", max: 10 },
{ name: "自信与情绪管理", max: 10 },
{ name: "仪表与职场礼仪", max: 10 },
{ name: "时间管理与条理性", max: 10 },
]}
lineClolr="#E8F5E9"
areaColor="#C8E6C9"
areaBorderColor="#66BB6A"
isGreenTheme={true}
/>
</div>
</div>
</div>
{/* 面试评价三板块区域 */}
<div className="interview-evaluation-sections">
{/* 专业能力板块 */}
<div className="evaluation-section">
<div className="section-header">
<div className="section-icon professional-icon">
<img src="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuUY5vlvUj2X.png" alt="专业能力" />
</div>
<h3 className="section-title">专业能力</h3>
<div className="section-score">
<span className="score-label">得分</span>
<span className="score-value">{getEvaluationData().professionalScore}</span>
<span className="score-total">/60</span>
</div>
</div>
<div className="section-content">
<ReactMarkdown>
{(() => {
const content = getEvaluationData().content;
const sections = content.split('#');
if (sections.length > 1) {
const professionalContent = sections[1].split('\n\n');
return professionalContent.slice(1, -1).join('\n\n');
}
return '';
})()}
</ReactMarkdown>
</div>
</div>
{/* 现场表现力板块 */}
<div className="evaluation-section">
<div className="section-header">
<div className="section-icon performance-icon">
<img src="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuUY5uY7Ek50.png" alt="现场表现力" />
</div>
<h3 className="section-title">现场表现力</h3>
<div className="section-score">
<span className="score-label">得分</span>
<span className="score-value">{getEvaluationData().performanceScore}</span>
<span className="score-total">/40</span>
</div>
</div>
<div className="section-content">
<ReactMarkdown>
{getEvaluationData().content.split('# 现场表现力')[1].split('# 综合评价')[0].trim()}
</ReactMarkdown>
</div>
</div>
{/* 综合评价板块 */}
<div className="evaluation-section comprehensive-section">
<div className="section-header">
<h3 className="section-title">综合评价</h3>
<div className="section-badge">
{getEvaluationData().totalScore >= 80 ? '优秀' :
getEvaluationData().totalScore >= 60 ? '良好' :
getEvaluationData().totalScore >= 40 ? '及格' : '需努力'}
</div>
</div>
<div className="section-content">
<ReactMarkdown>
{getEvaluationData().content.split('# 综合评价')[1].trim()}
</ReactMarkdown>
</div>
</div>
</div>
</>
)}
</div>
);
};

View File

@@ -39,21 +39,6 @@ export default ({ onItemSelect }) => {
>
<p>
面试初体验
<span style={{
marginLeft: '8px',
padding: '2px 8px',
background: 'linear-gradient(135deg, #5DADE2 0%, #2874A6 100%)',
borderRadius: '12px',
color: '#ffffff',
fontSize: '12px',
fontWeight: 'bold',
fontStyle: 'italic',
letterSpacing: '1px',
textTransform: 'uppercase',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}>
demo
</span>
</p>
<div className="time-line-item-info">
初次接触面试环境体验真实面试流程了解自身在面试中的基本表现和待提升的方面
@@ -71,21 +56,6 @@ export default ({ onItemSelect }) => {
>
<p>
未来的自己
<span style={{
marginLeft: '8px',
padding: '2px 8px',
background: 'linear-gradient(135deg, #5DADE2 0%, #2874A6 100%)',
borderRadius: '12px',
color: '#ffffff',
fontSize: '12px',
fontWeight: 'bold',
fontStyle: 'italic',
letterSpacing: '1px',
textTransform: 'uppercase',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}>
demo
</span>
</p>
<div className="time-line-item-info">
经过系统训练后的面试表现展示个人成长和进步体现出更强的职场竞争力和专业素养

View File

@@ -0,0 +1,144 @@
import { useState } from "react";
import { Timeline } from "@arco-design/web-react";
import IconFont from "@/components/IconFont";
import "./index.css";
const TimelineItem = Timeline.Item;
export default ({ onItemSelect }) => {
const [selectedItem, setSelectedItem] = useState("面试初体验");
const handleItemClick = (itemName) => {
setSelectedItem(itemName);
onItemSelect && onItemSelect(itemName);
};
return (
<div className="mock-interview-wrapper">
<p className="mock-interview-title">
<IconFont className="mock-interview-title-icon" src="recuUY5uwYg4km" />
<span>面试模拟</span>
</p>
<ul className="mock-interview-list">
<li className="mock-interview-item">
<p className="mock-interview-item-title">
<i className="mock-interview-item-title-icon" />
<span>懵懂初试</span>
</p>
<Timeline>
<TimelineItem
lineType="dashed"
dot={
<div className={`time-line-dot-icon ${selectedItem === "面试初体验" ? "time-line-dot-icon-active" : ""}`} />
}
>
<div
className={`time-line-item ${selectedItem === "面试初体验" ? "time-line-item-active" : ""}`}
onClick={() => handleItemClick("面试初体验")}
style={{ cursor: "pointer" }}
>
<p>
面试初体验
<span style={{
marginLeft: '8px',
padding: '2px 8px',
background: 'linear-gradient(135deg, #5DADE2 0%, #2874A6 100%)',
borderRadius: '12px',
color: '#ffffff',
fontSize: '12px',
fontWeight: 'bold',
fontStyle: 'italic',
letterSpacing: '1px',
textTransform: 'uppercase',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}>
demo
</span>
</p>
<div className="time-line-item-info">
初次接触面试环境,体验真实面试流程,了解自身在面试中的基本表现和待提升的方面
</div>
</div>
</TimelineItem>
<TimelineItem
lineType="dashed"
dot={<div className={`time-line-dot-icon ${selectedItem === "未来的自己" ? "time-line-dot-icon-active" : ""}`} />}
>
<div
className={`time-line-item ${selectedItem === "未来的自己" ? "time-line-item-active" : ""}`}
onClick={() => handleItemClick("未来的自己")}
style={{ cursor: "pointer" }}
>
<p>
未来的自己
<span style={{
marginLeft: '8px',
padding: '2px 8px',
background: 'linear-gradient(135deg, #5DADE2 0%, #2874A6 100%)',
borderRadius: '12px',
color: '#ffffff',
fontSize: '12px',
fontWeight: 'bold',
fontStyle: 'italic',
letterSpacing: '1px',
textTransform: 'uppercase',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}>
demo
</span>
</p>
<div className="time-line-item-info">
经过系统训练后的面试表现,展示个人成长和进步,体现出更强的职场竞争力和专业素养
</div>
</div>
</TimelineItem>
</Timeline>
</li>
<li className="mock-interview-item">
<p className="mock-interview-item-title">
<i className="mock-interview-item-title-icon" />
<span>有备而战</span>
</p>
<Timeline>
<TimelineItem
lineType="dashed"
dot={<div className={`time-line-dot-icon ${selectedItem === "第一次线下面试模拟" ? "time-line-dot-icon-active" : ""}`} />}
>
<div
className={`time-line-item ${selectedItem === "第一次线下面试模拟" ? "time-line-item-active" : ""}`}
onClick={() => handleItemClick("第一次线下面试模拟")}
style={{ cursor: "pointer" }}
>
<p>第一次线下面试模拟</p>
</div>
</TimelineItem>
<TimelineItem
lineType="dashed"
dot={<div className={`time-line-dot-icon ${selectedItem === "第二次线下面试模拟" ? "time-line-dot-icon-active" : ""}`} />}
>
<div
className={`time-line-item ${selectedItem === "第二次线下面试模拟" ? "time-line-item-active" : ""}`}
onClick={() => handleItemClick("第二次线下面试模拟")}
style={{ cursor: "pointer" }}
>
<p>第二次线下面试模拟</p>
</div>
</TimelineItem>
<TimelineItem
lineType="dashed"
dot={<div className={`time-line-dot-icon ${selectedItem === "第三次线下面试模拟" ? "time-line-dot-icon-active" : ""}`} />}
>
<div
className={`time-line-item ${selectedItem === "第三次线下面试模拟" ? "time-line-item-active" : ""}`}
onClick={() => handleItemClick("第三次线下面试模拟")}
style={{ cursor: "pointer" }}
>
<p>第三次线下面试模拟</p>
</div>
</TimelineItem>
</Timeline>
</li>
</ul>
</div>
);
};

View File

@@ -6,9 +6,9 @@ import "./index.css";
export default ({ locked = true }) => {
return (
<div className="curved-employment-wrapper">
<img src={CHARTIMG} />
<img src={CHARTIMG} style={locked ? { filter: 'blur(10px)' } : {}} />
{locked && (
<Locked text="该板块将在「垂直能力提升」阶段开放完成线上1V1求职策略定制后解锁" />
<Locked text="该板块将在「垂直能力提升」阶段开放完成线上1V1求职策略定制后解锁" showBackground={false} />
)}
</div>
);

View File

@@ -0,0 +1,15 @@
import React from "react";
import CHARTIMG from "@/assets/images/JobStrategyDetailPage/bar_chart.png";
import Locked from "@/components/Locked";
import "./index.css";
export default ({ locked = true }) => {
return (
<div className="curved-employment-wrapper">
<img src={CHARTIMG} />
{locked && (
<Locked text="该板块将在「垂直能力提升」阶段开放完成线上1V1求职策略定制后解锁" />
)}
</div>
);
};

View File

@@ -343,7 +343,7 @@ export default ({ locked = false }) => {
>
<div className="target-position-wrapper">
<div className="target-position-content">
<div className="batch-icon">
<div className="batch-icon" style={locked ? { filter: 'blur(10px)' } : {}}>
<span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
第一批次
<Tooltip content="通过培训后能直接上岗的岗位,入职成功率最高。">
@@ -392,7 +392,7 @@ export default ({ locked = false }) => {
</div>
{/* 第一批次 */}
<div className="batch-content batch-content1" ref={batch1Ref}>
<div className="batch-content batch-content1" ref={batch1Ref} style={locked ? { filter: 'blur(10px)' } : {}}>
<SortableContext
items={batchPositions.batch1.map(p => `batch1-${p}`)}
strategy={horizontalListSortingStrategy}
@@ -409,7 +409,7 @@ export default ({ locked = false }) => {
</div>
{/* 第二批次 */}
<div className="batch-content batch-content2" ref={batch2Ref}>
<div className="batch-content batch-content2" ref={batch2Ref} style={locked ? { filter: 'blur(10px)' } : {}}>
<SortableContext
items={batchPositions.batch2.map(p => `batch2-${p}`)}
strategy={horizontalListSortingStrategy}
@@ -426,7 +426,7 @@ export default ({ locked = false }) => {
</div>
{/* 第三批次 */}
<div className="batch-content batch-content3" ref={batch3Ref}>
<div className="batch-content batch-content3" ref={batch3Ref} style={locked ? { filter: 'blur(10px)' } : {}}>
<SortableContext
items={batchPositions.batch3.map(p => `batch3-${p}`)}
strategy={horizontalListSortingStrategy}
@@ -443,7 +443,7 @@ export default ({ locked = false }) => {
</div>
{locked && (
<Locked text="该板块将在「垂直能力提升」阶段开放完成线上1V1求职策略定制后解锁" />
<Locked text="该板块将在「垂直能力提升」阶段开放完成线上1V1求职策略定制后解锁" showBackground={false} />
)}
</div>
</div>

View File

@@ -0,0 +1,654 @@
import { useEffect, useRef, useState } from "react";
import { Modal, Message, Tooltip } from "@arco-design/web-react";
import { useNavigate, useLocation } from "react-router-dom";
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragOverlay,
} from '@dnd-kit/core';
import {
SortableContext,
sortableKeyboardCoordinates,
horizontalListSortingStrategy,
} from '@dnd-kit/sortable';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import Locked from "@/components/Locked";
import jobLevelData from "@/data/joblevel.json";
import "./index.css";
// 可排序的岗位组件
const SortablePosition = ({ id, position, getPositionAvatar }) => {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
cursor: 'grab',
userSelect: 'none',
WebkitUserSelect: 'none',
};
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
className="avatar-wrapper"
>
<div className="student-avatar">
<img
alt="avatar"
src={getPositionAvatar(position)}
draggable={false}
style={{ userSelect: 'none', WebkitUserSelect: 'none', pointerEvents: 'none' }}
/>
</div>
<span className="student-name" style={{ userSelect: 'none', WebkitUserSelect: 'none' }}>{position}</span>
<div className="position-tooltip">{position}</div>
</div>
);
};
export default ({ locked = false }) => {
const navigate = useNavigate();
const location = useLocation();
const batch1Ref = useRef(null);
const batch2Ref = useRef(null);
const batch3Ref = useRef(null);
const [hasChanges, setHasChanges] = useState(false);
const [showSaveModal, setShowSaveModal] = useState(false);
const [pendingNavigation, setPendingNavigation] = useState(null);
const [activeId, setActiveId] = useState(null);
// 处理拖拽开始
const handleDragStart = (event) => {
const { active } = event;
setActiveId(active.id);
};
// 处理拖拽结束
const handleDragEnd = (event) => {
const { active, over } = event;
setActiveId(null);
if (!over) return;
const activePosition = active.id.split('-').slice(1).join('-');
const activeBatch = active.id.split('-')[0];
// 确定目标批次
let targetBatch = over.id.split('-')[0];
let targetPosition = over.id.split('-').slice(1).join('-');
// 如果目标和源相同,不做任何操作
if (active.id === over.id) return;
setBatchPositions((prev) => {
const newPositions = { ...prev };
// 如果是同一批次内的移动
if (activeBatch === targetBatch) {
const batch = [...newPositions[activeBatch]];
const activeIndex = batch.indexOf(activePosition);
const overIndex = batch.indexOf(targetPosition);
if (activeIndex !== -1 && overIndex !== -1 && activeIndex !== overIndex) {
// 移除原位置
batch.splice(activeIndex, 1);
// 计算新位置索引
const newOverIndex = activeIndex < overIndex ? overIndex - 1 : overIndex;
// 插入到新位置
batch.splice(newOverIndex, 0, activePosition);
newPositions[activeBatch] = batch;
}
} else {
// 跨批次移动
// 从原批次删除
const sourceBatch = [...newPositions[activeBatch]];
const activeIndex = sourceBatch.indexOf(activePosition);
if (activeIndex !== -1) {
sourceBatch.splice(activeIndex, 1);
newPositions[activeBatch] = sourceBatch;
}
// 添加到目标批次
const targetBatchArray = [...newPositions[targetBatch]];
const overIndex = targetBatchArray.indexOf(targetPosition);
if (overIndex !== -1) {
// 插入到特定位置
targetBatchArray.splice(overIndex, 0, activePosition);
} else {
// 如果目标批次为空或找不到目标位置,添加到末尾
targetBatchArray.push(activePosition);
}
newPositions[targetBatch] = targetBatchArray;
}
return newPositions;
});
setHasChanges(true);
};
// 处理拖拽到其他批次 - 仅用于预览,不实际移动
const handleDragOver = (event) => {
// 空函数 - 我们只在dragEnd时处理实际的移动
};
// 监听路由变化
useEffect(() => {
const handleBeforeUnload = (e) => {
if (hasChanges) {
e.preventDefault();
e.returnValue = '';
}
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, [hasChanges]);
// 拦截导航 - 监听所有可能的页面切换
useEffect(() => {
if (!hasChanges) return;
const handleNavigation = (e) => {
// 如果点击的是弹窗内的元素,不拦截
if (e.target.closest('.arco-modal')) {
return;
}
// 检查是否是链接点击
const link = e.target.closest('a') || (e.target.tagName === 'A' ? e.target : null);
const button = e.target.closest('button') || (e.target.tagName === 'BUTTON' ? e.target : null);
// 检查是否是导航相关的元素
if (link || (button && (button.textContent?.includes('返回') || button.onclick))) {
e.preventDefault();
e.stopPropagation();
setShowSaveModal(true);
if (link) {
setPendingNavigation(link.href);
}
}
};
// 监听点击事件(捕获阶段)
document.addEventListener('click', handleNavigation, true);
// 监听浏览器后退/前进
const handlePopState = (e) => {
if (hasChanges) {
e.preventDefault();
setShowSaveModal(true);
}
};
window.addEventListener('popstate', handlePopState);
return () => {
document.removeEventListener('click', handleNavigation, true);
window.removeEventListener('popstate', handlePopState);
};
}, [hasChanges]);
useEffect(() => {
// 添加鼠标滚轮事件监听,实现横向滚动
const handleWheel = (e, ref) => {
if (ref.current && ref.current.contains(e.target)) {
e.preventDefault();
ref.current.scrollLeft += e.deltaY;
}
};
const batch1El = batch1Ref.current;
const batch2El = batch2Ref.current;
const batch3El = batch3Ref.current;
const wheel1Handler = (e) => handleWheel(e, batch1Ref);
const wheel2Handler = (e) => handleWheel(e, batch2Ref);
const wheel3Handler = (e) => handleWheel(e, batch3Ref);
if (batch1El) batch1El.addEventListener('wheel', wheel1Handler, { passive: false });
if (batch2El) batch2El.addEventListener('wheel', wheel2Handler, { passive: false });
if (batch3El) batch3El.addEventListener('wheel', wheel3Handler, { passive: false });
return () => {
if (batch1El) batch1El.removeEventListener('wheel', wheel1Handler);
if (batch2El) batch2El.removeEventListener('wheel', wheel2Handler);
if (batch3El) batch3El.removeEventListener('wheel', wheel3Handler);
};
}, []);
// 根据岗位名称获取头像
const getPositionAvatar = (positionName) => {
const jobData = jobLevelData.data;
for (const [key, levelData] of Object.entries(jobData)) {
const found = levelData.list.find(item => item.position_name === positionName);
if (found) {
return found.img;
}
}
return "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_teacher-avatar/recuUpSO4gUtJz.png"; // 默认头像
};
// 定义三个批次的岗位数据
const initialBatchPositions = {
batch1: [
"二次元周边店店员",
"会展执行助理",
"会展讲解员",
"会展营销",
"商业会展执行专员",
"景区运营专员",
"文旅运营总监助理",
"品牌策划运营专员",
"品牌推广专员",
"ip运营",
"文创产品设计师助理",
"新媒体运营专员",
"网络运营专员",
"社群运营",
"直播助理"
],
batch2: [
"宠物店店长",
"宠物营养师",
"二次元周边选品专员",
"二次元周边店店长",
"会展策划师",
"漫展策划师",
"活动执行",
"活动策划师",
"酒店运营专员",
"餐厅运营经理",
"露营地运营专员",
"旅游规划师",
"文旅项目投资拓展管培生",
"民宿管家",
"民宿客房管家",
"民宿运营专员",
"品牌公关",
"ip运营总监助理",
"品牌公关管培生",
"直播中控",
"SEO专员",
"SEM专员",
"赛事经纪"
],
batch3: [
"酒店餐饮主管",
"客房经理",
"酒店大堂副理",
"旅游计调专员",
"文创产品策划师",
"文创产品设计师",
"赛事礼仪",
"赛事编辑",
"艺人经纪人",
"演出执行经理",
"场馆运营人员"
]
};
const [batchPositions, setBatchPositions] = useState(initialBatchPositions);
// 拖拽传感器设置
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
// 获取所有岗位ID
const getAllPositionIds = () => {
const ids = [];
Object.entries(batchPositions).forEach(([batch, positions]) => {
positions.forEach(position => {
ids.push(`${batch}-${position}`);
});
});
return ids;
};
return (
<div style={{ userSelect: 'none', WebkitUserSelect: 'none', MozUserSelect: 'none', msUserSelect: 'none' }}>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
>
<div className="target-position-wrapper">
<div className="target-position-content">
<div className="batch-icon">
<span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
第一批次
<Tooltip content="通过培训后能直接上岗的岗位,入职成功率最高。">
<span style={{ fontSize: '12px',
color: '#ffffff',
backgroundColor: '#4080ff',
borderRadius: '50%',
width: '16px',
height: '16px',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 'bold' }}>?</span>
</Tooltip>
</span>
<span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
第二批次
<Tooltip content="需积累一定工作经验后可争取的晋升岗位方向。">
<span style={{ fontSize: '12px',
color: '#ffffff',
backgroundColor: '#4080ff',
borderRadius: '50%',
width: '16px',
height: '16px',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 'bold' }}>?</span>
</Tooltip>
</span>
<span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
第三批次
<Tooltip content="需长期经验和能力沉淀,可作为学员的终极职业目标。">
<span style={{ fontSize: '12px',
color: '#ffffff',
backgroundColor: '#4080ff',
borderRadius: '50%',
width: '16px',
height: '16px',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 'bold' }}>?</span>
</Tooltip>
</span>
</div>
{/* 第一批次 */}
<div className="batch-content batch-content1" ref={batch1Ref}>
<SortableContext
items={batchPositions.batch1.map(p => `batch1-${p}`)}
strategy={horizontalListSortingStrategy}
>
{batchPositions.batch1.map((position) => (
<SortablePosition
key={`batch1-${position}`}
id={`batch1-${position}`}
position={position}
getPositionAvatar={getPositionAvatar}
/>
))}
</SortableContext>
</div>
{/* 第二批次 */}
<div className="batch-content batch-content2" ref={batch2Ref}>
<SortableContext
items={batchPositions.batch2.map(p => `batch2-${p}`)}
strategy={horizontalListSortingStrategy}
>
{batchPositions.batch2.map((position) => (
<SortablePosition
key={`batch2-${position}`}
id={`batch2-${position}`}
position={position}
getPositionAvatar={getPositionAvatar}
/>
))}
</SortableContext>
</div>
{/* 第三批次 */}
<div className="batch-content batch-content3" ref={batch3Ref}>
<SortableContext
items={batchPositions.batch3.map(p => `batch3-${p}`)}
strategy={horizontalListSortingStrategy}
>
{batchPositions.batch3.map((position) => (
<SortablePosition
key={`batch3-${position}`}
id={`batch3-${position}`}
position={position}
getPositionAvatar={getPositionAvatar}
/>
))}
</SortableContext>
</div>
{locked && (
<Locked text="该板块将在「垂直能力提升」阶段开放完成线上1V1求职策略定制后解锁" />
)}
</div>
</div>
{/* 拖拽覆盖层 */}
<DragOverlay>
{activeId ? (
<div
style={{
cursor: 'grabbing',
userSelect: 'none',
WebkitUserSelect: 'none',
pointerEvents: 'none'
}}
>
<div
style={{
position: 'relative',
width: '64px',
height: '64px',
borderRadius: '50%',
overflow: 'hidden',
backgroundColor: '#ffffff',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
border: '2px solid #ffffff'
}}
>
<img
alt="avatar"
src={getPositionAvatar(activeId.split('-').slice(1).join('-'))}
draggable={false}
style={{
userSelect: 'none',
WebkitUserSelect: 'none',
pointerEvents: 'none',
width: '100%',
height: '100%',
objectFit: 'cover'
}}
/>
</div>
</div>
) : null}
</DragOverlay>
</DndContext>
{/* 保存提示模态框 */}
<Modal
title={
<div style={{
fontSize: '18px',
fontWeight: '600',
color: '#1d2129',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M10 0C4.48 0 0 4.48 0 10s4.48 10 10 10 10-4.48 10-10S15.52 0 10 0zm1 15h-2v-2h2v2zm0-4h-2V5h2v6z" fill="#2c7fff"/>
</svg>
保存更改
</div>
}
visible={showSaveModal}
onCancel={() => {
setShowSaveModal(false);
setPendingNavigation(null);
// 只关闭弹窗保留hasChanges状态下次点击返回还会弹出
}}
footer={[
<button
key="cancel"
className="arco-btn arco-btn-secondary"
onClick={() => {
setShowSaveModal(false);
setHasChanges(false);
setPendingNavigation(null);
navigate('/job-strategy'); // 返回定制求职策略页面
}}
style={{
padding: '8px 20px',
fontSize: '14px',
fontWeight: '500',
borderRadius: '6px',
border: '1px solid #e5e6eb',
backgroundColor: '#ffffff',
color: '#4e5969',
cursor: 'pointer',
transition: 'all 0.3s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = '#f7f8fa';
e.currentTarget.style.borderColor = '#c9cdd4';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = '#ffffff';
e.currentTarget.style.borderColor = '#e5e6eb';
}}
>
放弃更改
</button>,
<div
key="save-wrapper"
style={{
position: 'relative',
display: 'inline-block'
}}
onMouseEnter={(e) => {
const tooltip = document.createElement('div');
tooltip.className = 'save-tooltip';
tooltip.textContent = '非导师和学生本人无修改权限';
tooltip.style.cssText = `
position: absolute;
bottom: 120%;
left: 50%;
transform: translateX(-50%);
background: linear-gradient(135deg, #1d2129 0%, #2e3440 100%);
color: #ffffff;
padding: 10px 16px;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
white-space: nowrap;
z-index: 10000;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
animation: fadeIn 0.3s ease;
`;
const style = document.createElement('style');
style.textContent = `
@keyframes fadeIn {
from { opacity: 0; transform: translateX(-50%) translateY(5px); }
to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
`;
document.head.appendChild(style);
// 添加小箭头
const arrow = document.createElement('div');
arrow.style.cssText = `
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-style: solid;
border-width: 6px 6px 0 6px;
border-color: #2e3440 transparent transparent transparent;
`;
tooltip.appendChild(arrow);
e.currentTarget.appendChild(tooltip);
}}
onMouseLeave={(e) => {
const tooltip = e.currentTarget.querySelector('.save-tooltip');
if (tooltip) {
tooltip.remove();
}
const style = document.querySelector('style');
if (style && style.textContent.includes('fadeIn')) {
style.remove();
}
}}
>
<button
className="arco-btn arco-btn-primary"
disabled
style={{
padding: '8px 24px',
fontSize: '14px',
fontWeight: '500',
borderRadius: '6px',
backgroundColor: '#e5e6eb',
color: '#86909c',
cursor: 'not-allowed',
border: 'none',
opacity: '0.6',
transition: 'all 0.3s ease'
}}
>
保存更改
</button>
</div>
]}
style={{
borderRadius: '12px'
}}
>
<div style={{
display: 'flex',
alignItems: 'flex-start',
gap: '12px'
}}>
<svg width="18" height="18" viewBox="0 0 20 20" fill="none" style={{ marginTop: '2px', flexShrink: 0 }}>
<path d="M9 13h2v2H9v-2zm0-8h2v6H9V5zm.99-5C4.47 0 0 4.48 0 10s4.47 10 9.99 10C15.52 20 20 15.52 20 10S15.52 0 9.99 0zM10 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" fill="#ff7d00"/>
</svg>
<div>
<p style={{ margin: 0, fontWeight: '500', color: '#1d2129', marginBottom: '8px' }}>
您对岗位顺序进行了修改
</p>
<p style={{ margin: 0, fontSize: '13px', color: '#86909c' }}>
离开此页面前,是否要保存您的更改?未保存的更改将会丢失。
</p>
</div>
</div>
</Modal>
</div>
);
};

View File

@@ -54,8 +54,8 @@ const JobStrategyDetailPage = () => {
{/* 内容区域 */}
<div className="job-strategy-detail-content">
{activeItem === "1" && <TargetPosition locked={false} />}
{activeItem === "2" && <CurvedEmployment locked={false} />}
{activeItem === "1" && <TargetPosition locked={true} />}
{activeItem === "2" && <CurvedEmployment locked={true} />}
</div>
</div>
</div>

View File

@@ -6,8 +6,7 @@ const JobStrategyPage = () => {
return (
<div className="job-strategy-page">
<CoursesVideoPlayer
isLock
backgroundImage="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/public_bg/recuWolCCOlOCz.jpg"
isLock
/>
<LiveSummary showBtn />
</div>

View File

@@ -0,0 +1,17 @@
import CoursesVideoPlayer from "@/components/CoursesVideoPlayer";
import LiveSummary from "@/components/LiveSummary";
import "./index.css";
const JobStrategyPage = () => {
return (
<div className="job-strategy-page">
<CoursesVideoPlayer
isLock
backgroundImage="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/public_bg/recuWolCCOlOCz.jpg"
/>
<LiveSummary showBtn />
</div>
);
};
export default JobStrategyPage;