You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

920 lines
22 KiB

<template>
<div class="month-view">
<div class="month-header">
<h2>{{ displayYear }}{{ displayMonth }}</h2>
<div class="month-controls">
<el-date-picker
v-model="selectedDate"
style="float: right; width: 120px; border: 0"
popper-class="card-date-editor"
type="month"
size="mini"
placeholder="选择月"
:picker-options="pickerOptions"
@change="onMonthChange"
>
</el-date-picker>
<div class="nav-buttons">
<button class="nav-btn" @click="prevMonth">&lt;</button>
<button class="nav-btn" @click="nextMonth">&gt;</button>
</div>
</div>
</div>
<div class="calendar-grid">
<div class="weekday-header">
<div class="weekday" v-for="day in weekdays" :key="day">{{ day }}</div>
</div>
<div class="days-grid">
<!-- 上月剩余天数 -->
<div
class="day-cell empty"
v-for="empty in firstDayOffset"
:key="'empty-' + empty"
></div>
<!-- 当月天数 -->
<div
class="day-cell"
v-for="day in daysInMonth"
:key="day"
:class="{ today: isToday(day) }"
>
<div class="day-header">
<div class="day-number">{{ day }}</div>
</div>
<div class="tasks-list" v-if="hasTasks(day)">
<div
class="task-item"
v-for="(task, index) in getTasksForDay(day)"
:key="index"
:class="`status-${task.status}`"
@mouseenter="showTooltip($event, task.name)"
@mouseleave="hideTooltip"
>
<span class="task-name">{{ task.name }}</span>
</div>
</div>
</div>
<!-- 下月补充天数 -->
<div
class="day-cell empty"
v-for="empty in lastDayOffset"
:key="'empty-end-' + empty"
></div>
</div>
</div>
<!-- 工具提示 -->
<div v-if="tooltipVisible" class="task-tooltip" :style="tooltipStyle">
{{ tooltipText }}
</div>
</div>
</template>
<script>
import { calenderMonth } from "@/api/videosMonitor/index";
export default {
name: "MonthView",
props: {
year: {
type: Number,
required: false,
default: null,
},
month: {
type: Number,
required: false,
default: null,
},
},
data() {
const today = new Date();
const currentYear = today.getFullYear();
const currentMonth = today.getMonth() + 1;
// console.log('初始化当前日期:', currentYear, currentMonth);
return {
// 内部维护的当前显示年月(默认当前月)
displayYear: currentYear,
displayMonth: currentMonth,
selectedDate: new Date(currentYear, currentMonth - 1, 1),
pickerOptions: {},
weekdays: ["周日", "周一", "周二", "周三", "周四", "周五", "周六"],
tasksData: [],
loading: false,
// 工具提示相关
tooltipVisible: false,
tooltipText: "",
tooltipStyle: {
left: "0px",
top: "0px",
},
currentUpdatePosition: null,
// 防抖标记
fetchTimeout: null,
// 标记是否已经执行过首次数据加载
initialDataLoaded: false,
};
},
computed: {
daysInMonth() {
return new Date(this.displayYear, this.displayMonth, 0).getDate();
},
firstDayOffset() {
return new Date(this.displayYear, this.displayMonth - 1, 1).getDay();
},
lastDayOffset() {
const totalCells =
Math.ceil((this.firstDayOffset + this.daysInMonth) / 7) * 7;
return totalCells - this.firstDayOffset - this.daysInMonth;
},
},
watch: {
displayYear: {
handler(newYear, oldYear) {
if (newYear && oldYear && newYear !== oldYear) {
this.updateSelectedDate();
this.debouncedFetchData();
// 同步到父组件
this.$emit("update:year", newYear);
}
},
immediate: false,
},
displayMonth: {
handler(newMonth, oldMonth) {
if (newMonth && oldMonth && newMonth !== oldMonth) {
this.updateSelectedDate();
this.debouncedFetchData();
// 同步到父组件
this.$emit("update:month", newMonth);
}
},
immediate: false,
},
// 监听父组件传入的props,只有当父组件想要控制时才同步到内部状态
// 但我们需要一个标志来判断是否应该使用父组件的值
year: {
handler(newYear) {
// 只有当父组件明确想要控制(即传入了非null/undefined的值)且
// 组件还没有初始化完成时,才使用父组件的值
// 或者我们可以让父组件通过一个prop来控制是否使用父组件的值
// 注意:这里我们不自动更新displayYear,而是由父组件通过prop控制
// 或者我们可以在created中一次性处理
},
immediate: false,
},
month: {
handler(newMonth) {
// 同样,不自动更新
},
immediate: false,
},
},
created() {
const today = new Date();
const currentYear = today.getFullYear();
const currentMonth = today.getMonth() + 1;
// 关键修改:始终使用当前日期作为默认值
// 只有当父组件明确传入了有效的年月,并且我们确定要使用父组件的值时,才使用父组件的值
// 但根据您的需求,您想要显示当前月,所以我们应该忽略父组件的传入值
// 方案1:完全忽略父组件传入的值,始终使用当前日期
this.displayYear = currentYear;
this.displayMonth = currentMonth;
// 通知父组件当前月份(如果父组件需要知道)
this.$emit("update:year", this.displayYear);
this.$emit("update:month", this.displayMonth);
this.updateSelectedDate();
},
mounted() {
// 只在首次挂载时获取数据(确保当前月份数据加载)
if (!this.initialDataLoaded) {
this.getMonthData();
this.initialDataLoaded = true;
}
// 监听窗口变化,重新计算工具提示位置
window.addEventListener("resize", this.handleResize);
},
beforeDestroy() {
// 清理事件监听
window.removeEventListener("resize", this.handleResize);
// 清理防抖定时器
if (this.fetchTimeout) {
clearTimeout(this.fetchTimeout);
}
// 清理鼠标移动监听
if (this.currentUpdatePosition) {
document.removeEventListener("mousemove", this.currentUpdatePosition);
}
},
methods: {
// 更新选中的日期
updateSelectedDate() {
if (this.displayYear && this.displayMonth) {
this.selectedDate = new Date(this.displayYear, this.displayMonth - 1, 1);
}
},
// 防抖获取数据
debouncedFetchData() {
if (this.fetchTimeout) {
clearTimeout(this.fetchTimeout);
}
this.fetchTimeout = setTimeout(() => {
this.getMonthData();
}, 300);
},
onMonthChange() {
if (this.selectedDate) {
const year = this.selectedDate.getFullYear();
const month = this.selectedDate.getMonth() + 1;
// 更新内部状态
if (year !== this.displayYear || month !== this.displayMonth) {
this.displayYear = year;
this.displayMonth = month;
// 注意:不需要在这里调用getMonthData,watch会触发
}
}
},
prevMonth() {
const newDate = new Date(this.displayYear, this.displayMonth - 2, 1);
this.displayYear = newDate.getFullYear();
this.displayMonth = newDate.getMonth() + 1;
},
nextMonth() {
const newDate = new Date(this.displayYear, this.displayMonth, 1);
this.displayYear = newDate.getFullYear();
this.displayMonth = newDate.getMonth() + 1;
},
isToday(day) {
const today = new Date();
return (
today.getFullYear() === this.displayYear &&
today.getMonth() + 1 === this.displayMonth &&
today.getDate() === day
);
},
hasTasks(day) {
return this.tasksData.some((task) => {
if (!task.start) return false;
const taskDate = new Date(task.start);
return (
taskDate.getDate() === day &&
taskDate.getMonth() + 1 === this.displayMonth &&
taskDate.getFullYear() === this.displayYear
);
});
},
getTasksForDay(day) {
return this.tasksData
.filter((task) => {
if (!task.start) return false;
const taskDate = new Date(task.start);
return (
taskDate.getDate() === day &&
taskDate.getMonth() + 1 === this.displayMonth &&
taskDate.getFullYear() === this.displayYear
);
})
.map((task) => ({
name: task.name || "未命名任务",
status: this.mapTaskStatus(task.status),
}));
},
getTaskStats(day) {
const dayTasks = this.getTasksForDay(day);
const stats = {
completed: 0,
executing: 0,
"not-executed": 0,
};
dayTasks.forEach((task) => {
if (stats.hasOwnProperty(task.status)) {
stats[task.status]++;
}
});
return {
stats,
total: dayTasks.length,
};
},
mapTaskStatus(apiStatus) {
const statusMap = {
executed: "completed",
executing: "executing",
pending: "not-executed",
};
return statusMap[apiStatus] || "not-executed";
},
async getMonthData() {
// 避免重复请求
if (this.loading) return;
try {
this.loading = true;
const response = await calenderMonth({
year: this.displayYear,
month: this.displayMonth,
});
// 处理不同的响应格式
const data = response?.rows || response?.data || response || [];
this.tasksData = Array.isArray(data) ? data : [];
} catch (error) {
this.tasksData = [];
} finally {
this.loading = false;
}
},
// 显示工具提示
showTooltip(event, text) {
this.tooltipText = text;
this.tooltipVisible = true;
// 使用当前鼠标位置
const x = event.clientX;
const y = event.clientY;
const offset = 10;
this.tooltipStyle = {
left: `${x + offset}px`,
top: `${y + offset}px`,
};
// 监听鼠标移动来更新位置
const updatePosition = (e) => {
if (this.tooltipVisible) {
this.tooltipStyle = {
left: `${e.clientX + offset}px`,
top: `${e.clientY + offset}px`,
};
}
};
document.addEventListener("mousemove", updatePosition);
this.currentUpdatePosition = updatePosition;
},
// 隐藏工具提示
hideTooltip() {
this.tooltipVisible = false;
if (this.currentUpdatePosition) {
document.removeEventListener("mousemove", this.currentUpdatePosition);
this.currentUpdatePosition = null;
}
},
// 处理窗口大小变化
handleResize() {
if (this.tooltipVisible) {
this.tooltipVisible = false;
}
},
},
};
</script>
<style scoped>
.month-view {
width: 100%;
min-height: 100%;
background-color: #172533;
display: flex;
flex-direction: column;
overflow: hidden;
}
.month-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
background-color: #101d29;
padding: 15px 20px;
border-radius: 8px;
flex-shrink: 0;
}
.month-header h2 {
margin: 0;
font-size: 20px;
color: #fff !important;
}
.month-controls {
display: flex;
gap: 10px;
align-items: center;
}
.nav-buttons {
display: flex;
gap: 5px;
}
.nav-btn {
width: 80px;
height: 30px;
background-color: #202f3e;
color: #fff;
border: 2px solid #40486a;
border-radius: 4px;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
transition: all 0.3s ease;
}
.nav-btn:hover {
background-color: #40486a;
color: #ffffff;
}
.calendar-grid {
background-color: #101d29;
border-radius: 8px;
overflow: hidden;
border: 2px solid #40486a;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.weekday-header {
display: grid;
grid-template-columns: repeat(7, 1fr);
background-color: #101d29;
padding: 10px 0;
border-bottom: 2px solid #40486a;
flex-shrink: 0;
}
.weekday {
text-align: center;
font-weight: bold;
color: #fff;
padding: 5px;
border-right: 2px solid #40486a;
}
.weekday:last-child {
border-right: none;
}
.days-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-template-rows: repeat(auto-fill, minmax(120px, 1fr));
background-color: #172533;
flex: 1;
min-height: 0;
}
/* 固定日历格子的宽度和高度自适应 */
.day-cell {
padding: 5px;
position: relative;
min-height: 120px;
border-right: 2px solid #40486a;
border-bottom: 2px solid #40486a;
background-color: #172533;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* 确保所有格子等宽 */
.day-cell {
width: 100%;
min-width: 0; /* 允许宽度压缩 */
}
/* 响应式:小屏幕时适当调整最小高度 */
@media (max-width: 1200px) {
.day-cell {
min-height: 100px;
}
}
@media (max-width: 992px) {
.day-cell {
min-height: 90px;
}
}
/* 行背景色保持不变 */
.days-grid > .day-cell:nth-child(7n + 1):nth-child(-n + 7),
.days-grid > .day-cell:nth-child(7n + 2):nth-child(-n + 7),
.days-grid > .day-cell:nth-child(7n + 3):nth-child(-n + 7),
.days-grid > .day-cell:nth-child(7n + 4):nth-child(-n + 7),
.days-grid > .day-cell:nth-child(7n + 5):nth-child(-n + 7),
.days-grid > .day-cell:nth-child(7n + 6):nth-child(-n + 7),
.days-grid > .day-cell:nth-child(7n + 7):nth-child(-n + 7) {
background-color: #172533;
}
/* 偶数行颜色 */
.days-grid > .day-cell:nth-child(7n + 1):nth-child(n + 8),
.days-grid > .day-cell:nth-child(7n + 2):nth-child(n + 8),
.days-grid > .day-cell:nth-child(7n + 3):nth-child(n + 8),
.days-grid > .day-cell:nth-child(7n + 4):nth-child(n + 8),
.days-grid > .day-cell:nth-child(7n + 5):nth-child(n + 8),
.days-grid > .day-cell:nth-child(7n + 6):nth-child(n + 8),
.days-grid > .day-cell:nth-child(7n + 7):nth-child(n + 8) {
background-color: #202f3e;
}
/* 第三行颜色 */
.days-grid > .day-cell:nth-child(7n + 1):nth-child(n + 15),
.days-grid > .day-cell:nth-child(7n + 2):nth-child(n + 15),
.days-grid > .day-cell:nth-child(7n + 3):nth-child(n + 15),
.days-grid > .day-cell:nth-child(7n + 4):nth-child(n + 15),
.days-grid > .day-cell:nth-child(7n + 5):nth-child(n + 15),
.days-grid > .day-cell:nth-child(7n + 6):nth-child(n + 15),
.days-grid > .day-cell:nth-child(7n + 7):nth-child(n + 15) {
background-color: #172533;
}
/* 第四行颜色 */
.days-grid > .day-cell:nth-child(7n + 1):nth-child(n + 22),
.days-grid > .day-cell:nth-child(7n + 2):nth-child(n + 22),
.days-grid > .day-cell:nth-child(7n + 3):nth-child(n + 22),
.days-grid > .day-cell:nth-child(7n + 4):nth-child(n + 22),
.days-grid > .day-cell:nth-child(7n + 5):nth-child(n + 22),
.days-grid > .day-cell:nth-child(7n + 6):nth-child(n + 22),
.days-grid > .day-cell:nth-child(7n + 7):nth-child(n + 22) {
background-color: #202f3e;
}
/* 第五行颜色 */
.days-grid > .day-cell:nth-child(7n + 1):nth-child(n + 29),
.days-grid > .day-cell:nth-child(7n + 2):nth-child(n + 29),
.days-grid > .day-cell:nth-child(7n + 3):nth-child(n + 29),
.days-grid > .day-cell:nth-child(7n + 4):nth-child(n + 29),
.days-grid > .day-cell:nth-child(7n + 5):nth-child(n + 29),
.days-grid > .day-cell:nth-child(7n + 6):nth-child(n + 29),
.days-grid > .day-cell:nth-child(7n + 7):nth-child(n + 29) {
background-color: #172533;
}
/* 第六行颜色 */
.days-grid > .day-cell:nth-child(7n + 1):nth-child(n + 36),
.days-grid > .day-cell:nth-child(7n + 2):nth-child(n + 36),
.days-grid > .day-cell:nth-child(7n + 3):nth-child(n + 36),
.days-grid > .day-cell:nth-child(7n + 4):nth-child(n + 36),
.days-grid > .day-cell:nth-child(7n + 5):nth-child(n + 36),
.days-grid > .day-cell:nth-child(7n + 6):nth-child(n + 36),
.days-grid > .day-cell:nth-child(7n + 7):nth-child(n + 36) {
background-color: #202f3e;
}
/* 去掉最右边的边框 */
.day-cell:nth-child(7n) {
border-right: none;
}
/* 去掉最后一行的底部边框 */
.days-grid > .day-cell:nth-last-child(-n + 7) {
border-bottom: none;
}
.day-cell.empty {
background-color: #101d29 !important;
border-right: 2px solid #40486a;
border-bottom: 2px solid #40486a;
}
.day-cell.today {
background-color: rgba(36, 115, 130, 0.2) !important;
border: 1px solid #247382;
z-index: 1;
}
/* 新增:日期+统计的头部容器,弹性布局左右分布 */
.day-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 5px;
flex-shrink: 0;
}
.day-number {
font-size: 25px;
font-weight: bold;
color: #7984b1;
line-height: 1;
}
/* 新增:任务统计样式 - 小字体、对应颜色、横向排列 */
.task-stats {
display: flex;
gap: 4px;
}
.stat-item {
font-size: 10px; /* 小字体 */
padding: 1px 4px;
border-radius: 2px;
min-width: 16px;
text-align: center;
}
.tasks-list {
display: flex;
flex-direction: column;
gap: 3px;
flex: 1;
overflow-y: auto;
min-height: 0;
max-height: calc(100% - 30px);
}
/* 任务较多时的自适应高度 */
.day-cell:has(.tasks-list) {
min-height: 120px;
}
.day-cell:has(.task-item:nth-child(n + 4)) {
min-height: 140px;
}
.day-cell:has(.task-item:nth-child(n + 6)) {
min-height: 160px;
}
.day-cell:has(.task-item:nth-child(n + 8)) {
min-height: 180px;
}
.task-item {
padding: 2px 4px;
border-radius: 3px;
font-size: 12px;
cursor: default;
transition: all 0.3s ease;
flex-shrink: 0;
width: 100%;
overflow: hidden;
position: relative;
}
.task-name {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
.status-executing {
background-color: #247382;
color: #ffffff;
}
.status-completed {
background-color: #296a5a;
color: #ffffff;
}
.status-not-executed {
background-color: #6d7d92;
color: #ffffff;
}
/* 工具提示样式 */
.task-tooltip {
position: fixed;
z-index: 9999;
background-color: #202f3e;
border: 2px solid #40486a;
border-radius: 4px;
padding: 8px 12px;
font-size: 12px;
color: #ffffff;
max-width: 300px;
word-break: break-all;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
pointer-events: none;
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 滚动条样式 */
.tasks-list::-webkit-scrollbar {
width: 4px;
}
.tasks-list::-webkit-scrollbar-track {
background: transparent;
}
.tasks-list::-webkit-scrollbar-thumb {
background: #40486a;
border-radius: 2px;
}
.tasks-list::-webkit-scrollbar-thumb:hover {
background: #7984b1;
}
/* 覆盖Element UI的默认样式 */
:deep(.el-date-editor--month) {
background-color: #101d29;
border: none;
border-radius: 4px;
}
:deep(.el-input__inner) {
background-color: #101d29;
color: #7984b1;
border: 2px solid #40486a;
}
:deep(.el-input__inner::placeholder) {
color: #6d7d92;
}
:deep(.el-date-picker__header-label) {
color: #7984b1;
}
:deep(.el-date-picker__month-btn) {
color: #7984b1;
}
:deep(.el-date-picker__month-btn:hover) {
color: #247382;
}
:deep(.el-picker-panel) {
background-color: #101d29;
border: 2px solid #40486a;
}
.card-date-editor {
background-color: #101d29;
}
/* Blue dashboard theme */
.month-view {
background: #0b1248;
color: #dceaff;
}
.month-header {
margin-bottom: 14px;
padding: 10px 0;
border-radius: 0;
background: transparent;
}
.month-header h2 {
color: #b9d6ff !important;
}
.nav-btn {
width: 36px;
border: 1px solid #438fe5;
border-radius: 0;
color: #9dcbff;
background: #0b1d58;
}
.nav-btn:hover {
border-color: #55baff;
color: #ffffff;
background: #123b82;
}
.calendar-grid {
border: 1px solid #2787ed;
border-radius: 0;
background: #0b174f;
}
.weekday-header {
padding: 0;
border-bottom: 1px solid #2787ed;
background: #0b174f;
}
.weekday {
padding: 12px 5px;
border-right: 1px solid #2787ed;
color: #35bcff;
}
.days-grid,
.days-grid > .day-cell {
background: #101e63 !important;
}
.days-grid > .day-cell:nth-child(14n + 8),
.days-grid > .day-cell:nth-child(14n + 9),
.days-grid > .day-cell:nth-child(14n + 10),
.days-grid > .day-cell:nth-child(14n + 11),
.days-grid > .day-cell:nth-child(14n + 12),
.days-grid > .day-cell:nth-child(14n + 13),
.days-grid > .day-cell:nth-child(14n + 14) {
background: #13266f !important;
}
.day-cell {
border-right: 1px solid #2787ed;
border-bottom: 1px solid #2787ed;
}
.day-cell.empty {
border-right: 1px solid #2787ed;
border-bottom: 1px solid #2787ed;
background: #091442 !important;
}
.day-number {
color: #8096ce;
}
.day-cell.today {
z-index: 2;
background: linear-gradient(135deg, rgba(13, 99, 173, 0.72), rgba(13, 65, 139, 0.72)) !important;
border: 2px solid #54ffff !important;
box-shadow: inset 0 0 20px rgba(84, 255, 255, 0.18), 0 0 8px rgba(84, 255, 255, 0.42);
}
.day-cell.today .day-number {
display: inline-flex;
align-items: center;
gap: 7px;
color: #ffffff;
text-shadow: 0 0 6px rgba(84, 255, 255, 0.75);
}
.day-cell.today .day-number::after {
content: "今";
display: inline-grid;
place-items: center;
width: 20px;
height: 20px;
border-radius: 50%;
color: #062653;
background: #54ffff;
font-size: 11px;
line-height: 1;
text-shadow: none;
}
.status-executing { background-color: #147fc1; }
.status-completed { background-color: #178a70; }
.status-not-executed { background-color: #536c9f; }
.task-tooltip {
border: 1px solid #438fe5;
border-radius: 2px;
background: #0b1d58;
}
:deep(.el-date-editor--month),
:deep(.el-input__inner) {
border-radius: 0;
background-color: #0b1d58;
}
:deep(.el-input__inner) {
border: 1px solid #438fe5;
color: #dceaff;
}
</style>