957 lines
28 KiB
Vue
957 lines
28 KiB
Vue
<template>
|
||
<dv-full-screen-container :style="{background: '#0f1c2b', color: '#fff'}">
|
||
<!-- 顶部标题 -->
|
||
<dv-border-box-8 style="height:80px; padding: 10px;">
|
||
<div style="font-size: 38px; font-weight: 600; text-align: center; color: #40e0d0;">设计部门任务进度看板</div>
|
||
<div style="font-size: 28px;position: absolute; top: 50%; right: 100px; transform: translateY(-50%); font-size: 28px; font-weight: 600; color: #40e0d0;">{{ getCurrentTime() }}</div>
|
||
</dv-border-box-8>
|
||
|
||
<!-- 中间布局 -->
|
||
<div style="display:flex; height: calc(100% - 100px); padding: 10px;">
|
||
|
||
<!-- 左侧:轮播表 -->
|
||
<dv-border-box-2 style="flex:2; margin-right: 10px; padding:10px;">
|
||
<dv-scroll-board :config="tableConfig" style="width:100%; height:100%;"/>
|
||
</dv-border-box-2>
|
||
|
||
<!-- 右侧:图表区 -->
|
||
<div style="flex:1; display: flex; flex-direction: column; gap: 10px;">
|
||
|
||
<!-- 超期项目 -->
|
||
<dv-border style="flex:1; padding:10px;">
|
||
<div style="height: 100%; display: flex; flex-direction: column;">
|
||
<!-- 标题和统计信息 -->
|
||
<div style="display: flex; justify-content: center; gap: 20px; align-items: center; margin-bottom: 10px;">
|
||
<h4 style="margin: 0; color: #ff6b6b; font-size: 24px;font-weight: 600;">超期项目</h4>
|
||
<div style="display: flex; gap: 15px; font-size: 18px;font-weight: 600;">
|
||
<span style="color: #ff6b6b;">总超期: {{ overdueStats.total }}项</span>
|
||
<span style="color: #ffa726;">严重超期: {{ overdueStats.critical }}项</span>
|
||
<span style="color: #42a5f5;">一般超期: {{ overdueStats.normal }}项</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 超期项目列表 -->
|
||
<div class="overdue-list" style="flex: 2; overflow-y: auto;">
|
||
<!-- 轮播显示:当项目超过3条时启用 -->
|
||
<div
|
||
v-if="overdueProjects.length > 3"
|
||
class="carousel-container"
|
||
@mouseenter="stopCarousel"
|
||
@mouseleave="startCarousel"
|
||
>
|
||
<div
|
||
v-for="(item, index) in visibleOverdueProjects"
|
||
:key="`carousel-${index}`"
|
||
class="overdue-item"
|
||
:class="getOverdueLevelClass(item.overdueDays)"
|
||
>
|
||
<div class="item-header">
|
||
<span class="project-code">{{ item.projectCode }}</span>
|
||
<span class="overdue-days" v-if="item.overdueDays > 0" :class="getOverdueLevelClass(item.overdueDays)">
|
||
超期{{ item.overdueDays }}天
|
||
</span>
|
||
</div>
|
||
<div class="item-content">
|
||
<div class="designer">{{ item.designer }}</div>
|
||
<div class="description">{{ item.description }}</div>
|
||
<div class="time-info">
|
||
<span>开始: {{ item.startDate }}</span>
|
||
<span>计划: {{ item.planDate }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 普通显示:当项目不超过1条时 -->
|
||
<div v-else>
|
||
<div
|
||
v-for="(item, index) in overdueProjects"
|
||
:key="index"
|
||
class="overdue-item"
|
||
:class="getOverdueLevelClass(item.overdueDays)"
|
||
>
|
||
<div class="item-header">
|
||
<span class="project-code">{{ item.projectCode }}</span>
|
||
<span class="overdue-days" :class="getOverdueLevelClass(item.overdueDays)">
|
||
{{ item.overdueDays * -1 }}天
|
||
</span>
|
||
</div>
|
||
<div class="item-content">
|
||
<div class="designer">{{ item.designer }}</div>
|
||
<div class="description">{{ item.description }}</div>
|
||
<div class="time-info">
|
||
<span>开始: {{ item.startDate }}</span>
|
||
<span>计划: {{ item.planDate }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 无超期项目时的提示 -->
|
||
<div v-if="overdueProjects.length === 0" class="no-overdue">
|
||
<div style="text-align: center; color: #4caf50; padding: 20px;">
|
||
<i class="el-icon-success" style="font-size: 24px;"></i>
|
||
<div style="margin-top: 10px;">暂无超期项目</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</dv-border>
|
||
|
||
<!-- 临期项目排名轮播表 -->
|
||
<dv-border-box-10 style="flex:1; padding:10px;">
|
||
<div style="height: 100%; display: flex; flex-direction: column;">
|
||
<!-- 标题 -->
|
||
<div style="text-align: center; margin-bottom: 10px;">
|
||
<h3 style="margin: 0; color: #ffa726; font-size: 20px;">临期项目</h3>
|
||
</div>
|
||
|
||
<!-- 临期项目轮播表 -->
|
||
<div class="deadline-list" style="flex: 1; overflow: hidden;">
|
||
<div
|
||
v-if="overdueProjects.length > 3"
|
||
class="deadline-carousel"
|
||
@mouseenter="stopDeadlineCarousel"
|
||
@mouseleave="startDeadlineCarousel"
|
||
>
|
||
<div
|
||
v-for="(item, index) in visibleDeadlineProjects"
|
||
:key="`deadline-${index}`"
|
||
class="deadline-item"
|
||
:class="getDeadlineLevelClass(item.overdueDays)"
|
||
>
|
||
<div class="rank-number">{{ item.rank }}</div>
|
||
<div class="item-info">
|
||
<div class="designer-name" style="flex: 1;">{{ item.designer }}</div>
|
||
<div style="display: flex;flex: 1;">
|
||
<div v-if="item.overdueDays > 0" class="deadline-days" :class="getDeadlineLevelClass(item.overdueDays)">临期{{ item.overdueDays }}天</div>
|
||
</div>
|
||
<div class="customer-center">{{item.projectCode}}</div>
|
||
<div class="customer-center">{{item.planDate}}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 轮播指示器 -->
|
||
<div class="deadline-indicators">
|
||
<span
|
||
v-for="(_, index) in deadlinePages"
|
||
:key="index"
|
||
class="deadline-indicator"
|
||
:class="{ active: index === currentDeadlineIndex }"
|
||
@click="goToDeadlinePage(index)"
|
||
></span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 普通显示:当项目不超过4条时 -->
|
||
<div v-else>
|
||
<div
|
||
v-for="(item, index) in overdueProjects"
|
||
:key="index"
|
||
class="deadline-item"
|
||
:class="getDeadlineLevelClass(item.overdueDays)"
|
||
>
|
||
<div class="rank-number">{{ index + 1 }}</div>
|
||
<div class="item-info">
|
||
<div class="designer-name">{{ item.designer }}</div>
|
||
<div class="deadline-days" :class="getDeadlineLevelClass(item.overdueDays)">
|
||
临期{{ item.overdueDays }}天
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 无临期项目时的提示 -->
|
||
<div v-if="overdueProjects.length === 0" class="no-deadline">
|
||
<div style="text-align: center; color: #4caf50; padding: 20px;">
|
||
<i class="el-icon-success" style="font-size: 24px;"></i>
|
||
<div style="margin-top: 10px;">暂无临期项目</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</dv-border-box-10>
|
||
|
||
<!-- 项目数量热力图(周/月/年 x 设计人) -->
|
||
<dv-border-box-13 style="flex:1; padding:10px;">
|
||
<div style="display:flex; justify-content: space-between; align-items:center; margin-bottom: 8px;">
|
||
<h4 style="margin:0; color:#40e0d0;">项目方案数</h4>
|
||
<el-radio-group v-model="timeGranularity" size="mini">
|
||
<el-radio-button label="week">周</el-radio-button>
|
||
<el-radio-button label="month">月</el-radio-button>
|
||
<el-radio-button label="year">年</el-radio-button>
|
||
</el-radio-group>
|
||
</div>
|
||
<dv-capsule-chart :config="capsuleConfig" style="width:100%; height:calc(100% - 30px);" />
|
||
</dv-border-box-13>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Loading -->
|
||
<dv-loading v-if="loading">加载中...</dv-loading>
|
||
</dv-full-screen-container>
|
||
</template>
|
||
|
||
<script>
|
||
import { listProPlan, listProPlanOverdue ,listProPlanExpirys} from '@/api/system/proPlan'
|
||
export default {
|
||
data () {
|
||
return {
|
||
loading: false,
|
||
timer: null, // 定时器
|
||
carouselTimer: null, // 轮播定时器
|
||
currentCarouselIndex: 0, // 当前轮播索引
|
||
carouselInterval: 4000, // 轮播间隔时间(毫秒)
|
||
deadlineTimer: null, // 临期项目轮播定时器
|
||
currentDeadlineIndex: 0, // 当前临期项目轮播索引
|
||
deadlineInterval: 4000, // 临期项目轮播间隔时间(毫秒)
|
||
// 超期项目数据
|
||
overdueProjects: [],
|
||
// 临期项目数据
|
||
expiryOverdueProjects: [],
|
||
tableConfig: {
|
||
header: ['执行令号/方案号', '设计人', '内容', '开始时间', '完成时间', '重要程度'],
|
||
data: [],
|
||
index: true,
|
||
// columnWidth: [120, 200, 230, 400, 150, 150, 200, 100],
|
||
align: ['center','center','center','center','center','center','center'],
|
||
rowNum: 8,
|
||
waitTime: 1500,
|
||
hoverPause:false
|
||
}
|
||
,
|
||
timeGranularity: 'month'
|
||
}
|
||
},
|
||
created() {
|
||
// 初始化超期项目数据,动态计算超期天数
|
||
this.initProjects();
|
||
// 拉取方案列表填充滚动表格
|
||
this.fetchProPlanList();
|
||
// 拉取超期项目
|
||
this.fetchOverdue();
|
||
// 拉取临期项目
|
||
this.ExpiryOverdue();
|
||
},
|
||
mounted() {
|
||
// 每小时更新一次天数
|
||
this.timer = setInterval(() => {
|
||
this.initProjects();
|
||
}, 60 * 60 * 1000); // 1小时
|
||
|
||
// 启动轮播(当项目超过3条时)
|
||
this.startCarousel();
|
||
|
||
// 启动临期项目轮播(当项目超过4条时)
|
||
this.startDeadlineCarousel();
|
||
},
|
||
beforeDestroy() {
|
||
// 清理定时器
|
||
if (this.timer) {
|
||
clearInterval(this.timer);
|
||
}
|
||
if (this.carouselTimer) {
|
||
clearInterval(this.carouselTimer);
|
||
}
|
||
if (this.deadlineTimer) {
|
||
clearInterval(this.deadlineTimer);
|
||
}
|
||
},
|
||
computed: {
|
||
// 超期项目统计
|
||
overdueStats() {
|
||
console.log(this.overdueProjects);
|
||
const total = this.overdueProjects.length;
|
||
const critical = this.overdueProjects.filter(item => item.overdueDays > 5).length;
|
||
const normal = this.overdueProjects.filter(item => item.overdueDays <= 5).length;
|
||
|
||
return { total, critical, normal };
|
||
},
|
||
// 轮播相关计算属性
|
||
carouselPages() {
|
||
return Math.ceil(this.overdueProjects.length / 3);
|
||
},
|
||
visibleOverdueProjects() {
|
||
const start = this.currentCarouselIndex * 3;
|
||
const end = start + 3;
|
||
return this.overdueProjects.slice(start, end);
|
||
},
|
||
// 临期项目相关计算属性
|
||
deadlinePages() {
|
||
return Math.ceil(this.expiryOverdueProjects.length / 3);
|
||
},
|
||
visibleDeadlineProjects() {
|
||
const start = this.currentDeadlineIndex * 3;
|
||
const end = start + 3;
|
||
return this.expiryOverdueProjects.slice(start, end).map((item, index) => ({
|
||
...item,
|
||
rank: start + index + 1
|
||
}));
|
||
},
|
||
heatmapOption() {
|
||
const designers = this.getUniqueDesigners();
|
||
const timeLabels = this.getTimeLabels(this.timeGranularity);
|
||
|
||
const indexOfDesigner = new Map();
|
||
designers.forEach((d, i) => indexOfDesigner.set(d, i));
|
||
const indexOfTime = new Map();
|
||
timeLabels.forEach((t, i) => indexOfTime.set(t, i));
|
||
|
||
const counts = Array.from({ length: designers.length }, () => Array(timeLabels.length).fill(0));
|
||
|
||
(this.tableConfig.data || []).forEach(row => {
|
||
const designer = row[1];
|
||
const dateStr = row[4];
|
||
if (!designer || !dateStr) return;
|
||
const label = this.formatTimeLabel(dateStr, this.timeGranularity);
|
||
if (!indexOfDesigner.has(designer) || !indexOfTime.has(label)) return;
|
||
counts[indexOfDesigner.get(designer)][indexOfTime.get(label)] += 1;
|
||
});
|
||
|
||
const seriesData = [];
|
||
let maxVal = 0;
|
||
for (let y = 0; y < designers.length; y++) {
|
||
for (let x = 0; x < timeLabels.length; x++) {
|
||
const val = counts[y][x];
|
||
maxVal = Math.max(maxVal, val);
|
||
seriesData.push([x, y, val]);
|
||
}
|
||
}
|
||
|
||
return {
|
||
backgroundColor: 'transparent',
|
||
tooltip: {
|
||
position: 'top',
|
||
formatter: (p) => {
|
||
const t = timeLabels[p.data[0]];
|
||
const d = designers[p.data[1]];
|
||
const v = p.data[2];
|
||
return `${d}<br/>${t}: ${v}`;
|
||
}
|
||
},
|
||
grid: { top: 20, right: 10, bottom: 20, left: 80 },
|
||
xAxis: {
|
||
type: 'category',
|
||
data: timeLabels,
|
||
axisLabel: { color: '#e2e8f0' },
|
||
axisLine: { lineStyle: { color: '#334155' } },
|
||
splitLine: { show: false }
|
||
},
|
||
yAxis: {
|
||
type: 'category',
|
||
data: designers,
|
||
axisLabel: { color: '#e2e8f0' },
|
||
axisLine: { lineStyle: { color: '#334155' } },
|
||
splitLine: { show: false }
|
||
},
|
||
visualMap: {
|
||
min: 0,
|
||
max: Math.max(5, maxVal),
|
||
calculable: true,
|
||
orient: 'vertical',
|
||
right: 0,
|
||
top: 'middle',
|
||
textStyle: { color: '#e2e8f0' },
|
||
inRange: {
|
||
color: ['#0f172a', '#22d3ee']
|
||
}
|
||
},
|
||
series: [
|
||
{
|
||
type: 'heatmap',
|
||
data: seriesData,
|
||
label: { show: false },
|
||
emphasis: { itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0,0,0,0.5)' } }
|
||
}
|
||
]
|
||
};
|
||
},
|
||
capsuleConfig() {
|
||
const designers = this.getUniqueDesigners();
|
||
const timeLabels = this.getTimeLabels(this.timeGranularity);
|
||
const latestLabel = timeLabels[timeLabels.length - 1];
|
||
|
||
const countsByDesigner = new Map();
|
||
designers.forEach(d => countsByDesigner.set(d, 0));
|
||
|
||
(this.tableConfig.data || []).forEach(row => {
|
||
const designer = row[1];
|
||
const dateStr = row[4];
|
||
if (!designer || !dateStr) return;
|
||
const label = this.formatTimeLabel(dateStr, this.timeGranularity);
|
||
if (label === latestLabel && countsByDesigner.has(designer)) {
|
||
countsByDesigner.set(designer, countsByDesigner.get(designer) + 1);
|
||
}
|
||
});
|
||
|
||
const data = Array.from(countsByDesigner.entries())
|
||
.map(([name, value]) => ({ name, value }))
|
||
.sort((a, b) => b.value - a.value);
|
||
|
||
return {
|
||
data,
|
||
colors: ['#ef4444', '#f59e0b', '#22c55e', '#3b82f6', '#8b5cf6', '#06b6d4', '#ea580c'],
|
||
unit: '项',
|
||
showValue: true
|
||
};
|
||
}
|
||
},
|
||
methods: {
|
||
getCurrentTime() {
|
||
const now = new Date();
|
||
const year = now.getFullYear();
|
||
const month = String(now.getMonth() + 1).padStart(2, '0'); // 月份是从0开始的,所以加1
|
||
const day = String(now.getDate()).padStart(2, '0');
|
||
const hours = String(now.getHours()).padStart(2, '0');
|
||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||
const seconds = String(now.getSeconds()).padStart(2, '0');
|
||
|
||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||
},
|
||
|
||
// 初始化项目数据
|
||
initProjects() {
|
||
const today = new Date();
|
||
|
||
// 超期项目天数更新
|
||
this.overdueProjects.forEach(project => {
|
||
const planDate = new Date(project.planDate);
|
||
const diffTime = planDate.getTime() - today.getTime();
|
||
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
||
|
||
// 超期天数:正数显示超期天数,负数不显示
|
||
project.overdueDays = diffDays > 0 ? diffDays : null;
|
||
});
|
||
|
||
// 临期项目天数更新
|
||
this.expiryOverdueProjects.forEach(project => {
|
||
const planDate = new Date(project.planDate);
|
||
const diffTime = planDate.getTime() - today.getTime();
|
||
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
||
|
||
// 临期天数:正数显示剩余天数,负数不显示
|
||
project.overdueDays = diffDays > 0 ? diffDays : null;
|
||
});
|
||
|
||
// 按临期天数排序,临期时间短的排在前面
|
||
// this.overdueProjects.sort((a, b) => {
|
||
// if (a.overdueDays === null) return 1;
|
||
// if (b.overdueDays === null) return -1;
|
||
// return a.overdueDays - b.overdueDays;
|
||
// });
|
||
|
||
// 重新启动超期项目轮播
|
||
this.startCarousel();
|
||
|
||
// 重新启动临期项目轮播
|
||
this.startDeadlineCarousel();
|
||
},
|
||
|
||
// 获取超期等级样式类
|
||
getOverdueLevelClass(overdueDays) {
|
||
if (overdueDays > 5) {
|
||
return 'critical-overdue';
|
||
} else if (overdueDays > 2) {
|
||
return 'normal-overdue';
|
||
} else {
|
||
return 'light-overdue';
|
||
}
|
||
},
|
||
// 获取临期等级样式类
|
||
getDeadlineLevelClass(deadlineDays) {
|
||
if (deadlineDays > 5) {
|
||
return 'critical-deadline';
|
||
} else if (deadlineDays > 2) {
|
||
return 'normal-deadline';
|
||
} else {
|
||
return 'light-deadline';
|
||
}
|
||
},
|
||
// 轮播相关方法
|
||
goToPage(index) {
|
||
this.currentCarouselIndex = index;
|
||
},
|
||
// 临期项目轮播相关方法
|
||
goToDeadlinePage(index) {
|
||
this.currentDeadlineIndex = index;
|
||
},
|
||
|
||
// 启动临期项目轮播
|
||
startDeadlineCarousel() {
|
||
// 先停止现有的轮播
|
||
this.stopDeadlineCarousel();
|
||
|
||
if (this.expiryOverdueProjects.length > 4) {
|
||
this.deadlineTimer = setInterval(() => {
|
||
this.nextDeadlinePage();
|
||
}, this.deadlineInterval);
|
||
}
|
||
},
|
||
|
||
// 停止临期项目轮播
|
||
stopDeadlineCarousel() {
|
||
if (this.deadlineTimer) {
|
||
clearInterval(this.deadlineTimer);
|
||
this.deadlineTimer = null;
|
||
}
|
||
},
|
||
|
||
// 临期项目下一页
|
||
nextDeadlinePage() {
|
||
if (this.deadlinePages > 1) {
|
||
this.currentDeadlineIndex = (this.currentDeadlineIndex + 1) % this.deadlinePages;
|
||
}
|
||
},
|
||
|
||
// 临期项目上一页
|
||
prevDeadlinePage() {
|
||
if (this.deadlinePages > 1) {
|
||
this.currentDeadlineIndex = this.currentDeadlineIndex === 0
|
||
? this.deadlinePages - 1
|
||
: this.currentDeadlineIndex - 1;
|
||
}
|
||
},
|
||
|
||
// 启动轮播
|
||
startCarousel() {
|
||
// 先停止现有的轮播
|
||
this.stopCarousel();
|
||
|
||
if (this.overdueProjects.length > 3) {
|
||
this.carouselTimer = setInterval(() => {
|
||
this.nextPage();
|
||
}, this.carouselInterval);
|
||
}
|
||
},
|
||
|
||
// 停止轮播
|
||
stopCarousel() {
|
||
if (this.carouselTimer) {
|
||
clearInterval(this.carouselTimer);
|
||
this.carouselTimer = null;
|
||
}
|
||
},
|
||
|
||
// 下一页
|
||
nextPage() {
|
||
if (this.carouselPages > 1) {
|
||
this.currentCarouselIndex = (this.currentCarouselIndex + 1) % this.carouselPages;
|
||
}
|
||
},
|
||
|
||
// 上一页
|
||
prevPage() {
|
||
if (this.carouselPages > 1) {
|
||
this.currentCarouselIndex = this.currentCarouselIndex === 0
|
||
? this.carouselPages - 1
|
||
: this.currentCarouselIndex - 1;
|
||
}
|
||
},
|
||
// 获取临期数据
|
||
async ExpiryOverdue(){
|
||
try {
|
||
this.loading = true;
|
||
const res = await listProPlanExpirys();
|
||
const list = res?.rows || res || [];
|
||
this.expiryOverdueProjects = (list || []).map(item => ({
|
||
projectCode: item.projectCode || item.code || '-',
|
||
designer: item.designer || '-',
|
||
description: item.description || item.remark || '',
|
||
startDate: (item.startDate || '').toString().slice(0, 10),
|
||
planDate: (item.planDate || item.endDate || '').toString().slice(0, 10),
|
||
overdueDays: Number(item.overdueDays ?? 0)
|
||
}));
|
||
// 排序与轮播刷新
|
||
this.expiryOverdueProjects.sort((a, b) => b.overdueDays - a.overdueDays);
|
||
this.startDeadlineCarousel();
|
||
} catch (e) {
|
||
console.error('fetchOverdue error:', e);
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
},
|
||
// 从后端加载超期数据并刷新视图
|
||
async fetchOverdue() {
|
||
try {
|
||
this.loading = true;
|
||
const res = await listProPlanOverdue();
|
||
const list = res?.rows || res || [];
|
||
this.overdueProjects = (list || []).map(item => ({
|
||
projectCode: item.projectCode || item.code || '-',
|
||
designer: item.designer || '-',
|
||
description: item.description || item.remark || '',
|
||
startDate: (item.startDate || '').toString().slice(0, 10),
|
||
planDate: (item.planDate || item.endDate || '').toString().slice(0, 10),
|
||
overdueDays: Number(item.overdueDays ?? 0)
|
||
}));
|
||
// 排序与轮播刷新
|
||
this.overdueProjects.sort((a, b) => b.overdueDays - a.overdueDays);
|
||
this.startCarousel();
|
||
} catch (e) {
|
||
console.error('fetchOverdue error:', e);
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
},
|
||
// 拉取方案列表,填充左侧滚动表格
|
||
async fetchProPlanList() {
|
||
try {
|
||
const { rows = [] } = await listProPlan();
|
||
// 映射为表格行:[执行令号, 设计人, 内容, 开始时间, 完成时间, 重要程度]
|
||
const mapped = rows.map(item => [
|
||
item.projectCode || '-',
|
||
item.designer || '-',
|
||
item.remark || '-',
|
||
item.startDate || '-',
|
||
item.endDate || '-',
|
||
'—'
|
||
]);
|
||
this.tableConfig = {
|
||
...this.tableConfig,
|
||
data: mapped
|
||
};
|
||
} catch (e) {
|
||
console.error('fetchProPlanList error:', e);
|
||
}
|
||
},
|
||
getUniqueDesigners() {
|
||
const set = new Set();
|
||
(this.tableConfig.data || []).forEach(row => { if (row[1]) set.add(row[1]); });
|
||
(this.overdueProjects || []).forEach(item => { if (item.designer) set.add(item.designer); });
|
||
(this.expiryOverdueProjects || []).forEach(item => { if (item.designer) set.add(item.designer); });
|
||
return Array.from(set);
|
||
},
|
||
getTimeLabels(granularity) {
|
||
const labelsSet = new Set();
|
||
const add = (dateStr) => {
|
||
if (!dateStr) return;
|
||
const lbl = this.formatTimeLabel(dateStr, granularity);
|
||
if (lbl) labelsSet.add(lbl);
|
||
};
|
||
(this.tableConfig.data || []).forEach(row => {
|
||
add(row[4]);
|
||
});
|
||
const labels = Array.from(labelsSet);
|
||
labels.sort((a, b) => a.localeCompare(b));
|
||
return labels;
|
||
},
|
||
formatTimeLabel(dateStr, granularity) {
|
||
const d = new Date(dateStr);
|
||
if (isNaN(d.getTime())) return '';
|
||
const year = d.getFullYear();
|
||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||
if (granularity === 'year') {
|
||
return `${year}`;
|
||
}
|
||
if (granularity === 'month') {
|
||
return `${year}-${month}`;
|
||
}
|
||
const { week, isoYear } = this.getISOWeek(d);
|
||
return `${isoYear}-W${String(week).padStart(2, '0')}`;
|
||
},
|
||
getISOWeek(dateObj) {
|
||
const d = new Date(Date.UTC(dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate()));
|
||
const dayNum = d.getUTCDay() || 7;
|
||
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
||
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
||
const weekNo = Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
|
||
return { week: weekNo, isoYear: d.getUTCFullYear() };
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
body, html, #app {
|
||
margin: 0;
|
||
height: 100%;
|
||
background: #0f1c2b;
|
||
}
|
||
.grid-item {
|
||
margin: 30px;
|
||
}
|
||
|
||
/* 超期项目样式 */
|
||
.overdue-list {
|
||
scrollbar-width: thin;
|
||
scrollbar-color: #4a5568 #2d3748;
|
||
}
|
||
|
||
.overdue-list::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
|
||
.overdue-list::-webkit-scrollbar-track {
|
||
background: #2d3748;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.overdue-list::-webkit-scrollbar-thumb {
|
||
background: #4a5568;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.overdue-list::-webkit-scrollbar-thumb:hover {
|
||
background: #718096;
|
||
}
|
||
|
||
.overdue-item {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
margin-bottom: 10px;
|
||
border-left: 4px solid;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.overdue-item:hover {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
transform: translateX(5px);
|
||
}
|
||
|
||
.overdue-item.critical-overdue {
|
||
border-left-color: #ff6b6b;
|
||
background: rgba(255, 107, 107, 0.1);
|
||
}
|
||
|
||
.overdue-item.normal-overdue {
|
||
border-left-color: #ffa726;
|
||
background: rgba(255, 167, 38, 0.1);
|
||
}
|
||
|
||
.overdue-item.light-overdue {
|
||
border-left-color: #42a5f5;
|
||
background: rgba(66, 165, 245, 0.1);
|
||
}
|
||
|
||
.item-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.project-code {
|
||
font-weight: bold;
|
||
color: #fff;
|
||
font-size: 20px;
|
||
}
|
||
|
||
.overdue-days {
|
||
font-weight: bold;
|
||
font-size: 12px;
|
||
padding: 2px 8px;
|
||
border-radius: 12px;
|
||
background: rgba(255, 255, 255, 0.2);
|
||
}
|
||
|
||
.overdue-days.critical-overdue {
|
||
color: #ff6b6b;
|
||
background: rgba(255, 107, 107, 0.2);
|
||
}
|
||
|
||
.overdue-days.normal-overdue {
|
||
color: #ffa726;
|
||
background: rgba(255, 167, 38, 0.2);
|
||
}
|
||
|
||
.overdue-days.light-overdue {
|
||
color: #42a5f5;
|
||
background: rgba(66, 165, 245, 0.2);
|
||
}
|
||
|
||
.item-content {
|
||
font-size: 12px;
|
||
}
|
||
|
||
.designer {
|
||
color: #40e0d0;
|
||
font-weight: bold;
|
||
margin-bottom: 19px;
|
||
}
|
||
|
||
.description {
|
||
color: #e2e8f0;
|
||
margin-bottom: 6px;
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.time-info {
|
||
display: flex;
|
||
gap: 15px;
|
||
color: #a0aec0;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.no-overdue {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 100%;
|
||
}
|
||
|
||
|
||
|
||
/* 轮播容器样式 */
|
||
.carousel-container {
|
||
position: relative;
|
||
height: 100%;
|
||
}
|
||
|
||
|
||
|
||
/* 轮播项目动画 */
|
||
.overdue-item {
|
||
animation: fadeIn 0.5s ease-in-out;
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(10px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
/* 临期项目样式 */
|
||
.deadline-list {
|
||
scrollbar-width: thin;
|
||
scrollbar-color: #4a5568 #2d3748;
|
||
}
|
||
|
||
.deadline-list::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
|
||
.deadline-list::-webkit-scrollbar-track {
|
||
background: #2d3748;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.deadline-list::-webkit-scrollbar-thumb {
|
||
background: #4a5568;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.deadline-list::-webkit-scrollbar-thumb:hover {
|
||
background: #718096;
|
||
}
|
||
|
||
.deadline-item {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
margin-bottom: 10px;
|
||
border-left: 4px solid;
|
||
transition: all 0.3s ease;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.deadline-item:hover {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
transform: translateX(5px);
|
||
}
|
||
|
||
.deadline-item.critical-deadline {
|
||
border-left-color: #ff6b6b;
|
||
background: rgba(255, 107, 107, 0.1);
|
||
}
|
||
|
||
.deadline-item.normal-deadline {
|
||
border-left-color: #ffa726;
|
||
background: rgba(255, 167, 38, 0.1);
|
||
}
|
||
|
||
.deadline-item.light-deadline {
|
||
border-left-color: #42a5f5;
|
||
background: rgba(66, 165, 245, 0.1);
|
||
}
|
||
|
||
.rank-number {
|
||
font-weight: bold;
|
||
font-size: 18px;
|
||
color: #40e0d0;
|
||
min-width: 30px;
|
||
text-align: center;
|
||
}
|
||
|
||
.item-info {
|
||
display: flex;
|
||
flex: 1;
|
||
justify-content: space-around;
|
||
gap: 20px;
|
||
justify-items: center;
|
||
text-align: center;
|
||
align-items: center;
|
||
|
||
}
|
||
|
||
.designer-name {
|
||
font-weight: bold;
|
||
color: #fff;
|
||
font-size: 18px;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.customer-center{
|
||
display: flex;
|
||
align-items: center;
|
||
flex: 1;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.deadline-days {
|
||
margin: 0 auto;
|
||
font-weight: bold;
|
||
font-size: 16px;
|
||
padding: 5px 8px;
|
||
border-radius: 8px;
|
||
background: rgba(255, 255, 255, 0.2);
|
||
}
|
||
|
||
.deadline-days.critical-deadline {
|
||
color: #ff6b6b;
|
||
background: rgba(255, 107, 107, 0.2);
|
||
}
|
||
|
||
.deadline-days.normal-deadline {
|
||
color: #ffa726;
|
||
background: rgba(255, 167, 38, 0.2);
|
||
}
|
||
|
||
.deadline-days.light-deadline {
|
||
color: #42a5f5;
|
||
background: rgba(66, 165, 245, 0.2);
|
||
}
|
||
|
||
.header{
|
||
font-size: 22px !important;
|
||
font-weight: 600 !important;
|
||
}
|
||
|
||
.row-item {
|
||
font-size: 20px !important;
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
@keyframes fadeIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(10px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
</style>
|