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>
|