250 lines
6.6 KiB
Vue
250 lines
6.6 KiB
Vue
<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>
|