Merge remote-tracking branch 'origin/main'
commit
1625515607
@ -0,0 +1,426 @@
|
||||
<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>{{ t('GanttChart.CardView.legendScheduled') }}</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background: #52c41a"></div>
|
||||
<span>{{ t('GanttChart.CardView.legendMerged') }}</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background: #faad14"></div>
|
||||
<span>{{ t('GanttChart.CardView.legendPaused') }}</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background: #ff7875"></div>
|
||||
<span>{{ t('GanttChart.CardView.legendPendingStorage') }}</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background: #b37feb"></div>
|
||||
<span>{{ t('GanttChart.CardView.legendStored') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<el-button circle @click="emit('refresh')">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</el-button>
|
||||
<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">{{ t('GanttChart.CardView.statPlanCount') }}</span>
|
||||
<span class="stat-value">{{ device.plans?.length || 0 }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">{{ t('GanttChart.CardView.statCapacity') }}</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">{{ t('GanttChart.CardView.planCodeLabel') }}</span>
|
||||
<span class="plan-value">{{ plan.taskCode }}</span>
|
||||
</div>
|
||||
<div class="plan-row">
|
||||
<span class="plan-label">{{ t('GanttChart.CardView.productLabel') }}</span>
|
||||
<span class="plan-value">{{ plan.productCode }} / {{ plan.productName }}</span>
|
||||
</div>
|
||||
<div class="plan-row">
|
||||
<span class="plan-label">{{ t('GanttChart.CardView.planNumberLabel') }}</span>
|
||||
<span class="plan-value">{{ plan.planNumber }}</span>
|
||||
</div>
|
||||
<div class="plan-row">
|
||||
<span class="plan-label">{{ t('GanttChart.CardView.deliveryDateLabel') }}</span>
|
||||
<span class="plan-value">{{ plan.deliveryDateStr || '-' }}</span>
|
||||
</div>
|
||||
<div class="plan-row">
|
||||
<span class="plan-label">{{ t('GanttChart.CardView.startLabel') }}</span>
|
||||
<span class="plan-value">{{ plan.planStartTimeStr }}</span>
|
||||
</div>
|
||||
<div class="plan-row">
|
||||
<span class="plan-label">{{ t('GanttChart.CardView.endLabel') }}</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="t('GanttChart.CardView.emptyDescription')" :image-size="40" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FullScreen, Refresh } from '@element-plus/icons-vue'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
|
||||
defineOptions({ name: 'ScheduleCardView' })
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
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 emit = defineEmits(['refresh'])
|
||||
|
||||
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 PLAN_STATUS_I18N_MAP: Record<number, string> = {
|
||||
1: 'GanttChart.CardView.statusScheduled',
|
||||
3: 'GanttChart.CardView.statusMerged',
|
||||
5: 'GanttChart.CardView.statusPaused',
|
||||
8: 'GanttChart.CardView.statusPendingStorage',
|
||||
9: 'GanttChart.CardView.statusStored'
|
||||
}
|
||||
|
||||
const planStatusMap: Record<number, { color: string }> = {
|
||||
1: { color: '#5dade2' },
|
||||
3: { color: '#52c41a' },
|
||||
5: { color: '#faad14' },
|
||||
8: { color: '#ff7875' },
|
||||
9: { color: '#b37feb' }
|
||||
}
|
||||
|
||||
const getPlanStatusLabel = (status: number) => {
|
||||
const i18nKey = PLAN_STATUS_I18N_MAP[status]
|
||||
return i18nKey ? t(i18nKey) : t('GanttChart.CardView.statusUnknown')
|
||||
}
|
||||
|
||||
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>
|
||||
@ -1,54 +1,59 @@
|
||||
<template>
|
||||
<el-dialog v-model="dialogVisible" title="产品信息" width="80%" @close="handleClose">
|
||||
<el-table v-loading="loading" :data="productList" :stripe="true" :show-overflow-tooltip="true">
|
||||
<el-table-column label="产品编码" align="center" prop="id" width="120px" />
|
||||
<el-table-column label="产品名称" align="center" prop="productName" />
|
||||
<el-table-column label="单位" align="center" prop="unitName" width="100px" />
|
||||
<el-table-column label="物料编码" align="center" prop="bomId" width="120px" />
|
||||
<el-table-column label="用量" align="center" prop="usageNumber" width="100px" />
|
||||
<el-table-column label="良率%" align="center" prop="yieldRate" width="100px" />
|
||||
<el-table-column label="备注" align="center" prop="remark" />
|
||||
<el-table-column label="是否启用" align="center" width="100px">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.isEnable ? 'success' : 'danger'">
|
||||
{{ scope.row.isEnable ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-dialog v-model="dialogVisible" :title="t('ProductionReport.ProductInfo.dialogTitle')" width="900px" @close="handleClose">
|
||||
<el-descriptions :column="5" border class="mb-20px" size="small">
|
||||
<el-descriptions-item :label="t('ProductionReport.ProductInfo.labelProductCode')">{{ productInfo.barCode }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('ProductionReport.ProductInfo.labelProductName')">{{ productInfo.productName }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('ProductionReport.ProductInfo.labelNumber')">{{ productInfo.number }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('ProductionReport.ProductInfo.labelUnit')">{{ productInfo.unitName }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('ProductionReport.ProductInfo.labelSpecification')">{{ productInfo.standard }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-table v-loading="loading" :data="materialList" :stripe="true" :show-overflow-tooltip="true">
|
||||
<el-table-column :label="t('ProductionReport.ProductInfo.tableMaterialCode')" align="center" prop="productCode" min-width="130" />
|
||||
<el-table-column :label="t('ProductionReport.ProductInfo.tableMaterialName')" align="center" prop="productName" min-width="150" />
|
||||
<el-table-column :label="t('ProductionReport.ProductInfo.tableRequiredQuantity')" align="center" prop="usageNumber" width="100" />
|
||||
<el-table-column :label="t('ProductionReport.ProductInfo.tableUnit')" align="center" prop="unitName" width="80" />
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BomApi } from '@/api/mes/bom'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
|
||||
defineOptions({ name: 'ProductInfoDialog' })
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const productList = ref<any[]>([])
|
||||
const materialList = ref<any[]>([])
|
||||
const productInfo = ref<any>({})
|
||||
|
||||
const open = async (productId: number) => {
|
||||
const open = async (row: any) => {
|
||||
dialogVisible.value = true
|
||||
productInfo.value = row || {}
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await BomApi.getBomByProductId(productId)
|
||||
productList.value = data || []
|
||||
const data = await BomApi.getBomByProductId(row.productId)
|
||||
materialList.value = data || []
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch product info:', error)
|
||||
productList.value = []
|
||||
console.error('Failed to fetch material list:', error)
|
||||
materialList.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
productList.value = []
|
||||
materialList.value = []
|
||||
productInfo.value = {}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open
|
||||
})
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.mb-20px {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
Reference in New Issue