Browse Source

做巡视计划和缺陷记录页面

master
douyage 1 month ago
parent
commit
b299c7d7cd
11 changed files with 2486 additions and 12 deletions
  1. BIN
      src/assets/images/nav/inspection-task.png
  2. +63
    -0
      src/assets/styles/ruoyi.scss
  3. +9
    -0
      src/assets/styles/sidebar.scss
  4. +12
    -5
      src/layout/components/Sidebar/Item.vue
  5. +60
    -1
      src/layout/components/TagsView/index.vue
  6. +33
    -6
      src/router/index.js
  7. +346
    -0
      src/views/cards/defectRecord/index.vue
  8. +710
    -0
      src/views/cards/tasksShow/dailyDisplay.vue
  9. +162
    -0
      src/views/cards/tasksShow/index.vue
  10. +791
    -0
      src/views/cards/tasksShow/monthlyDisplay.vue
  11. +300
    -0
      src/views/cards/tasksShow/yearDisplay.vue

BIN
src/assets/images/nav/inspection-task.png View File

Before After
Width: 32  |  Height: 32  |  Size: 716 B

+ 63
- 0
src/assets/styles/ruoyi.scss View File

@ -667,6 +667,69 @@
}
}
/* Global dark form controls and option panels. */
.el-input__inner,
.el-textarea__inner,
.el-date-editor.el-input__inner,
.el-date-editor .el-range-input {
color: #dcecff !important;
background-color: rgba(5, 27, 91, 0.82) !important;
border-color: #6384d8 !important;
}
.el-input__inner::placeholder,
.el-textarea__inner::placeholder,
.el-date-editor .el-range-input::placeholder {
color: rgba(220, 236, 255, 0.5) !important;
}
.el-input__inner:hover,
.el-textarea__inner:hover,
.el-input.is-active .el-input__inner,
.el-input__inner:focus,
.el-textarea__inner:focus,
.el-select .el-input.is-focus .el-input__inner {
border-color: #43cfff !important;
}
.el-input.is-disabled .el-input__inner,
.el-textarea.is-disabled .el-textarea__inner {
color: rgba(220, 236, 255, 0.55) !important;
background-color: rgba(8, 25, 69, 0.82) !important;
}
.el-select-dropdown .el-scrollbar,
.el-select-dropdown__wrap,
.el-select-dropdown__list,
.el-autocomplete-suggestion__wrap,
.el-autocomplete-suggestion li,
.el-cascader-panel,
.el-cascader-menu,
.el-picker-panel__body,
.el-picker-panel__content {
color: #dcecff !important;
background-color: #0f124a !important;
border-color: #0b2567 !important;
}
.el-select-dropdown__empty,
.el-cascader-menu__empty-text,
.el-autocomplete-suggestion li {
color: #b9d8ff !important;
}
.el-select-dropdown[x-placement^="bottom"] .popper__arrow::after,
.el-autocomplete-suggestion[x-placement^="bottom"] .popper__arrow::after,
.el-cascader__dropdown[x-placement^="bottom"] .popper__arrow::after {
border-bottom-color: #0f124a !important;
}
.el-select-dropdown[x-placement^="top"] .popper__arrow::after,
.el-autocomplete-suggestion[x-placement^="top"] .popper__arrow::after,
.el-cascader__dropdown[x-placement^="top"] .popper__arrow::after {
border-top-color: #0f124a !important;
}
.splitpanes.default-theme .splitpanes__splitter {
background-color: #0B2567 !important;
border-color: #0B2567 !important;


+ 9
- 0
src/assets/styles/sidebar.scss View File

@ -76,6 +76,15 @@
object-fit: contain;
}
.menu-icon-placeholder {
display: inline-block;
width: 16px;
height: 16px;
margin-right: 16px;
vertical-align: -3px;
visibility: hidden;
}
.el-menu {
border: none;
height: 100%;


+ 12
- 5
src/layout/components/Sidebar/Item.vue View File

@ -1,16 +1,21 @@
<script>
const dataNavIcon = require('@/assets/images/nav/data.png')
const reservedIconTitles = new Set([
'巡视任务',
'点位管理',
'巡视报告',
'巡视计划',
'缺陷记录'
])
const navIconMap = {
'首页': require('@/assets/images/nav/home.png'),
'Home': require('@/assets/images/nav/home.png'),
'遥视任务管理': require('@/assets/images/nav/task.png'),
'巡检任务管理': require('@/assets/images/nav/task.png'),
'巡检任务管理': require('@/assets/images/nav/inspection-task.png'),
'巡视任务管理': require('@/assets/images/nav/inspection-task.png'),
'巡检任务': require('@/assets/images/nav/task.png'),
'巡视任务': require('@/assets/images/nav/task.png'),
'巡视报告': require('@/assets/images/nav/task.png'),
'设备管理': require('@/assets/images/nav/device.png'),
'点位管理': require('@/assets/images/nav/point.png'),
'设备管理': require('@/assets/images/nav/monitor.png'),
'点位管理(实施用)': require('@/assets/images/nav/point.png'),
'系统管理': require('@/assets/images/nav/system.png'),
'区域管理': require('@/assets/images/nav/area.png'),
@ -54,6 +59,8 @@ export default {
vnodes.push(<img class="menu-icon-img" src={navIcon}/>)
} else if (icon) {
vnodes.push(<svg-icon icon-class={icon}/>)
} else if (reservedIconTitles.has(normalizedTitle)) {
vnodes.push(<span class="menu-icon-placeholder" aria-hidden="true" />)
}
if (title) {


+ 60
- 1
src/layout/components/TagsView/index.vue View File

@ -1,6 +1,17 @@
<template>
<div id="tags-view-container" class="tags-view-container">
<breadcrumb class="module-breadcrumb" />
<div v-if="$route.name === 'PatrolSchedule'" class="schedule-view-switch">
<button
v-for="view in scheduleViews"
:key="view.value"
type="button"
:class="{ active: scheduleView === view.value }"
@click="setScheduleView(view.value)"
>
{{ view.label }}
</button>
</div>
<scroll-pane ref="scrollPane" class="tags-view-wrapper tags-view-wrapper-hidden" @scroll="handleScroll">
<router-link
v-for="tag in visitedViews"
@ -42,7 +53,12 @@ export default {
top: 0,
left: 0,
selectedTag: {},
affixTags: []
affixTags: [],
scheduleViews: [
{ label: '年视图', value: 'year' },
{ label: '月视图', value: 'month' },
{ label: '日视图', value: 'day' }
]
}
},
computed: {
@ -54,6 +70,9 @@ export default {
},
theme() {
return this.$store.state.settings.theme;
},
scheduleView() {
return this.$route.query.view || 'year'
}
},
watch: {
@ -74,6 +93,13 @@ export default {
this.addTags()
},
methods: {
setScheduleView(view) {
if (view === this.scheduleView) return
this.$router.replace({
name: 'PatrolSchedule',
query: { ...this.$route.query, view }
})
},
isActive(route) {
return route.path === this.$route.path
},
@ -289,6 +315,39 @@ export default {
line-height: 40px !important;
}
.schedule-view-switch {
position: absolute;
z-index: 2;
top: 6px;
right: 18px;
display: flex;
height: 28px;
border: 1px solid rgba(72, 167, 255, 0.65);
background: #0b2460;
button {
width: 72px;
height: 26px;
padding: 0;
border: 0;
border-right: 1px solid rgba(72, 167, 255, 0.45);
color: #b9d8ff;
background: transparent;
font-size: 13px;
cursor: pointer;
&:last-child {
border-right: 0;
}
&:hover,
&.active {
color: #ffffff;
background: #176bc2;
}
}
}
.tags-view-wrapper-hidden {
position: absolute;
width: 0;


+ 33
- 6
src/router/index.js View File

@ -92,13 +92,13 @@ export const constantRoutes = [
path: '/task-summary',
component: Layout,
redirect: '/task-summary/inspectionTask',
meta: { title: '巡视任务', icon: 'task' },
meta: { title: '巡视任务' },
children: [
{
path: 'inspectionTask',
component: () => import('@/views/cards/taskSummary/inspectionTask'),
name: 'InspectionTaskSummary',
meta: { title: '巡视任务', icon: 'task', activeMenu: '/task-summary' }
meta: { title: '巡视任务', activeMenu: '/task-summary' }
}
]
},
@ -120,7 +120,7 @@ export const constantRoutes = [
path: '/site-management',
component: Layout,
redirect: '/site-management/index',
meta: { title: '点位管理', icon: 'point' },
meta: { title: '点位管理' },
children: [
{
path: 'index',
@ -128,7 +128,6 @@ export const constantRoutes = [
name: 'SiteManagementCard',
meta: {
title: '点位管理',
icon: 'point',
activeMenu: '/site-management'
}
}
@ -138,13 +137,13 @@ export const constantRoutes = [
path: '/patrol-report',
component: Layout,
redirect: '/patrol-report/index',
meta: { title: '巡视报告', icon: 'task' },
meta: { title: '巡视报告' },
children: [
{
path: 'index',
component: () => import('@/views/cards/patrolReport/InspectionReporter'),
name: 'PatrolReport',
meta: { title: '巡视报告', icon: 'task', activeMenu: '/patrol-report' }
meta: { title: '巡视报告', activeMenu: '/patrol-report' }
},
{
path: 'detail',
@ -154,6 +153,34 @@ export const constantRoutes = [
meta: { title: '巡视报告详情', activeMenu: '/patrol-report' }
}
]
},
{
path: '/patrol-schedule',
component: Layout,
redirect: '/patrol-schedule/index',
meta: { title: '巡视计划' },
children: [
{
path: 'index',
component: () => import('@/views/cards/tasksShow/index'),
name: 'PatrolSchedule',
meta: { title: '巡视计划', activeMenu: '/patrol-schedule' }
}
]
},
{
path: '/defect-record',
component: Layout,
redirect: '/defect-record/index',
meta: { title: '缺陷记录' },
children: [
{
path: 'index',
component: () => import('@/views/cards/defectRecord/index'),
name: 'DefectRecord',
meta: { title: '缺陷记录', activeMenu: '/defect-record' }
}
]
}
]


+ 346
- 0
src/views/cards/defectRecord/index.vue View File

@ -0,0 +1,346 @@
<template>
<CardBox
class="card-page"
:showTitle="false"
>
<div class="page-container">
<div class="table-card" ref="tableCardRef">
<el-table
v-loading="loading"
:data="dataList"
class="card-table"
ref="tableRef"
border
stripe
:height="tableHeight"
>
<el-table-column
label="序号"
type="index"
width="60"
align="center"
:index="table_index"
/>
<el-table-column label="区域" align="center" prop="areaName" />
<el-table-column label="点位" align="center" prop="pointName" />
<el-table-column label="采集时间" align="center" prop="patrolTime" />
<el-table-column label="缺陷照片" align="center">
<template slot-scope="scope">
<AsyncImage
v-if="scope.row.imgAnalyse"
class="table-img"
:src="scope.row.imgAnalyse"
@load="onImageLoad"
@click="lookImageDetail(scope.row.imgAnalyse)"
>
</AsyncImage>
<span v-else>--</span>
</template>
</el-table-column>
<el-table-column label="缺陷结果" align="center" prop="desc" />
</el-table>
<CardPagination
ref="paginationRef"
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</div>
</div>
<el-dialog
title="图片预览"
:visible.sync="showPreviewImages"
:close-on-click-modal="false"
:close-on-press-escape="false"
:destroy-on-close="true"
custom-class="card-dialog card-page"
width="70%"
append-to-body
>
<ImagePreview
v-if="showPreviewImages"
:imageUrls="showImgesUrls"
:selectImgPosition="previewImagePos"
:showTemperatureTool="false"
/>
</el-dialog>
</CardBox>
</template>
<script>
import CardBox from "../components/cardBox.vue";
import CardPagination from "../components/CardPagination/index.vue";
import { defectRecordList } from "@/api/videosMonitor/index";
import ImagePreview from "@/views/cards/components/ImagePreview/index.vue";
import AsyncImage from "@/components/AsyncImage/index.vue";
export default {
name: "DefectRecord",
components: { CardBox, CardPagination, ImagePreview, AsyncImage },
data() {
return {
queryParams: { pageNum: 1, pageSize: 20 },
dataList: [],
loading: false,
total: 0,
tableHeight: null,
addShow: false,
dialogTitle: "新建日常维护",
defaultProps: {
children: "children",
label: "label",
},
dateRange: [],
dateRangePickerOptions: {
disabledDate(selectDate) {
return new Date().getTime() < selectDate.getTime();
},
},
//
imageLoadCount: 0,
expectedImageCount: 0,
layoutRefreshTimer: null,
showPreviewImages: false,
showImgesUrls: [],
previewImagePos: 0,
};
},
created() {
this.getList();
window.addEventListener("resize", this.setTableHeight);
},
mounted() {
this.setTableHeight();
},
beforeDestroy() {
//
if (this.layoutRefreshTimer) {
clearTimeout(this.layoutRefreshTimer);
}
window.removeEventListener("resize", this.setTableHeight);
},
methods: {
setTableHeight() {
if (!this.$refs.tableCardRef) return;
const pagination = this.$refs.paginationRef;
const paginationHeight =
pagination && pagination.$el ? pagination.$el.offsetHeight : 0;
this.tableHeight = Math.max(
this.$refs.tableCardRef.clientHeight - paginationHeight,
160
);
},
//
table_index(index) {
const { pageNum, pageSize } = this.queryParams;
return (pageNum - 1) * pageSize + index + 1;
},
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
linkUrl() {
this.addShow = true;
this.dialogTitle = "新建日常维护";
},
controlChange() {},
editClick() {
this.addShow = true;
this.dialogTitle = "编辑日常维护";
},
delClick() {},
//
onImageLoad() {
this.imageLoadCount++;
//
if (
this.imageLoadCount === this.expectedImageCount &&
this.expectedImageCount > 0
) {
this.refreshTableLayout();
}
},
lookImageDetail(imgUrl) {
this.previewImagePos = 0;
this.showImgesUrls = [imgUrl];
this.showPreviewImages = true;
},
//
refreshTableLayout() {
this.$nextTick(() => {
if (this.layoutRefreshTimer) {
clearTimeout(this.layoutRefreshTimer);
}
// DOM
this.layoutRefreshTimer = setTimeout(() => {
if (this.$refs.tableRef && this.$refs.tableRef.doLayout) {
this.$refs.tableRef.doLayout();
}
//
this.imageLoadCount = 0;
this.expectedImageCount = 0;
}, 50);
});
},
//
forceRefreshTable() {
this.$nextTick(() => {
setTimeout(() => {
if (this.$refs.tableRef && this.$refs.tableRef.doLayout) {
this.$refs.tableRef.doLayout();
}
}, 100);
});
},
getList() {
this.loading = true;
//
this.imageLoadCount = 0;
this.expectedImageCount = 0;
const params = { ...this.queryParams };
if (this.dateRange && this.dateRange.length === 2) {
params.beginTime = this.dateRange[0];
params.endTime = this.dateRange[1];
}
defectRecordList(params)
.then((res) => {
this.dataList = res.rows || [];
this.total = res.total || 0;
this.$nextTick(this.setTableHeight);
//
this.expectedImageCount = this.dataList.filter(
(item) => item.imgAnalyse
).length;
//
if (this.expectedImageCount === 0) {
this.forceRefreshTable();
}
})
.catch((error) => {
console.error("获取缺陷记录失败:", error);
this.dataList = [];
this.total = 0;
})
.finally(() => {
this.loading = false;
});
},
onSubmit() {
this.addShow = false;
},
},
};
</script>
<style lang="scss" scoped>
.page-container {
height: 100%;
display: flex;
flex-direction: column;
}
.card-page {
height: 100%;
min-height: 0;
overflow: hidden;
}
.fss {
font-size: 20px;
margin: 0 15px;
cursor: pointer;
}
.table-card {
flex: 1;
height: 0;
::v-deep .el-card__body {
height: 100%;
}
}
.tree-container {
display: flex;
}
.tree-box {
width: 45%;
}
//
.table-img {
::v-deep img {
width: 80px;
height: 60px;
object-fit: cover;
display: block;
margin: 0 auto;
}
}
.card-page ::v-deep .el-table {
width: 100% !important;
color: #17345f;
background: transparent !important;
border-color: #6f9ed8 !important;
}
.card-page ::v-deep .el-table::before,
.card-page ::v-deep .el-table::after,
.card-page ::v-deep .el-table--border::after,
.card-page ::v-deep .el-table--group::after {
background-color: #6f9ed8 !important;
}
.card-page ::v-deep .el-table th.el-table__cell {
color: #ffffff !important;
background: #0b3d89 !important;
border-color: #6f9ed8 !important;
}
.card-page ::v-deep .el-table th.el-table__cell .cell {
color: #ffffff !important;
}
.card-page ::v-deep .el-table__body-wrapper,
.card-page ::v-deep .el-table__empty-block,
.card-page ::v-deep .el-table__fixed,
.card-page ::v-deep .el-table__fixed-right {
background: #0f124a !important;
}
.card-page ::v-deep .el-table__empty-text {
color: #9fc4ee !important;
}
.card-page ::v-deep .el-table__body .el-table__row,
.card-page ::v-deep .el-table__body .el-table__row td.el-table__cell {
color: #17345f !important;
background: #edf4ff !important;
border-color: #a9c4e8 !important;
}
.card-page ::v-deep .el-table__body .el-table__row td.el-table__cell .cell,
.card-page ::v-deep .el-table__body .el-table__row td.el-table__cell .cell span {
color: #17345f !important;
}
.card-page ::v-deep .el-table__body .el-table__row--striped,
.card-page ::v-deep .el-table__body .el-table__row--striped td.el-table__cell {
background: #dce9fa !important;
}
.card-page ::v-deep .el-table__body tr:hover > td.el-table__cell {
background: #c8dcf5 !important;
}
</style>

+ 710
- 0
src/views/cards/tasksShow/dailyDisplay.vue View File

@ -0,0 +1,710 @@
<template>
<div class="day-view-container">
<div class="day-view-header">
<div class="header-content">
<div class="current-date">{{ formattedDate }}</div>
<div class="time-week-info">
<!-- <div class="current-time">{{ formattedTime }}</div> -->
<div class="current-time">时间</div>
<div class="week-day">{{ dayOfWeek }}</div>
</div>
</div>
<el-date-picker
v-model="value"
type="date"
size="mini"
popper-class="card-date-editor"
placeholder="选择日期"
:picker-options="pickerOptions"
@change="handleDateChange"
style="width: 140px; float: right"
></el-date-picker>
</div>
<div class="day-view-content">
<!-- 时间轴 -->
<div class="time-axis">
<!-- 24小时制时间轴从1点到24点 -->
<div v-for="hour in 24" :key="hour" class="time-slot">
<div class="time-label hour-label">
{{ formatHour(hour - 1) }}:00
</div>
<div class="time-label half-hour-label">
{{ formatHour(hour - 1) }}:30
</div>
</div>
</div>
<!-- 任务区域 -->
<div class="tasks-container">
<!-- 所有格子线 -->
<div class="grid-lines">
<div v-for="i in 48" :key="i" class="grid-line"></div>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner"></div>
<div class="loading-text">加载中...</div>
</div>
<!-- 无数据提示 -->
<div v-if="!loading && tasks.length === 0" class="no-data">
暂无任务数据
</div>
<!-- 任务列表 -->
<div
v-for="task in tasks"
:key="task.id"
class="task-item"
:class="`task-status-${task.status}`"
:style="getTaskStyle(task)"
>
<div
class="task-time"
:title="`${task.title} (${task.startTime} - ${task.endTime})`"
>
{{ task.startTime }} - {{ task.endTime }}
<span class="task-title-inline">{{ task.title }}</span>
</div>
<!-- <div class="task-title">{{ task.title }}</div> -->
</div>
</div>
</div>
</div>
</template>
<script>
import { calenderDay } from "@/api/videosMonitor/index";
import debounce from "lodash/debounce";
export default {
data() {
return {
value: new Date(),
pickerOptions: {
disabledDate(time) {
return time.getTime() > Date.now();
},
},
tasks: [],
//
taskGridPositions: {},
loading: false,
currentRequestId: 0,
};
},
computed: {
formattedDate() {
const year = this.value.getFullYear();
const month = String(this.value.getMonth() + 1).padStart(2, "0");
const day = String(this.value.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
},
dayOfWeek() {
const weekDays = [
"星期日",
"星期一",
"星期二",
"星期三",
"星期四",
"星期五",
"星期六",
];
return weekDays[this.value.getDay()];
},
},
mounted() {
this.getCalenderDay();
this.updateCurrentTime();
this.debouncedCalculateTaskPositions = debounce(
this.calculateTaskPositions,
100
);
},
beforeDestroy() {
if (this.timeInterval) {
clearInterval(this.timeInterval);
}
},
methods: {
updateCurrentTime() {
this.timeInterval = setInterval(() => {
this.$forceUpdate();
}, 60000);
},
formatHour(hour) {
const normalizedHour = hour < 0 ? hour + 24 : hour;
return normalizedHour < 10 ? '0' + normalizedHour : normalizedHour;
},
formatTimeToHHMM(timeString) {
if (!timeString) return "";
const date = new Date(timeString);
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
return `${hours}:${minutes}`;
},
mapStatus(apiStatus) {
const statusMap = {
executed: "completed",
executing: "running",
pending: "pending",
};
return statusMap[apiStatus] || "pending";
},
processApiData(apiData) {
if (!apiData || !Array.isArray(apiData)) return [];
return apiData.map((item, index) => ({
id: `${index}_${item.startTime}_${Math.random()
.toString(36)
.substr(2, 9)}`,
title: item.name,
startTime: this.formatTimeToHHMM(item.startTime),
endTime: this.formatTimeToHHMM(item.endTime),
status: this.mapStatus(item.status),
rawData: item,
//
startMinutes: this.timeToMinutes(this.formatTimeToHHMM(item.startTime)),
endMinutes: this.timeToMinutes(this.formatTimeToHHMM(item.endTime)),
}));
},
timeToMinutes(time) {
if (!time) return 0;
const [hours, minutes] = time.split(":").map(Number);
return hours * 60 + minutes;
},
//
calculateTaskPositions() {
try {
//
this.taskGridPositions = {};
if (this.tasks.length === 0) {
return;
}
//
const sortedTasks = [...this.tasks].sort((a, b) => {
if (a.startMinutes !== b.startMinutes) {
return a.startMinutes - b.startMinutes;
}
//
return (
a.endMinutes - a.startMinutes - (b.endMinutes - b.startMinutes)
);
});
// 30
const gridWidth = 24 * 2; // 24 * 2= 48
const maxTracks = 10; //
//
const grid = Array(maxTracks)
.fill()
.map(() => Array(gridWidth).fill(null));
//
sortedTasks.forEach((task) => {
const startSlot = Math.floor(task.startMinutes / 30);
const endSlot = Math.ceil(task.endMinutes / 30);
const durationSlots = Math.max(1, endSlot - startSlot);
// slot
const validStartSlot = Math.max(
0,
Math.min(startSlot, gridWidth - 1)
);
const validEndSlot = Math.max(
validStartSlot + 1,
Math.min(endSlot, gridWidth)
);
const validDuration = Math.max(1, validEndSlot - validStartSlot);
//
let assignedTrack = -1;
for (let track = 0; track < maxTracks; track++) {
let canPlace = true;
// validStartSlotvalidDuration
for (
let slot = validStartSlot;
slot < validStartSlot + validDuration && slot < gridWidth;
slot++
) {
if (grid[track][slot] !== null) {
canPlace = false;
break;
}
}
if (canPlace) {
assignedTrack = track;
break;
}
}
//
if (assignedTrack !== -1) {
//
for (
let slot = validStartSlot;
slot < validStartSlot + validDuration && slot < gridWidth;
slot++
) {
grid[assignedTrack][slot] = task.id;
}
//
this.taskGridPositions[task.id] = {
track: assignedTrack,
startSlot: validStartSlot,
durationSlots: validDuration,
//
widthPercentage: 100 / maxTracks, //
leftPercentage: assignedTrack * (100 / maxTracks),
};
console.log(
`任务 ${task.title} 放置在轨道 ${assignedTrack}, 从格子 ${validStartSlot} 开始, 持续 ${validDuration} 个格子`
);
} else {
// console.warn(` ${task.title} `);
}
});
console.log("任务网格位置计算完成");
console.log(
"网格占用情况:",
grid.map(
(track, index) =>
`${index}: [${track.map((cell) => (cell ? "X" : ".")).join("")}]`
)
);
} catch (error) {
console.error("计算任务网格位置时出错:", error);
this.taskGridPositions = {};
}
},
//
getTaskStyle(task) {
try {
const startMinutes = this.timeToMinutes(task.startTime);
const endMinutes = this.timeToMinutes(task.endTime);
//
const top = startMinutes * 1; // -60
const height = Math.max(20, (endMinutes - startMinutes) * 1);
//
const positionInfo = this.taskGridPositions[task.id];
let widthPercentage = 8; //
let leftPercentage = 0;
if (positionInfo) {
widthPercentage = positionInfo.widthPercentage - 0.5; //
leftPercentage = positionInfo.leftPercentage + 0.25; //
} else {
//
widthPercentage = 8;
leftPercentage = 0.25;
}
return {
height: `${height}px`,
top: `${top}px`,
width: `${widthPercentage}%`,
left: `${leftPercentage}%`,
position: "absolute",
"z-index": positionInfo ? positionInfo.track + 1 : 1,
};
} catch (error) {
console.error("计算任务样式时出错:", error, task);
return {
height: "60px",
top: "0px",
width: "8%",
left: "0.25%",
position: "absolute",
"z-index": 1,
};
}
},
handleDateChange() {
this.getCalenderDay();
},
async getCalenderDay() {
const requestId = ++this.currentRequestId;
this.loading = true;
const year = this.value.getFullYear();
const month = this.value.getMonth() + 1;
const day = this.value.getDate();
try {
//
this.tasks = [];
this.taskGridPositions = {};
const res = await calenderDay({
year: year,
month: month,
day: day,
});
//
if (requestId !== this.currentRequestId) {
console.log("收到旧的响应,已忽略");
return;
}
if (res && Array.isArray(res)) {
this.tasks = this.processApiData(res);
//
this.calculateTaskPositions();
} else {
this.tasks = [];
console.warn("接口返回数据格式不正确");
}
} catch (error) {
if (requestId !== this.currentRequestId) {
return;
}
console.error("获取日历数据失败:", error);
this.tasks = [];
this.$message.error("加载任务数据失败");
} finally {
if (requestId === this.currentRequestId) {
this.loading = false;
}
}
},
},
};
</script>
<style scoped>
.day-view-container {
width: 100%;
height: 100%;
border-radius: 8px;
overflow: hidden;
position: relative;
}
.day-view-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
border-bottom: 1px solid #40486a;
color: #fff;
}
.header-content {
display: flex;
flex-direction: column;
gap: 8px;
}
.current-date {
font-size: 18px;
font-weight: bold;
}
.time-week-info {
display: flex;
justify-content: space-between;
align-items: center;
width: 200px;
font-size: 14px;
color: #7984b1;
}
.current-time {
flex: 1;
text-align: left;
}
.week-day {
flex: 1;
text-align: right;
}
.day-view-content {
display: flex;
height: calc(100% - 80px);
overflow-x: hidden;
overflow-y: auto;
position: relative;
scrollbar-width: none;
-ms-overflow-style: none;
}
.day-view-content::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
.time-axis {
width: 70px;
border-right: 1px solid #40486a;
padding: 0;
flex-shrink: 0;
}
.time-slot {
height: 60px;
position: relative;
}
.time-label {
height: 30px;
line-height: 30px;
font-size: 16px;
color: #7984b1;
text-align: center;
position: relative;
border-right: 1px solid #40486a;
}
.hour-label {
font-weight: 500;
border-bottom: 1px dashed #40486a;
}
.half-hour-label {
border-bottom: 1px solid #40486a;
}
.tasks-container {
flex: 1;
position: relative;
margin-left: -1px;
min-height: 300px;
}
.grid-lines {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
}
.grid-line {
height: 30px;
border-bottom: 1px dashed #40486a;
background: #202f3e;
}
.grid-line:nth-child(odd) {
border-bottom: 1px solid #40486a;
background: #172533;
}
.task-item {
position: absolute;
border-radius: 4px;
padding: 4px 8px;
font-size: 12px;
overflow: hidden;
cursor: pointer;
box-sizing: border-box;
white-space: nowrap;
text-overflow: ellipsis;
transition: all 0.3s ease;
border: 1px solid rgba(255, 255, 255, 0.1);
min-height: 20px;
}
.task-item:hover {
opacity: 0.9;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
z-index: 100 !important;
}
.task-time {
font-size: 10px;
margin-bottom: 2px;
opacity: 0.9;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
display: flex;
align-items: center;
}
.task-title-inline {
display: inline-block;
max-width: 60%;
overflow: hidden;
text-overflow: ellipsis;
margin-left: 4px;
flex-shrink: 1;
}
.task-title {
font-size: 11px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
}
/* 加载状态样式 */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(23, 37, 51, 0.8);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 10;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid #40486a;
border-top-color: #247382;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 10px;
}
.loading-text {
color: #7984b1;
font-size: 14px;
}
/* 无数据样式 */
.no-data {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #7984b1;
font-size: 16px;
text-align: center;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.task-status-running {
background-color: rgba(36, 115, 130, 0.9) !important;
color: #fff;
/* border-left: 3px solid #1a9ba8; */
}
.task-status-completed {
background-color: rgba(41, 106, 90, 0.9) !important;
color: #fff;
/* border-left: 3px solid #1f8b6e; */
}
.task-status-pending {
background-color: rgba(109, 125, 146, 0.9) !important;
color: #fff;
/* border-left: 3px solid #8a9bb3; */
}
/* 为不同轨道添加不同的背景色提示 */
.task-item[style*="left: 0.25%"] {
background-color: rgba(36, 115, 130, 0.85);
}
.task-item[style*="left: 8.75%"] {
background-color: rgba(41, 106, 90, 0.85);
}
.task-item[style*="left: 17.25%"] {
background-color: rgba(106, 41, 82, 0.85);
}
.task-item[style*="left: 25.75%"] {
background-color: rgba(106, 82, 41, 0.85);
}
.task-item[style*="left: 34.25%"] {
background-color: rgba(41, 82, 106, 0.85);
}
/* 滚动条样式 */
.tasks-container::-webkit-scrollbar,
.time-axis::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.tasks-container::-webkit-scrollbar-track,
.time-axis::-webkit-scrollbar-track {
background: transparent;
}
.tasks-container::-webkit-scrollbar-thumb,
.time-axis::-webkit-scrollbar-thumb {
background: #40486a;
border-radius: 3px;
}
.tasks-container::-webkit-scrollbar-thumb:hover,
.time-axis::-webkit-scrollbar-thumb:hover {
background: #4d5779;
}
/* Element UI 日期选择器样式 */
:deep(.card-date-editor) {
background-color: #172533;
border-color: #40486a;
color: #fff;
}
:deep(.el-date-picker__header) {
border-bottom-color: #40486a;
}
:deep(.el-date-picker__header-label) {
color: #fff;
}
:deep(.el-date-table th) {
color: #7984b1;
}
:deep(.el-date-table td) {
color: #fff;
}
:deep(.el-date-table td.current:not(.disabled) > div) {
background-color: #247382;
}
:deep(.el-date-table td.today:not(.current) > div) {
color: #247382;
}
</style>

+ 162
- 0
src/views/cards/tasksShow/index.vue View File

@ -0,0 +1,162 @@
<template>
<CardBox
class="card-page"
:showTitle="false"
>
<div class="view-content">
<YearView
v-if="currentView === 'year'"
:year="currentYear"
@year-changed="updateYear"
/>
<MonthView
v-if="currentView === 'month'"
:year="currentYear"
:month="currentMonth"
@update:year="updateYear"
@update:month="updateMonth"
/>
<DayView
v-if="currentView === 'day'"
:year="currentYear"
:month="currentMonth"
:day="currentDay"
/>
</div>
<!-- </div> -->
</CardBox>
</template>
<script>
import YearView from "./yearDisplay.vue";
import MonthView from "./monthlyDisplay.vue";
import DayView from "./dailyDisplay.vue";
import CardBox from "../components/cardBox.vue";
export default {
name: "PatrolSchedule",
components: {
CardBox,
YearView,
MonthView,
DayView,
},
data() {
const today = new Date();
const routeView = this.$route.query.view;
return {
currentView: ["year", "month", "day"].includes(routeView)
? routeView
: "year",
currentYear: today.getFullYear(),
currentMonth: today.getMonth() + 1,
currentDay: today.getDate(),
};
},
watch: {
"$route.query.view"(view) {
if (["year", "month", "day"].includes(view)) {
this.currentView = view;
}
},
},
methods: {
//
updateYear(year) {
this.currentYear = year;
},
updateMonth(month) {
this.currentMonth = month;
},
},
};
</script>
<style lang="scss" scoped>
/* .calendar-container {
width: 100%;
max-width: 1200px;
margin: 0 auto;
font-family: Arial, sans-serif;
color: #ffffff;
background-color: #1a1a2e;
} */
.card-page .el-table td .cell,
.card-page .el-table td .cell div,
.card-page .el-table td .cell span {
color: #cbd5e7;
}
.card-page {
height: 100%;
min-height: 0;
overflow: hidden;
}
.calendar-header {
background-size: cover;
background-position: center;
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 8px 8px 0 0;
}
.date-title {
font-size: 24px;
font-weight: bold;
color: #ffffff;
}
.view-buttons {
display: flex;
gap: 10px;
}
.view-btn {
background-size: cover;
background-position: center;
border: none;
padding: 10px 20px;
color: #ffffff;
font-weight: bold;
cursor: pointer;
border-radius: 4px;
transition: all 0.3s ease;
}
.view-btn:hover {
opacity: 0.8;
}
.view-btn.active {
transform: scale(1.05);
box-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
}
.view-content {
// padding: 20px;
// background-color: #16213e;
border-radius: 0 0 8px 8px;
height: 100%;
min-height: 0;
overflow: auto;
scrollbar-width: none;
-ms-overflow-style: none;
}
.view-content::-webkit-scrollbar,
.view-content ::v-deep .tasks-container::-webkit-scrollbar,
.view-content ::v-deep .time-axis::-webkit-scrollbar,
.view-content ::v-deep .tasks-list::-webkit-scrollbar {
display: none !important;
width: 0 !important;
height: 0 !important;
}
.view-content ::v-deep .tasks-container,
.view-content ::v-deep .time-axis,
.view-content ::v-deep .tasks-list {
scrollbar-width: none !important;
-ms-overflow-style: none;
}
</style>

+ 791
- 0
src/views/cards/tasksShow/monthlyDisplay.vue View File

@ -0,0 +1,791 @@
<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使
// displayYearprop
// 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;
// getMonthDatawatch
}
}
},
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;
}
</style>

+ 300
- 0
src/views/cards/tasksShow/yearDisplay.vue View File

@ -0,0 +1,300 @@
<template>
<div class="year-view">
<div class="year-header">
<h2>{{ currentYear }}</h2>
<div class="year-controls">
<el-date-picker
v-model="selectedYear"
style="float: right; width: 120px"
type="year"
size="mini"
placeholder="选择年"
@change="onYearChange"
>
</el-date-picker>
</div>
</div>
<div class="months-grid">
<div class="month-card" v-for="month in 12" :key="month">
<div class="month-title">{{ getChineseMonth(month) }}</div>
<!-- <div class="month-stats" v-if="hasData(month)"> -->
<div class="month-stats">
<div class="stat-item">
正在执行:
<span class="status-executing">{{
getMonthData(month).executing
}}</span>
</div>
<div class="stat-item">
已经完成:
<span class="status-completed">{{
getMonthData(month).executed
}}</span>
</div>
<div class="stat-item">
:
<span class="status-not-executed">{{
getMonthData(month).pending
}}</span>
</div>
</div>
<!-- <div class="month-stats empty" v-else>
<div class="stat-item">无数据</div>
</div> -->
</div>
</div>
</div>
</template>
<script>
import { calenderYear } from "@/api/videosMonitor/index";
export default {
name: "YearView",
props: {
year: {
type: Number,
required: true,
},
},
data() {
//
const currentYear = new Date().getFullYear();
return {
selectedYear: new Date(currentYear, 0, 1),
// 使
currentYear: currentYear,
chineseMonths: [
"一月",
"二月",
"三月",
"四月",
"五月",
"六月",
"七月",
"八月",
"九月",
"十月",
"十一月",
"十二月",
],
//
yearDataMap: {},
loading: false,
};
},
watch: {
// prop
year(newYear) {
this.currentYear = newYear;
this.selectedYear = new Date(newYear, 0, 1);
//
this.getYearData();
},
},
mounted() {
//
this.getYearData();
},
methods: {
onYearChange() {
if (this.selectedYear) {
this.currentYear = this.selectedYear.getFullYear();
//
this.$emit("year-changed", this.currentYear);
//
this.getYearData();
}
},
//
async getYearData() {
try {
this.loading = true;
const response = await calenderYear({
year: this.currentYear,
});
//
const data = response?.rows || response || [];
//
const monthData = {};
data.forEach(item => {
monthData[item.month] = {
executed: item.executed || 0,
executing: item.executing || 0,
pending: item.pending || 0,
total: item.total || 0
};
});
//
this.yearDataMap = {
[this.currentYear]: monthData
};
} catch (error) {
console.error('获取年份数据失败:', error);
//
this.yearDataMap = {
[this.currentYear]: {}
};
} finally {
this.loading = false;
}
},
getMonthData(month) {
// null
const yearData = this.yearDataMap[this.currentYear];
//
if (!yearData) {
return {
executed: 0,
executing: 0,
pending: 0,
total: 0
};
}
//
const monthStats = yearData[month];
if (!monthStats) {
return {
executed: 0,
executing: 0,
pending: 0,
total: 0
};
}
//
return monthStats;
},
hasData(month) {
const data = this.getMonthData(month);
return data.executed > 0 || data.executing > 0 || data.pending > 0 || data.total > 0;
},
getChineseMonth(monthNumber) {
return this.chineseMonths[monthNumber - 1] || monthNumber + "月";
},
},
};
</script>
<style scoped>
.year-view {
width: 100%;
}
.year-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.year-header h2 {
margin: 0;
font-size: 20px;
color: #a1aad3;
}
.year-controls {
display: flex;
gap: 10px;
align-items: center;
}
.months-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 15px;
}
.month-card {
background-color: #172533;
padding: 50px 15px;
border-radius: 8px;
border: 2px solid #40486a;
transition: transform 0.3s ease;
}
.month-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(64, 72, 106, 0.3);
}
.month-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 15px;
color: #a1aad3;
text-align: center;
}
.month-stats {
display: flex;
flex-direction: column;
gap: 8px;
}
.month-stats.empty {
text-align: center;
padding: 20px 0;
}
.stat-item {
font-size: 14px;
color: #6b7491;
}
.status-executing {
color: #fff;
font-weight: bold;
}
.status-completed {
color: #fff;
font-weight: bold;
}
.status-not-executed {
color: #fff;
font-weight: bold;
}
/* 覆盖Element UI的默认样式 */
:deep(.el-date-editor--year) {
background-color: #172533;
border: 2px solid #40486a;
border-radius: 4px;
}
:deep(.el-input__inner) {
background-color: #172533;
color: #a1aad3;
border: 2px solid #40486a;
}
:deep(.el-input__inner::placeholder) {
color: #6b7491;
}
:deep(.el-date-picker__header-label) {
color: #a1aad3;
}
:deep(.el-date-picker__year-btn) {
color: #a1aad3;
}
:deep(.el-date-picker__year-btn:hover) {
color: #1a53ff;
}
:deep(.el-picker-panel) {
background-color: #172533;
border: 2px solid #40486a;
}
</style>

Loading…
Cancel
Save