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

@@ -1,127 +1,145 @@
import { getMonthDays } from "@/data/mockData";
const MonthView = ({
currentDate,
events,
onDateClick,
onEventClick,
selectedDate,
}) => {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
const days = getMonthDays(year, month);
const weekDays = ["日", "一", "二", "三", "四", "五", "六"];
// 获取指定日期的事件
const getEventsForDate = (date, month, year) => {
if (!events || events.length === 0) return [];
const dateString = `${year}-${(month + 1)
.toString()
.padStart(2, "0")}-${date.toString().padStart(2, "0")}`;
return events.filter((event) => {
const eventDate = event.startTime.split(" ")[0];
return eventDate === dateString;
});
};
const handleDateClick = (day, dayEvents) => {
// 所有日期都可以点击
if (onDateClick) {
const clickedDate = new Date(day.year, day.month, day.date);
onDateClick(clickedDate, dayEvents || []);
}
};
const isSelected = (day) => {
if (!selectedDate) return false;
return (
selectedDate.getFullYear() === day.year &&
selectedDate.getMonth() === day.month &&
selectedDate.getDate() === day.date
);
};
const renderEventItem = (event, index, dayEvents) => {
const maxVisible = 3; // 每个日期最多显示3个事件
if (
index >= maxVisible - 1 &&
index === maxVisible - 1 &&
dayEvents.length > maxVisible
) {
// 显示"更多"指示器
const remainingCount = dayEvents.length - maxVisible + 1;
return (
<div key={`more-${index}`} className="event-more">
+{remainingCount}更多
</div>
);
}
if (index >= maxVisible) return null;
return (
<div
key={event.id}
className={`event-item ${event.type}`}
title={`${event.title} (${event.startTime.split(" ")[1]} - ${
event.endTime.split(" ")[1]
})`}
onClick={(e) => {
e.stopPropagation();
if (onEventClick) {
onEventClick(event);
} else {
}
}}
>
{event.title}
</div>
);
};
return (
<div className="month-view">
{/* 星期标题 */}
<div className="month-header">
{weekDays.map((day) => (
<div key={day} className="weekday-header">
{day}
</div>
))}
</div>
{/* 日期网格 */}
<div className="month-grid">
{days.map((day, index) => {
const dayEvents = getEventsForDate(day.date, day.month, day.year);
const isToday = day.isToday;
const isCurrentMonth = day.isCurrentMonth;
const isSelectedDate = isSelected(day);
return (
<div
key={index}
className={`day-cell ${!isCurrentMonth ? "other-month" : ""} ${
isToday ? "today" : ""
} ${isSelectedDate ? "selected" : ""}`}
onClick={() => handleDateClick(day, dayEvents)}
>
<div className="day-number">{day.date}</div>
<div className="event-list">
{dayEvents.map((event, eventIndex) =>
renderEventItem(event, eventIndex, dayEvents)
)}
</div>
</div>
);
})}
</div>
</div>
);
};
export default MonthView;
import { getMonthDays } from "@/data/mockData";
import { getEventStyleByType } from "@/utils/calendarEventStyles";
const MonthView = ({
currentDate,
events,
onDateClick,
onEventClick,
selectedDate,
}) => {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
const days = getMonthDays(year, month);
const weekDays = ["日", "一", "二", "三", "四", "五", "六"];
// 获取指定日期的事件
const getEventsForDate = (date, month, year) => {
if (!events || events.length === 0) return [];
const dateString = `${year}-${(month + 1)
.toString()
.padStart(2, "0")}-${date.toString().padStart(2, "0")}`;
return events.filter((event) => {
const eventDate = event.startTime.split(" ")[0];
return eventDate === dateString;
});
};
const handleDateClick = (day, dayEvents) => {
// 所有日期都可以点击
if (onDateClick) {
const clickedDate = new Date(day.year, day.month, day.date);
onDateClick(clickedDate, dayEvents || []);
}
};
const isSelected = (day) => {
if (!selectedDate) return false;
return (
selectedDate.getFullYear() === day.year &&
selectedDate.getMonth() === day.month &&
selectedDate.getDate() === day.date
);
};
const renderEventItem = (event, index, dayEvents) => {
const maxVisible = 3; // 每个日期最多显示3个事件
if (
index >= maxVisible - 1 &&
index === maxVisible - 1 &&
dayEvents.length > maxVisible
) {
// 显示"更多"指示器
const remainingCount = dayEvents.length - maxVisible + 1;
return (
<div key={`more-${index}`} className="event-more">
+{remainingCount}更多
</div>
);
}
if (index >= maxVisible) return null;
// 获取事项样式
const eventStyle = getEventStyleByType(event.type);
return (
<div
key={event.id}
className="event-item-new"
title={`${event.title} (${event.startTime.split(" ")[1]} - ${
event.endTime.split(" ")[1]
})`}
style={{
backgroundColor: eventStyle.backgroundColor,
color: eventStyle.textColor,
}}
onClick={(e) => {
e.stopPropagation();
if (onEventClick) {
onEventClick(event);
} else {
}
}}
>
<div className="event-content">
{eventStyle.icon && (
<img
src={eventStyle.icon}
alt=""
className="event-icon"
style={{ width: '16px', height: '16px', marginRight: '4px' }}
/>
)}
<span className="event-title">{event.title}</span>
</div>
</div>
);
};
return (
<div className="month-view">
{/* 星期标题 */}
<div className="month-header">
{weekDays.map((day) => (
<div key={day} className="weekday-header">
{day}
</div>
))}
</div>
{/* 日期网格 */}
<div className="month-grid">
{days.map((day, index) => {
const dayEvents = getEventsForDate(day.date, day.month, day.year);
const isToday = day.isToday;
const isCurrentMonth = day.isCurrentMonth;
const isSelectedDate = isSelected(day);
return (
<div
key={index}
className={`day-cell ${!isCurrentMonth ? "other-month" : ""} ${
isToday ? "today" : ""
} ${isSelectedDate ? "selected" : ""}`}
onClick={() => handleDateClick(day, dayEvents)}
>
<div className="day-number">{day.date}</div>
<div className="event-list">
{dayEvents.map((event, eventIndex) =>
renderEventItem(event, eventIndex, dayEvents)
)}
</div>
</div>
);
})}
</div>
</div>
);
};
export default MonthView;

View File

@@ -1,165 +1,181 @@
import { useEffect, useRef } from "react";
import { getWeekDays } from "@/data/mockData";
const WeekView = ({ currentDate, events, onDateClick, onEventClick }) => {
const containerRef = useRef(null);
const weekDays = getWeekDays(currentDate);
const timeSlots = Array.from(
{ length: 24 },
(_, i) => `${i.toString().padStart(2, "0")}:00`
);
const weekDayNames = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
// 滚动到当前时间
useEffect(() => {
const now = new Date();
const currentHour = now.getHours();
const scrollTop = currentHour * 60; // 每小时60px
if (containerRef.current) {
containerRef.current.scrollTop = Math.max(0, scrollTop - 120); // 提前2小时显示
}
}, []);
// 获取指定日期的事件
const getEventsForDate = (date) => {
if (!events || events.length === 0) return [];
const dateString = `${date.getFullYear()}-${(date.getMonth() + 1)
.toString()
.padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`;
return events.filter((event) => {
const eventDate = event.startTime.split(" ")[0];
return eventDate === dateString;
});
};
// 计算事件在时间轴上的位置和高度
const calculateEventStyle = (event) => {
const startTime = event.startTime.split(" ")[1];
const endTime = event.endTime.split(" ")[1];
const startHour = parseInt(startTime.split(":")[0]);
const startMinute = parseInt(startTime.split(":")[1]);
const endHour = parseInt(endTime.split(":")[0]);
const endMinute = parseInt(endTime.split(":")[1]);
const startOffset = startHour * 60 + startMinute; // 转换为分钟
const endOffset = endHour * 60 + endMinute;
const duration = endOffset - startOffset;
const top = startOffset; // 1分钟 = 1px
const height = Math.max(duration, 30); // 最小高度30px
return {
top: `${top}px`,
height: `${height}px`,
};
};
const handleDateClick = (date) => {
if (onDateClick) {
onDateClick(date);
}
};
const getCurrentTimeLine = () => {
const now = new Date();
const currentHour = now.getHours();
const currentMinute = now.getMinutes();
const totalMinutes = currentHour * 60 + currentMinute;
// 只在今天显示当前时间线
const isToday = weekDays.some(
(date) => date.toDateString() === now.toDateString()
);
if (!isToday) return null;
return (
<div className="current-time-line" style={{ top: `${totalMinutes}px` }} />
);
};
return (
<div className="week-view">
{/* 周标题 */}
<div className="week-header">
<div className="time-header">时间</div>
{weekDays.map((date, index) => {
const isToday = date.toDateString() === new Date().toDateString();
return (
<div
key={date.toISOString()}
className={`day-header ${isToday ? "today" : ""}`}
onClick={() => handleDateClick(date)}
>
<div className="day-name">{weekDayNames[index]}</div>
<div className="day-date">{date.getDate()}</div>
</div>
);
})}
</div>
{/* 周网格 */}
<div className="week-grid" ref={containerRef}>
{/* 时间列 */}
<div className="time-column">
{timeSlots.map((time) => (
<div key={time} className="time-slot">
{time}
</div>
))}
</div>
{/* 日期列 */}
{weekDays.map((date) => {
const dayEvents = getEventsForDate(date);
return (
<div key={date.toISOString()} className="day-column">
{/* 小时格子 */}
{timeSlots.map((time) => (
<div key={time} className="hour-slot" />
))}
{/* 事件块 */}
{dayEvents.map((event) => {
const style = calculateEventStyle(event);
return (
<div
key={event.id}
className={`event-block ${event.type}`}
style={style}
title={event.description}
onClick={(e) => {
e.stopPropagation();
if (onEventClick) {
onEventClick(event);
} else {
}
}}
>
<div className="event-title">{event.title}</div>
<div className="event-time">
{event.startTime.split(" ")[1]} -{" "}
{event.endTime.split(" ")[1]}
</div>
</div>
);
})}
</div>
);
})}
{/* 当前时间线 */}
{getCurrentTimeLine()}
</div>
</div>
);
};
export default WeekView;
import { useEffect, useRef } from "react";
import { getWeekDays } from "@/data/mockData";
import { getEventStyleByType } from "@/utils/calendarEventStyles";
const WeekView = ({ currentDate, events, onDateClick, onEventClick }) => {
const containerRef = useRef(null);
const weekDays = getWeekDays(currentDate);
const timeSlots = Array.from(
{ length: 24 },
(_, i) => `${i.toString().padStart(2, "0")}:00`
);
const weekDayNames = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
// 滚动到当前时间
useEffect(() => {
const now = new Date();
const currentHour = now.getHours();
const scrollTop = currentHour * 60; // 每小时60px
if (containerRef.current) {
containerRef.current.scrollTop = Math.max(0, scrollTop - 120); // 提前2小时显示
}
}, []);
// 获取指定日期的事件
const getEventsForDate = (date) => {
if (!events || events.length === 0) return [];
const dateString = `${date.getFullYear()}-${(date.getMonth() + 1)
.toString()
.padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`;
return events.filter((event) => {
const eventDate = event.startTime.split(" ")[0];
return eventDate === dateString;
});
};
// 计算事件在时间轴上的位置和高度
const calculateEventStyle = (event) => {
const startTime = event.startTime.split(" ")[1];
const endTime = event.endTime.split(" ")[1];
const startHour = parseInt(startTime.split(":")[0]);
const startMinute = parseInt(startTime.split(":")[1]);
const endHour = parseInt(endTime.split(":")[0]);
const endMinute = parseInt(endTime.split(":")[1]);
const startOffset = startHour * 60 + startMinute; // 转换为分钟
const endOffset = endHour * 60 + endMinute;
const duration = endOffset - startOffset;
const top = startOffset; // 1分钟 = 1px
const height = Math.max(duration, 30); // 最小高度30px
return {
top: `${top}px`,
height: `${height}px`,
};
};
const handleDateClick = (date) => {
if (onDateClick) {
onDateClick(date);
}
};
const getCurrentTimeLine = () => {
const now = new Date();
const currentHour = now.getHours();
const currentMinute = now.getMinutes();
const totalMinutes = currentHour * 60 + currentMinute;
// 只在今天显示当前时间线
const isToday = weekDays.some(
(date) => date.toDateString() === now.toDateString()
);
if (!isToday) return null;
return (
<div className="current-time-line" style={{ top: `${totalMinutes}px` }} />
);
};
return (
<div className="week-view">
{/* 周标题 */}
<div className="week-header">
<div className="time-header">时间</div>
{weekDays.map((date, index) => {
const isToday = date.toDateString() === new Date().toDateString();
return (
<div
key={date.toISOString()}
className={`day-header ${isToday ? "today" : ""}`}
onClick={() => handleDateClick(date)}
>
<div className="day-name">{weekDayNames[index]}</div>
<div className="day-date">{date.getDate()}</div>
</div>
);
})}
</div>
{/* 周网格 */}
<div className="week-grid" ref={containerRef}>
{/* 时间列 */}
<div className="time-column">
{timeSlots.map((time) => (
<div key={time} className="time-slot">
{time}
</div>
))}
</div>
{/* 日期列 */}
{weekDays.map((date) => {
const dayEvents = getEventsForDate(date);
return (
<div key={date.toISOString()} className="day-column">
{/* 小时格子 */}
{timeSlots.map((time) => (
<div key={time} className="hour-slot" />
))}
{/* 事件块 */}
{dayEvents.map((event) => {
const style = calculateEventStyle(event);
const eventStyle = getEventStyleByType(event.type);
return (
<div
key={event.id}
className="event-block-new"
style={{
...style,
backgroundColor: eventStyle.backgroundColor,
color: eventStyle.textColor,
}}
title={event.description}
onClick={(e) => {
e.stopPropagation();
if (onEventClick) {
onEventClick(event);
} else {
}
}}
>
<div className="event-header">
{eventStyle.icon && (
<img
src={eventStyle.icon}
alt=""
className="event-icon"
style={{ width: '18px', height: '18px', marginRight: '4px' }}
/>
)}
<div className="event-title">{event.title}</div>
</div>
<div className="event-time">
{event.startTime.split(" ")[1]} -{" "}
{event.endTime.split(" ")[1]}
</div>
</div>
);
})}
</div>
);
})}
{/* 当前时间线 */}
{getCurrentTimeLine()}
</div>
</div>
);
};
export default WeekView;

View File

@@ -217,6 +217,47 @@
overflow: hidden;
}
/* 新的事项样式 - 使用JSON配置 */
.event-item-new {
font-size: 11px;
padding: 3px 6px;
border-radius: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 500;
max-width: 100%;
display: block;
margin: 1px 0;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(0, 0, 0, 0.1);
}
.event-item-new:hover {
transform: translateX(2px);
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);
opacity: 0.9;
}
.event-item-new .event-content {
display: flex;
align-items: center;
overflow: hidden;
}
.event-item-new .event-icon {
flex-shrink: 0;
}
.event-item-new .event-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 保持原有样式作为备用 */
.event-item {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
@@ -397,6 +438,54 @@
background: #fbfcfd;
}
/* 新的周视图事件块样式 */
.event-block-new {
position: absolute;
left: 4px;
right: 4px;
border-radius: 4px;
padding: 4px 6px;
font-size: 11px;
z-index: 5;
cursor: pointer;
transition: all 0.15s ease;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(0, 0, 0, 0.1);
}
.event-block-new:hover {
transform: translateY(-1px);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
opacity: 0.9;
}
.event-block-new .event-header {
display: flex;
align-items: center;
margin-bottom: 2px;
}
.event-block-new .event-icon {
flex-shrink: 0;
}
.event-block-new .event-title {
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.event-block-new .event-time {
font-size: 10px;
opacity: 0.9;
}
/* 保持原有样式作为备用 */
.event-block {
position: absolute;
left: 4px;