feat:甘特图菜单页-添加排产看板

pull/1/head
黄伟杰 4 weeks ago
parent 5653fa98c0
commit 1d8a70de9b

@ -0,0 +1,418 @@
<template>
<div class="schedule-card-view" :class="{ 'fullscreen-mode': isFullscreen }">
<!-- 工具栏 -->
<div class="schedule-toolbar">
<div class="schedule-legend">
<div class="legend-item">
<div class="legend-color" style="background: #5dade2"></div>
<span>已排产</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #52c41a"></div>
<span>已并工</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #faad14"></div>
<span>暂停</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #ff7875"></div>
<span>待入库</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #b37feb"></div>
<span>已入库</span>
</div>
</div>
<div class="toolbar-actions">
<el-button
circle
@click="toggleFullscreen"
>
<el-icon><FullScreen /></el-icon>
</el-button>
</div>
</div>
<!-- 设备卡片列表 -->
<div class="device-cards-container">
<div v-for="device in scheduleList" :key="device.deviceId" class="device-card">
<!-- 设备信息头 -->
<div class="device-header">
<div class="device-info">
<div class="device-name">{{ device.deviceName }}</div>
<div class="device-code">{{ device.deviceCode }}</div>
</div>
<div class="device-stats">
<div class="stat-item">
<span class="stat-label">计划数</span>
<span class="stat-value">{{ device.plans?.length || 0 }}</span>
</div>
<div class="stat-item">
<span class="stat-label">产能</span>
<span class="stat-value">{{ device.ratedCapacity || '-' }}</span>
</div>
</div>
</div>
<!-- 计划卡片列表 -->
<div class="plans-container">
<div
v-for="plan in device.plans"
:key="plan.planId"
class="plan-card"
:style="{ borderLeftColor: getPlanStatusColor(plan.planStatus) }"
>
<div class="plan-header">
<span class="plan-status-badge" :style="{ backgroundColor: getPlanStatusColor(plan.planStatus) }">
{{ getPlanStatusLabel(plan.planStatus) }}
</span>
<span class="plan-code">{{ plan.planCode }}</span>
</div>
<div class="plan-content">
<div class="plan-row">
<span class="plan-label">计划编号:</span>
<span class="plan-value">{{ plan.taskCode }}</span>
</div>
<div class="plan-row">
<span class="plan-label">产品:</span>
<span class="plan-value">{{ plan.productCode }} / {{ plan.productName }}</span>
</div>
<div class="plan-row">
<span class="plan-label">计划数量:</span>
<span class="plan-value">{{ plan.planNumber }}</span>
</div>
<div class="plan-row">
<span class="plan-label">交货日期:</span>
<span class="plan-value">{{ plan.deliveryDateStr || '-' }}</span>
</div>
<div class="plan-row">
<span class="plan-label">开始:</span>
<span class="plan-value">{{ plan.planStartTimeStr }}</span>
</div>
<div class="plan-row">
<span class="plan-label">结束:</span>
<span class="plan-value">{{ plan.planEndTimeStr }}</span>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="!device.plans || device.plans.length === 0" class="empty-state">
<el-empty description="暂无计划" :image-size="40" />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { FullScreen } from '@element-plus/icons-vue'
defineOptions({ name: 'ScheduleCardView' })
type UnifiedPlan = {
planId: number
planCode: string
taskCode: string
productCode: string
productName: string
planNumber: number
planStartTimeStr: string
planEndTimeStr: string
deliveryDateStr?: string
planStatus: number
sourceType: 'HISTORY'
}
type UnifiedDevice = {
deviceId: string | number
deviceName: string
deviceCode: string
ratedCapacity?: number | string
plans: UnifiedPlan[]
}
withDefaults(
defineProps<{
scheduleList: UnifiedDevice[]
}>(),
{
scheduleList: () => []
}
)
const isFullscreen = ref(false)
const toggleFullscreen = () => {
isFullscreen.value = !isFullscreen.value
if (isFullscreen.value) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = ''
}
}
onBeforeUnmount(() => {
if (isFullscreen.value) {
document.body.style.overflow = ''
}
})
//
const planStatusMap: Record<number, { label: string; color: string }> = {
1: { label: '已排产', color: '#5dade2' },
3: { label: '已并工', color: '#52c41a' },
5: { label: '暂停', color: '#faad14' },
8: { label: '待入库', color: '#ff7875' },
9: { label: '已入库', color: '#b37feb' }
}
const getPlanStatusLabel = (status: number) => {
return planStatusMap[status]?.label || '未知'
}
const getPlanStatusColor = (status: number) => {
return planStatusMap[status]?.color || '#999999'
}
</script>
<style scoped>
.schedule-card-view {
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
background: #fff;
}
.schedule-card-view.fullscreen-mode {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
border-radius: 0;
}
.schedule-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: #f5f7fa;
border-bottom: 1px solid #e4e7eb;
flex-shrink: 0;
}
.schedule-legend {
display: flex;
gap: 24px;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #666;
}
.legend-color {
width: 12px;
height: 12px;
border-radius: 2px;
}
.toolbar-actions {
display: flex;
gap: 8px;
}
.device-cards-container {
flex: 1;
overflow-x: auto;
overflow-y: hidden;
padding: 16px;
display: flex;
gap: 16px;
align-items: flex-start;
scroll-behavior: smooth;
}
/* 隐藏滚动条但保留滚动功能 */
.device-cards-container::-webkit-scrollbar {
height: 6px;
}
.device-cards-container::-webkit-scrollbar-track {
background: #f5f7fa;
}
.device-cards-container::-webkit-scrollbar-thumb {
background: #d9d9d9;
border-radius: 3px;
}
.device-cards-container::-webkit-scrollbar-thumb:hover {
background: #bfbfbf;
}
.device-card {
background: #fff;
border: 1px solid #e4e7eb;
border-radius: 4px;
overflow: hidden;
display: flex;
flex-direction: column;
flex-shrink: 0;
width: 340px;
max-height: 100%;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
}
.device-header {
padding: 12px 16px;
border-bottom: 1px solid #e4e7eb;
background: #fafbfc;
}
.device-info {
margin-bottom: 8px;
}
.device-name {
font-size: 14px;
font-weight: 600;
color: #1f2937;
margin-bottom: 2px;
}
.device-code {
font-size: 12px;
color: #999;
}
.device-stats {
display: flex;
gap: 16px;
}
.stat-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
}
.stat-label {
color: #666;
}
.stat-value {
font-weight: 600;
color: #1f2937;
}
.plans-container {
padding: 12px;
overflow-y: auto;
flex: 1;
max-height: calc(100vh - 200px);
scrollbar-width: thin;
scrollbar-color: transparent transparent;
}
.plans-container:hover {
scrollbar-color: rgba(144, 147, 153, 0.3) transparent;
}
.plans-container::-webkit-scrollbar {
width: 6px;
}
.plans-container::-webkit-scrollbar-thumb {
background: transparent;
border-radius: 3px;
}
.plans-container:hover::-webkit-scrollbar-thumb {
background: rgba(144, 147, 153, 0.3);
}
.plans-container::-webkit-scrollbar-track {
background: transparent;
}
.plan-card {
background: #fff;
border: 1px solid #e4e7eb;
border-left: 3px solid;
border-radius: 2px;
padding: 10px;
margin-bottom: 8px;
font-size: 12px;
}
.plan-card:last-child {
margin-bottom: 0;
}
.plan-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.plan-status-badge {
padding: 2px 6px;
border-radius: 2px;
color: #fff;
font-size: 11px;
font-weight: 600;
white-space: nowrap;
flex-shrink: 0;
}
.plan-code {
font-weight: 600;
color: #1f2937;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.plan-content {
display: flex;
flex-direction: column;
gap: 4px;
}
.plan-row {
display: flex;
gap: 4px;
line-height: 1.4;
}
.plan-label {
color: #999;
flex-shrink: 0;
}
.plan-value {
color: #1f2937;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.empty-state {
padding: 20px;
text-align: center;
}
</style>

@ -20,20 +20,31 @@
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" />查询</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" />重置</el-button>
<el-button circle @click="toggleViewMode">
<el-icon><Switch /></el-icon>
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<ContentWrap>
<ContentWrap v-if="viewMode === 'gantt'">
<div v-loading="loading">
<ScheduleGanttPanel :schedule-list="filteredList" :editable="false" height="calc(100vh - 320px)" />
</div>
</ContentWrap>
<div v-else style="width: 100%; height: calc(100vh - 60px)">
<div v-loading="loading" style="width: 100%; height: 100%">
<ScheduleCardView :schedule-list="filteredList" />
</div>
</div>
</template>
<script setup lang="ts">
import { Switch } from '@element-plus/icons-vue'
import { PlanApi } from '@/api/mes/plan'
import ScheduleGanttPanel from '@/views/mes/components/ScheduleGanttPanel.vue'
import ScheduleCardView from '@/views/mes/ganttChart/components/ScheduleCardView.vue'
import dayjs from 'dayjs'
defineOptions({ name: 'MesGanttChart' })
@ -63,6 +74,7 @@ type UnifiedDevice = {
const loading = ref(false)
const queryFormRef = ref()
const scheduleList = ref<UnifiedDevice[]>([])
const viewMode = ref<'gantt' | 'card'>('gantt')
const buildDefaultRange = () => {
const start = dayjs().startOf('month').startOf('day')
@ -117,6 +129,10 @@ const resetQuery = () => {
getList()
}
const toggleViewMode = () => {
viewMode.value = viewMode.value === 'gantt' ? 'card' : 'gantt'
}
onMounted(() => {
handleQuery()
})

Loading…
Cancel
Save