feat: 添加HR访问量弹窗和日历事项样式优化

- 新增HR访问详情弹窗组件,支持左右切换查看不同HR信息
- 优化日历事项样式系统,基于事件类型匹配样式配置
- 完善侧边栏HR访问量组件,添加重叠头像和点击交互
- 增加班级排名弹窗组件
- 更新专家支持页面布局和样式

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
KQL
2025-09-11 18:40:40 +08:00
parent c969677ef6
commit 4f64941d85
32 changed files with 7335 additions and 1722 deletions

View File

@@ -13,6 +13,12 @@
justify-content: flex-start;
position: relative;
overflow: hidden;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
transform: translateY(-2px);
}
&::after {
content: "";

View File

@@ -1,11 +1,14 @@
import { useState } from "react";
import { Avatar, Spin, Empty } from "@arco-design/web-react";
import IconFont from "@/components/IconFont";
import ClassRankModal from "@/components/ClassRankModal";
import "./index.css";
const positions = ["item2", "item1", "item3"];
const icons = ["icon2", "icon1", "icon3"];
const Rank = ({ className, data, loading }) => {
const [modalVisible, setModalVisible] = useState(false);
const rankings = data?.rankings?.slice(0, 6) || [];
// 安全处理领奖台学生确保至少有3个位置
@@ -17,8 +20,17 @@ const Rank = ({ className, data, loading }) => {
const listStudents = rankings.slice(3);
const handleClick = () => {
setModalVisible(true);
};
return (
<div className={`module-class-rank ${className}`}>
<>
<div
className={`module-class-rank ${className}`}
onClick={handleClick}
style={{ cursor: "pointer" }}
>
<p className="module-class-rank-title">
<IconFont className="title-icon" src="recuUY5nNf7DWT" />
<span>班级排名</span>
@@ -74,7 +86,14 @@ const Rank = ({ className, data, loading }) => {
</ul>
</>
)}
</div>
</div>
{/* 班级排名弹窗 */}
<ClassRankModal
visible={modalVisible}
onClose={() => setModalVisible(false)}
/>
</>
);
};

View File

@@ -0,0 +1,273 @@
/* 班级排名弹窗样式 */
.class-rank-modal {
width: 600px;
max-height: 80vh;
background-color: #fff7f1;
border-radius: 16px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.class-rank-modal-header {
padding: 20px 30px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
display: flex;
justify-content: space-between;
align-items: center;
background: linear-gradient(135deg, #fff7f1 0%, #ffe8d6 100%);
position: relative;
&::after {
content: "";
width: 180px;
height: 110px;
position: absolute;
right: 0;
top: 0;
background-image: url("@/assets/images/Rank/bg.png");
background-size: 100% 100%;
opacity: 0.5;
}
}
.class-rank-modal-title {
font-size: 20px;
font-weight: 600;
color: #262626;
margin: 0;
display: flex;
align-items: center;
position: relative;
z-index: 1;
span {
display: flex;
align-items: center;
}
}
.close-icon {
width: 24px;
height: 24px;
background-image: url("@/assets/images/icon/close.png");
background-size: 100% 100%;
cursor: pointer;
position: relative;
z-index: 1;
transition: opacity 0.3s;
&:hover {
opacity: 0.8;
}
}
.class-rank-modal-content {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
/* 自定义滚动条样式 */
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.04);
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.12);
border-radius: 3px;
&:hover {
background: rgba(0, 0, 0, 0.2);
}
}
}
/* 复用班级排名板块的领奖台样式 */
.class-rank-modal-content .module-class-rank-podium {
width: 333px;
height: 138px;
margin: 20px auto 20px;
display: flex;
justify-content: space-between;
align-items: flex-end;
> li {
width: 88px;
border-radius: 8px;
position: relative;
background-image: linear-gradient(
to bottom,
rgba(255, 255, 255, 1),
rgba(255, 255, 255, 0)
);
.module-class-rank-podium-avatar {
position: absolute;
left: 50%;
top: -24px;
transform: translateX(-50%);
width: 48px;
height: 48px;
border: 1px solid;
border-radius: 50%;
position: relative;
&::before {
content: "";
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: -10px;
width: 57px;
height: 16px;
background-size: 100% 100%;
}
}
.module-class-rank-podium-name {
color: #1d2129;
font-size: 14px;
position: absolute;
left: 50%;
top: 30px;
transform: translateX(-50%);
z-index: 10;
}
> i {
height: 27px;
background-size: 100% 100%;
position: absolute;
left: 50%;
bottom: 0;
transform: translateX(-50%);
}
}
.module-class-rank-podium-item1 {
height: 98px;
&::after {
content: "";
position: absolute;
left: 50%;
transform: translateX(-50%);
top: -40px;
width: 20px;
height: 20px;
background-image: url("@/assets/images/Rank/first_icon.png");
background-size: 100% 100%;
}
.module-class-rank-podium-avatar {
border-color: #ffc15b;
&::before {
background-image: url("@/assets/images/Rank/icon1.png");
}
}
}
.module-class-rank-podium-item2 {
height: 80px;
&::after {
content: "";
position: absolute;
left: 50%;
transform: translateX(-50%);
top: -40px;
width: 20px;
height: 20px;
background-image: url("@/assets/images/Rank/second_icon.png");
background-size: 100% 100%;
}
.module-class-rank-podium-avatar {
border-color: #9ab9e3;
&::before {
background-image: url("@/assets/images/Rank/icon2.png");
}
}
}
.module-class-rank-podium-item3 {
height: 70px;
&::after {
content: "";
position: absolute;
left: 50%;
transform: translateX(-50%);
top: -40px;
width: 20px;
height: 20px;
background-image: url("@/assets/images/Rank/third_icon.png");
background-size: 100% 100%;
}
.module-class-rank-podium-avatar {
border-color: #d7a770;
&::before {
background-image: url("@/assets/images/Rank/icon3.png");
}
}
}
}
/* 其余排名列表 */
.class-rank-modal-list {
width: 100%;
max-width: 500px;
display: flex;
flex-direction: column;
gap: 8px;
.class-rank-modal-list-item {
width: 100%;
height: 64px;
display: flex;
justify-content: flex-start;
align-items: center;
background-color: #fff;
border-radius: 8px;
box-sizing: border-box;
padding: 0 20px;
position: relative;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transform: translateY(-2px);
}
> em {
margin-left: 5px;
color: #86909c;
font-size: 22px;
font-family: "HarmonyOS_Sans_TC_Bold";
font-style: normal;
min-width: 40px;
}
> p {
font-size: 16px;
font-weight: 500;
line-height: 24px;
color: #1d2129;
margin-left: 20px;
text-align: left;
flex: 1;
}
> span {
font-size: 15px;
font-weight: 400;
color: #86909c;
}
}
}

View File

@@ -0,0 +1,108 @@
import { useState, useEffect } from "react";
import { Avatar } from "@arco-design/web-react";
import Modal from "@/components/Modal";
import classRankData from "../../../网页未导入数据/文旅产业/班级排名.json";
import "./index.css";
const positions = ["item2", "item1", "item3"];
const icons = ["icon2", "icon1", "icon3"];
const ClassRankModal = ({ visible, onClose }) => {
const [rankings, setRankings] = useState([]);
useEffect(() => {
// 处理班级排名数据,确保按排名排序
const sortedData = [...classRankData].sort(
(a, b) => parseInt(a.班级排名) - parseInt(b.班级排名)
);
setRankings(sortedData);
}, []);
// 获取前三名的数据,按照领奖台顺序排列
const podiumStudents = [
rankings[1] || null, // 第2名
rankings[0] || null, // 第1名
rankings[2] || null, // 第3名
];
// 获取第4名及以后的数据
const restRankings = rankings.slice(3);
return (
<Modal visible={visible} onClose={onClose}>
<div className="class-rank-modal">
<div className="class-rank-modal-header">
<h2 className="class-rank-modal-title">
<span>班级排名</span>
</h2>
<i className="close-icon" onClick={onClose} />
</div>
<div className="class-rank-modal-content">
{/* 前三名展示区 - 复用ClassRank组件的样式 */}
<ul className="module-class-rank-podium">
{podiumStudents.map((student, index) => {
// 使用具体的头像URL
let avatarUrl = null;
if (student) {
const name = student.学员名称.trim();
switch(name) {
case "万圆":
avatarUrl = "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/avatar/douyin/02393125baa474d558c484c0677664b1.jpg";
break;
case "李阳":
avatarUrl = "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/avatar/douyin/07a0a14c8c8d5476b2c8d54de12e6a06.jpg";
break;
case "何晓彤":
avatarUrl = "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/avatar/douyin/13823046201f0ef17517fb46da12bc35.jpg";
break;
default:
avatarUrl = "https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp";
}
}
return student ? (
<li
key={student.班级排名}
className={`module-class-rank-podium-${positions[index]}`}
>
<Avatar className="module-class-rank-podium-avatar">
<img alt="avatar" src={avatarUrl} />
</Avatar>
<span className="module-class-rank-podium-name">
{student.学员名称.trim()}
</span>
<i className={`module-class-rank-podium-${icons[index]}`}></i>
</li>
) : (
<li
key={`empty-${index}`}
className={`module-class-rank-podium-${positions[index]} empty`}
>
<div className="module-class-rank-podium-placeholder">
<span>-</span>
</div>
</li>
);
})}
</ul>
{/* 其余排名列表 */}
<div className="class-rank-modal-list">
{restRankings.map((item) => (
<div
key={item.班级排名}
className="class-rank-modal-list-item"
>
<em>{item.班级排名}</em>
<p>{item.学员名称.trim()}</p>
<span>{item.学分}学分</span>
</div>
))}
</div>
</div>
</div>
</Modal>
);
};
export default ClassRankModal;

View File

@@ -0,0 +1,202 @@
.hr-visit-modal .arco-modal {
border-radius: 16px;
overflow: hidden;
}
.hr-visit-modal .arco-modal-content {
padding: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
position: relative;
}
.hr-visit-modal-content {
padding: 40px 30px 30px;
text-align: center;
position: relative;
background: linear-gradient(135deg, #a8e6cf 0%, #88c999 50%, #67b26f 100%);
min-height: 400px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.modal-close-btn {
position: absolute;
top: 16px;
right: 16px;
width: 32px;
height: 32px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: white;
font-size: 16px;
transition: background 0.3s ease;
z-index: 20;
}
.modal-close-btn:hover {
background: rgba(255, 255, 255, 0.3);
}
.hr-avatars-section {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
}
.avatar-navigation {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
position: relative;
}
.nav-btn {
background: rgba(255, 255, 255, 0.2);
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
color: white;
cursor: pointer;
transition: all 0.3s ease;
font-size: 16px;
z-index: 15;
}
.nav-btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.1);
}
.avatars-container {
position: relative;
width: 200px;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 20px;
overflow: hidden;
}
.avatar-item {
position: absolute;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.avatar-item.hidden {
opacity: 0 !important;
}
.hr-avatar-large {
width: 80px;
height: 80px;
border-radius: 50%;
border: 4px solid rgba(255, 255, 255, 0.8);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
transition: all 0.4s ease;
}
.avatar-item.active .hr-avatar-large {
border-color: white;
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.3);
}
.company-info {
margin-bottom: 20px;
}
.company-name {
color: white;
font-size: 24px;
font-weight: 600;
margin: 0 0 12px 0;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.hr-tag {
display: inline-flex;
align-items: center;
background: rgba(255, 255, 255, 0.2);
border-radius: 20px;
padding: 6px 16px;
backdrop-filter: blur(10px);
}
.hr-label {
background: #007AFF;
color: white;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
margin-right: 8px;
}
.hr-name {
color: white;
font-size: 16px;
font-weight: 500;
}
.visit-info {
margin-bottom: 30px;
}
.visit-message {
color: rgba(255, 255, 255, 0.9);
font-size: 16px;
margin: 0;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.indicators {
display: flex;
justify-content: center;
gap: 8px;
}
.indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.4);
cursor: pointer;
transition: all 0.3s ease;
}
.indicator.active {
background: white;
transform: scale(1.2);
}
.indicator:hover {
background: rgba(255, 255, 255, 0.7);
}
/* 动画效果 */
@keyframes modalSlideIn {
from {
opacity: 0;
transform: translateY(-20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.hr-visit-modal .arco-modal-content {
animation: modalSlideIn 0.4s ease-out;
}

View File

@@ -0,0 +1,134 @@
import React, { useState } from 'react';
import { Modal } from '@arco-design/web-react';
import { IconLeft, IconRight, IconClose } from '@arco-design/web-react/icon';
import './index.css';
const HRVisitModal = ({ visible, onClose }) => {
const [currentIndex, setCurrentIndex] = useState(0);
// 模拟HR数据
const hrData = [
{
id: 1,
name: '王先生',
company: '武汉联影科技有限公司',
avatar: '//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp',
visitMessage: '访问了您的个人档案'
},
{
id: 2,
name: '李女士',
company: '腾讯科技有限公司',
avatar: '//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/581b17753093199839f2e327e726b157.svg~tplv-49unhts6dw-image.image',
visitMessage: '访问了您的项目经验'
},
{
id: 3,
name: '张先生',
company: '阿里巴巴集团',
avatar: '//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/e278888093bef8910e829486fb45dd69.png~tplv-uwbnlip3yd-webp.webp',
visitMessage: '访问了您的技能证书'
},
{
id: 4,
name: '陈女士',
company: '字节跳动科技有限公司',
avatar: '//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp',
visitMessage: '访问了您的求职意向'
},
{
id: 5,
name: '刘先生',
company: '华为技术有限公司',
avatar: '//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/581b17753093199839f2e327e726b157.svg~tplv-49unhts6dw-image.image',
visitMessage: '访问了您的工作经历'
}
];
const handlePrev = () => {
setCurrentIndex(prev => prev === 0 ? hrData.length - 1 : prev - 1);
};
const handleNext = () => {
setCurrentIndex(prev => prev === hrData.length - 1 ? 0 : prev + 1);
};
const currentHR = hrData[currentIndex];
return (
<Modal
visible={visible}
onCancel={onClose}
footer={null}
closable={false}
className="hr-visit-modal"
width={500}
>
<div className="hr-visit-modal-content">
{/* 关闭按钮 */}
<div className="modal-close-btn" onClick={onClose}>
<IconClose />
</div>
{/* 头像区域 */}
<div className="hr-avatars-section">
<div className="avatar-navigation">
<button className="nav-btn" onClick={handlePrev}>
<IconLeft />
</button>
<div className="avatars-container">
{hrData.map((hr, index) => (
<div
key={hr.id}
className={`avatar-item ${index === currentIndex ? 'active' : ''} ${
Math.abs(index - currentIndex) > 2 ? 'hidden' : ''
}`}
style={{
transform: `translateX(${(index - currentIndex) * 80}px)`,
opacity: index === currentIndex ? 1 : 0.6,
zIndex: index === currentIndex ? 10 : 5,
scale: index === currentIndex ? 1.2 : 1
}}
>
<img src={hr.avatar} alt={hr.name} className="hr-avatar-large" />
</div>
))}
</div>
<button className="nav-btn" onClick={handleNext}>
<IconRight />
</button>
</div>
</div>
{/* 公司信息 */}
<div className="company-info">
<h3 className="company-name">{currentHR.company}</h3>
<div className="hr-tag">
<span className="hr-label">HR</span>
<span className="hr-name">{currentHR.name}</span>
</div>
</div>
{/* 访问信息 */}
<div className="visit-info">
<p className="visit-message">{currentHR.visitMessage}</p>
</div>
{/* 指示器 */}
<div className="indicators">
{hrData.map((_, index) => (
<div
key={index}
className={`indicator ${index === currentIndex ? 'active' : ''}`}
onClick={() => setCurrentIndex(index)}
/>
))}
</div>
</div>
</Modal>
);
};
export default HRVisitModal;

View File

@@ -86,13 +86,22 @@
}
.visitor-count {
width: 208px;
height: 44px;
height: 64px;
border-radius: 8px;
background-color: #e5f1ff;
position: relative;
margin-top: 10px;
box-sizing: border-box;
padding: 0 10px;
padding: 0 12px;
display: flex;
align-items: center;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
&::after {
content: "";
@@ -103,12 +112,63 @@
height: 100%;
background-image: url("@/assets/images/Sidebar/visitor_count_bg.png");
background-size: 100% 100%;
z-index: 1;
}
.arco-statistic-value {
font-size: 12px;
font-weight: 700;
line-height: 41px;
.hr-visitor-content {
position: relative;
z-index: 2;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
.hr-visitor-text {
font-size: 12px;
font-weight: 700;
color: #1d2129;
}
.hr-avatars-wrapper {
display: flex;
align-items: center;
gap: 4px;
.hr-avatars {
display: flex;
position: relative;
.hr-avatar {
width: 24px;
height: 24px;
border-radius: 50%;
border: 2px solid #fff;
box-sizing: border-box;
position: relative;
&.hr-avatar-1 {
z-index: 3;
}
&.hr-avatar-2 {
z-index: 2;
margin-left: -8px;
}
&.hr-avatar-3 {
z-index: 1;
margin-left: -8px;
}
}
}
.hr-count-text {
font-size: 12px;
font-weight: 400;
color: #4e5969;
white-space: nowrap;
}
}
}
}
.sidebar-menu {

View File

@@ -1,7 +1,8 @@
import { useState } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import { Statistic } from "@arco-design/web-react";
import { useSelector } from "react-redux";
import IconFont from "@/components/IconFont";
import HRVisitModal from "@/components/HRVisitModal";
import ICON from "@/assets/images/Sidebar/sidebar_icon.png";
import ICONRETRACT from "@/assets/images/Sidebar/logo.png";
import BTNICON from "@/assets/images/Sidebar/btn_icon.png";
@@ -12,6 +13,7 @@ const Sidebar = ({ isCollapsed, setIsCollapsed }) => {
const navigate = useNavigate();
const location = useLocation();
const studentInfo = useSelector((state) => state.student.studentInfo);
const [hrModalVisible, setHrModalVisible] = useState(false);
const handleNavClick = (path) => {
navigate(path);
};
@@ -21,6 +23,16 @@ const Sidebar = ({ isCollapsed, setIsCollapsed }) => {
setIsCollapsed((prev) => !prev);
};
// 打开HR访问详情弹窗
const handleHRClick = () => {
setHrModalVisible(true);
};
// 关闭HR访问详情弹窗
const handleCloseHRModal = () => {
setHrModalVisible(false);
};
return (
<div
className={`${
@@ -45,12 +57,31 @@ const Sidebar = ({ isCollapsed, setIsCollapsed }) => {
</div>
)}
</div>
<Statistic
className="visitor-count"
groupSeparator
value={87}
prefix="HR访问量"
/>
<div className="visitor-count" onClick={handleHRClick}>
<div className="hr-visitor-content">
<span className="hr-visitor-text">HR访问量</span>
<div className="hr-avatars-wrapper">
<div className="hr-avatars">
<img
src="//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp"
alt="HR头像"
className="hr-avatar hr-avatar-1"
/>
<img
src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/581b17753093199839f2e327e726b157.svg~tplv-49unhts6dw-image.image"
alt="HR头像"
className="hr-avatar hr-avatar-2"
/>
<img
src="//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/e278888093bef8910e829486fb45dd69.png~tplv-uwbnlip3yd-webp.webp"
alt="HR头像"
className="hr-avatar hr-avatar-3"
/>
</div>
<span className="hr-count-text">等87位HR</span>
</div>
</div>
</div>
<ul className="sidebar-menu">
{routes
.filter((item) => item.showMenu)
@@ -90,6 +121,12 @@ const Sidebar = ({ isCollapsed, setIsCollapsed }) => {
<div className="sidebar-btn" onClick={toggleSidebar}>
<img src={BTNICON} alt="btn" className="sidebar-btn-icon" />
</div>
{/* HR访问详情弹窗 */}
<HRVisitModal
visible={hrModalVisible}
onClose={handleCloseHRModal}
/>
</div>
);
};