Files
im-uniapp/pages/workbench/reportSchedule/gantt.vue

250 lines
6.6 KiB
Vue
Raw Normal View History

2025-07-16 11:25:16 +08:00
<template>
<view class="gantt-container">
<scroll-view scroll-x class="gantt-scroll-x">
<view class="gantt-table">
<!-- 时间轴头部 -->
<view class="gantt-header-row">
<view class="gantt-header-cell gantt-project-col">项目</view>
<view class="gantt-header-cell gantt-date-col" v-for="d in dateList" :key="d">{{ d.slice(5) }}</view>
</view>
<!-- 任务条 -->
<view v-for="(task, idx) in tasks" :key="idx" class="gantt-row">
<view class="gantt-project-col gantt-task-label">{{ task.projectName }}</view>
<view class="gantt-bar-area" :style="{ minWidth: (dateList.length * cellWidth) + 'rpx' }">
<view
class="gantt-bar"
:style="getBarStyle(task)"
@click="showDetail(task)"
>
<text class="gantt-bar-text">{{ task.scheduleName }}{{ task.headerName }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 详情弹窗 -->
<uni-popup ref="detailPopup" type="center" :mask-click="true">
<view class="detail-popup-content" v-if="currentTask">
<view class="detail-title">排产详情</view>
<view class="detail-row"><text class="detail-label">项目</text>{{ currentTask.projectName }}</view>
<view class="detail-row"><text class="detail-label">排产名称</text>{{ currentTask.scheduleName }}</view>
<view class="detail-row"><text class="detail-label">负责人</text>{{ currentTask.headerName }}</view>
<view class="detail-row"><text class="detail-label">开始日期</text>{{ currentTask.startDate }}</view>
<view class="detail-row"><text class="detail-label">结束日期</text>{{ currentTask.endDate }}</view>
<view class="detail-row"><text class="detail-label">备注</text>{{ currentTask.remark || '-' }}</view>
<view class="detail-btns">
<button class="detail-btn" @click="closeDetail">关闭</button>
</view>
</view>
</uni-popup>
</view>
</template>
<script>
import uniPopup from '@/uni_modules/uni-popup/components/uni-popup/uni-popup.vue'
function dateDiffInDays(a, b) {
return Math.floor((new Date(a) - new Date(b)) / (1000 * 60 * 60 * 24));
}
export default {
components: { uniPopup },
props: {
chartData: {
type: Object,
required: true
}
},
data() {
return {
currentTask: null,
cellWidth: 80 // 每个日期格子的宽度,单位 rpx
}
},
computed: {
tasks() {
if (!this.chartData || !this.chartData.series || !this.chartData.series[0] || !this.chartData.series[0].data) return []
return this.chartData.series[0].data
},
minDate() {
if (!this.tasks.length) return null
const min = this.tasks.reduce((min, t) => t.startDate < min ? t.startDate : min, this.tasks[0].startDate)
const d = new Date(min)
d.setDate(d.getDate() - 2)
return d.toISOString().slice(0, 10)
},
maxDate() {
if (!this.tasks.length) return null
const max = this.tasks.reduce((max, t) => t.endDate > max ? t.endDate : max, this.tasks[0].endDate)
const d = new Date(max)
d.setDate(d.getDate() + 2)
return d.toISOString().slice(0, 10)
},
dateList() {
if (!this.minDate || !this.maxDate) return []
const res = []
let cur = new Date(this.minDate)
const end = new Date(this.maxDate)
while (cur <= end) {
res.push(cur.toISOString().slice(0, 10))
cur.setDate(cur.getDate() + 1)
}
return res
}
},
methods: {
getBarStyle(task) {
if (!this.minDate || !this.maxDate) return {}
const startOffset = dateDiffInDays(task.startDate, this.minDate)
const duration = dateDiffInDays(task.endDate, task.startDate) + 1
const left = startOffset * this.cellWidth + 'rpx'
const width = duration * this.cellWidth + 'rpx'
return {
left,
width,
position: 'absolute',
height: '32rpx',
background: '#2979ff',
borderRadius: '8rpx',
color: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '24rpx',
padding: '0 12rpx',
boxSizing: 'border-box',
minWidth: '60rpx',
zIndex: 2
}
},
showDetail(task) {
this.currentTask = task
this.$refs.detailPopup.open('center')
},
closeDetail() {
this.$refs.detailPopup.close()
this.currentTask = null
}
}
}
</script>
<style scoped>
.gantt-container {
padding: 24rpx;
min-height: 100vh;
}
.gantt-scroll-x {
width: 100%;
overflow-x: auto;
}
.gantt-table {
min-width: 800rpx;
}
.gantt-header-row {
display: flex;
align-items: center;
border-radius: 8rpx 8rpx 0 0;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.03);
margin-bottom: 8rpx;
}
.gantt-header-cell {
text-align: center;
font-size: 24rpx;
color: #888;
padding: 0 8rpx;
flex-shrink: 0;
}
.gantt-project-col {
width: 200rpx;
min-width: 200rpx;
text-align: right;
font-weight: bold;
color: #2979ff;
background: #f0f7ff;
border-right: 1rpx solid #e0e0e0;
padding-right: 12rpx;
}
.gantt-date-col {
width: 80rpx;
min-width: 80rpx;
}
.gantt-row {
display: flex;
align-items: center;
min-height: 64rpx;
position: relative;
}
.gantt-task-label {
font-size: 24rpx;
color: #2979ff;
padding-right: 12rpx;
}
.gantt-bar-area {
flex: 1;
position: relative;
height: 64rpx;
background: transparent;
/* min-width 由内联 style 动态设置 */
}
.gantt-bar {
position: absolute;
top: 16rpx;
height: 40rpx;
background: #2979ff;
border-radius: 8rpx;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
padding: 0 12rpx;
min-width: 60rpx;
z-index: 2;
box-shadow: 0 2rpx 8rpx rgba(41,121,255,0.08);
cursor: pointer;
}
.gantt-bar-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 详情弹窗样式 */
.detail-popup-content {
background: #fff;
border-radius: 16rpx;
padding: 40rpx 32rpx 24rpx 32rpx;
min-width: 480rpx;
max-width: 90vw;
}
.detail-title {
font-size: 32rpx;
font-weight: bold;
color: #2979ff;
margin-bottom: 32rpx;
text-align: center;
}
.detail-row {
font-size: 28rpx;
color: #333;
margin-bottom: 18rpx;
display: flex;
align-items: flex-start;
}
.detail-label {
color: #888;
min-width: 120rpx;
display: inline-block;
}
.detail-btns {
margin-top: 32rpx;
text-align: center;
}
.detail-btn {
background: #2979ff;
color: #fff;
border: none;
border-radius: 8rpx;
padding: 16rpx 48rpx;
font-size: 28rpx;
}
</style>