Merge remote-tracking branch 'origin/main'

pull/3/head
liutao 3 weeks ago
commit 1625515607

@ -4269,5 +4269,228 @@ export default {
messageDeviceNoParams: 'No parameters under this device',
messageFetchChartFailed: 'Failed to load chart data'
}
},
ProductionReport: {
Index: {
searchCodeLabel: 'Task Code',
searchCodePlaceholder: 'Please enter task code',
searchOrderDateLabel: 'Order Date',
searchDeliveryDateLabel: 'Delivery Date',
searchRemarkLabel: 'Remark',
searchRemarkPlaceholder: 'Please enter remark',
searchCreateTimeLabel: 'Create Time',
startDatePlaceholder: 'Start Date',
endDatePlaceholder: 'End Date',
buttonSearch: 'Search',
buttonReset: 'Reset',
buttonExport: 'Export',
tabAll: 'All',
tabIssued: 'Issued',
tabPartialScheduled: 'Partial Scheduled',
tabPendingProduction: 'Pending',
tabInProduction: 'In Production',
tabCompleted: 'Completed',
tableCode: 'Task Code',
tableOrderDate: 'Order Date',
tableDeliveryDate: 'Delivery Date',
tableStatus: 'Status',
tableIsScheduled: 'Scheduled',
tableProductionProgress: 'Progress',
tableRemark: 'Remark',
yes: 'Yes',
no: 'No',
detailTabBasicInfo: 'Basic Info',
detailTabRelatedPlan: 'Related Plans',
detailTabQualityInfo: 'Quality Info',
detailTabBaogongInfo: 'Work Report',
exportFilename: 'ProductionReport'
},
BasicInfo: {
buttonRefresh: 'Refresh',
tableTaskCode: 'Task Code',
tableProductCode: 'Product Code',
tableProductName: 'Product Name',
tableTotalNumber: 'Total Qty',
tablePlanNumber: 'Planned Qty',
tableUnplannedNumber: 'Unplanned Qty',
tableOperate: 'Operate',
buttonViewProductInfo: 'View Product'
},
ProductInfo: {
dialogTitle: 'Product Info',
labelProductCode: 'Product Code',
labelProductName: 'Product Name',
labelNumber: 'Quantity',
labelUnit: 'Unit',
labelSpecification: 'Specification',
tableMaterialCode: 'Material Code',
tableMaterialName: 'Material Name',
tableRequiredQuantity: 'Required Qty',
tableUnit: 'Unit'
},
BaogongInfo: {
dateRangeSeparator: 'To',
startDatePlaceholder: 'Start Date',
endDatePlaceholder: 'End Date',
buttonToday: 'Today',
buttonLast7Days: 'Last 7 Days',
buttonLast30Days: 'Last 30 Days',
buttonReset: 'Reset',
buttonRefresh: 'Refresh',
tableTaskCode: 'Task Code',
tablePlanCode: 'Plan Code',
tableEmployeeId: 'Employee ID',
tableEmployeeName: 'Employee Name',
tableProductName: 'Product Name',
tableProductCode: 'Product Code',
tableBaogongNumber: 'Report Qty',
tablePassNumber: 'Pass Qty',
tableNoPassNumber: 'Fail Qty',
tablePassRate: 'Pass Rate%',
tableNoPassReason: 'Fail Reason',
tableBaogongTime: 'Report Time'
},
QualityInfo: {
buttonRefresh: 'Refresh',
expandTableName: 'Inspection Item',
expandTableMethod: 'Method',
expandTableStandard: 'Standard',
expandTableUnit: 'Unit',
expandTableUpperLimit: 'Upper Limit',
expandTableLowerLimit: 'Lower Limit',
expandTableInputValue: 'Input Value',
expandTableImage: 'Image',
expandTablePreview: 'Preview',
expandTableResult: 'Result',
expandTableRemark: 'Remark',
tableCode: 'Inspection Code',
tableCategory: 'Category',
tableStatus: 'Status',
tableTicketCode: 'Ticket Code',
tableProcess: 'Process',
tableSchema: 'Inspection Schema',
tableManager: 'Manager',
tableExecutor: 'Executor',
tableExecuteTime: 'Execute Time',
tableRemark: 'Remark',
tableResult: 'Result',
resultPass: 'Pass',
resultFail: 'Fail',
inspectionPending: 'Pending',
inspectionPass: 'Passed',
inspectionFail: 'Failed',
unknown: 'Unknown'
},
RelatedPlan: {
buttonRefresh: 'Refresh',
emptyDescription: 'No related plans'
},
PlanCard: {
labelProductName: 'Product Name',
labelDeviceName: 'Device Name',
labelPlanStartTime: 'Plan Start Time',
labelPlanEndTime: 'Plan End Time',
labelLatestStartTime: 'Latest Start Time',
statPlanNumber: 'Planned Qty',
statWangongNumber: 'Completed Qty',
statPassNumber: 'Pass Qty',
statNoPassNumber: 'Fail Qty',
statPassRate: 'Pass Rate',
emptyDescription: 'No data'
},
Timeline: {
unknown: 'Unknown',
statusScheduled: 'Scheduled',
statusPaused: 'Paused',
statusPendingStorage: 'Pending Storage',
statusStored: 'Stored',
statusStarted: 'Started'
}
},
GanttChart: {
Index: {
searchTimeLabel: 'Query Time',
startPlaceholder: 'Start Time',
endPlaceholder: 'End Time',
deviceLabel: 'Device',
devicePlaceholder: 'Please select device',
buttonSearch: 'Search',
buttonReset: 'Reset'
},
CardView: {
legendScheduled: 'Scheduled',
legendMerged: 'Merged',
legendPaused: 'Paused',
legendPendingStorage: 'Pending Storage',
legendStored: 'Stored',
statPlanCount: 'Plans',
statCapacity: 'Capacity',
planCodeLabel: 'Plan Code:',
productLabel: 'Product:',
planNumberLabel: 'Plan Qty:',
deliveryDateLabel: 'Delivery Date:',
startLabel: 'Start:',
endLabel: 'End:',
emptyDescription: 'No Plans',
statusScheduled: 'Scheduled',
statusMerged: 'Merged',
statusPaused: 'Paused',
statusPendingStorage: 'Pending Storage',
statusStored: 'Stored',
statusUnknown: 'Unknown'
},
GanttPanel: {
detailTitle: 'Schedule Info',
deviceNameLabel: 'Device Name',
deviceIdLabel: 'Device ID',
capacityLabel: 'Capacity',
dailyAvgLabel: 'Daily Avg Report',
dataCollectionCapacityLabel: 'DC Capacity',
planCountLabel: 'Plan Count',
planDetailTitle: 'Plan Details',
planCodeColon: 'Plan Code: ',
planNumberColon: 'Plan Qty: ',
deliveryDateColon: 'Delivery Date: ',
startColon: 'Start: ',
endColon: 'End: ',
latestStartColon: 'Latest Start: ',
emptyDescription: 'No Schedule Info',
adjustTaskTitle: 'Adjust Task',
deviceLabel: 'Device',
devicePlaceholder: 'Please select device',
startDateLabel: 'Start Date',
startDatePlaceholder: 'Please select start date',
durationLabel: 'Days',
buttonCancel: 'Cancel',
buttonConfirm: 'Confirm',
editStartDateTitle: 'Edit Start Time',
startTimeLabel: 'Start Time',
startTimePlaceholder: 'Please select start time',
warningCompleteDeviceDate: 'Please complete device and start date',
warningValidTime: 'Please select a valid time',
columnTaskName: 'Task Name',
columnStartTime: 'Start Time',
columnDays: 'Days',
scaleMonthFormat: 'MMM YYYY',
tooltipTaskDetail: 'Task Detail',
tooltipTaskCode: 'Task Code: ',
tooltipProduct: 'Product: ',
tooltipDetailId: 'Detail ID: ',
tooltipPlanNumber: 'Plan Qty: ',
tooltipStart: 'Start: ',
tooltipEnd: 'End: ',
tooltipLatestStart: 'Latest Start: ',
tooltipSummary: 'Summary',
tooltipDevice: 'Device: ',
tooltipDetailCount: 'Detail Count: ',
tooltipTotalPlanNumber: 'Total Plan Qty: ',
tooltipEarliestStart: 'Earliest Start: ',
tooltipLatestEnd: 'Latest End: ',
statusScheduled: 'Scheduled',
statusStarted: 'Started',
statusPaused: 'Paused',
statusPendingStorage: 'Pending Storage',
statusStored: 'Stored'
}
}
}

@ -4486,6 +4486,229 @@ export default {
}
}
},
ProductionReport: {
Index: {
searchCodeLabel: '任务单编码',
searchCodePlaceholder: '请输入任务单编码',
searchOrderDateLabel: '下达日期',
searchDeliveryDateLabel: '交货日期',
searchRemarkLabel: '备注',
searchRemarkPlaceholder: '请输入备注',
searchCreateTimeLabel: '创建时间',
startDatePlaceholder: '开始日期',
endDatePlaceholder: '结束日期',
buttonSearch: '搜索',
buttonReset: '重置',
buttonExport: '导出',
tabAll: '全部',
tabIssued: '已下达',
tabPartialScheduled: '部分排产',
tabPendingProduction: '待生产',
tabInProduction: '生产中',
tabCompleted: '已完成',
tableCode: '任务单编码',
tableOrderDate: '下达日期',
tableDeliveryDate: '交货日期',
tableStatus: '状态',
tableIsScheduled: '是否排产',
tableProductionProgress: '生产进度',
tableRemark: '备注',
yes: '是',
no: '否',
detailTabBasicInfo: '基础信息',
detailTabRelatedPlan: '关联计划',
detailTabQualityInfo: '质检信息',
detailTabBaogongInfo: '报工信息',
exportFilename: '生产报表'
},
BasicInfo: {
buttonRefresh: '刷新',
tableTaskCode: '任务单编码',
tableProductCode: '产品编码',
tableProductName: '产品名称',
tableTotalNumber: '总数量',
tablePlanNumber: '已计划数量',
tableUnplannedNumber: '未计划数量',
tableOperate: '操作',
buttonViewProductInfo: '查看产品信息'
},
ProductInfo: {
dialogTitle: '产品信息',
labelProductCode: '产品编码',
labelProductName: '产品名称',
labelNumber: '数量',
labelUnit: '单位',
labelSpecification: '规格',
tableMaterialCode: '原料编码',
tableMaterialName: '原料名称',
tableRequiredQuantity: '需求量',
tableUnit: '单位'
},
BaogongInfo: {
dateRangeSeparator: '至',
startDatePlaceholder: '开始日期',
endDatePlaceholder: '结束日期',
buttonToday: '今日',
buttonLast7Days: '近七天',
buttonLast30Days: '近30日',
buttonReset: '重置',
buttonRefresh: '刷新',
tableTaskCode: '任务单编码',
tablePlanCode: '计划编码',
tableEmployeeId: '员工ID',
tableEmployeeName: '员工名称',
tableProductName: '产品名称',
tableProductCode: '产品编码',
tableBaogongNumber: '报工数量',
tablePassNumber: '合格数量',
tableNoPassNumber: '不合格数量',
tablePassRate: '合格率%',
tableNoPassReason: '不合格原因',
tableBaogongTime: '报工时间'
},
QualityInfo: {
buttonRefresh: '刷新',
expandTableName: '检验项名称',
expandTableMethod: '检验方式',
expandTableStandard: '判定基准',
expandTableUnit: '单位',
expandTableUpperLimit: '上限值',
expandTableLowerLimit: '下限值',
expandTableInputValue: '输入值',
expandTableImage: '图片',
expandTablePreview: '预览',
expandTableResult: '检验结果',
expandTableRemark: '备注',
tableCode: '质检编码',
tableCategory: '质检分类',
tableStatus: '状态',
tableTicketCode: '工单编码',
tableProcess: '工序',
tableSchema: '检验方案',
tableManager: '负责人',
tableExecutor: '执行人',
tableExecuteTime: '执行时间',
tableRemark: '备注',
tableResult: '结果',
resultPass: '通过',
resultFail: '不通过',
inspectionPending: '待检测',
inspectionPass: '检测通过',
inspectionFail: '检测不通过',
unknown: '未知'
},
RelatedPlan: {
buttonRefresh: '刷新',
emptyDescription: '暂无关联计划'
},
PlanCard: {
labelProductName: '产品名称',
labelDeviceName: '设备名称',
labelPlanStartTime: '计划开始时间',
labelPlanEndTime: '计划结束时间',
labelLatestStartTime: '最晚开始时间',
statPlanNumber: '计划数量',
statWangongNumber: '完工数量',
statPassNumber: '合格数量',
statNoPassNumber: '不合格数量',
statPassRate: '合格率',
emptyDescription: '暂无数据'
},
Timeline: {
unknown: '未知',
statusScheduled: '已排产',
statusPaused: '暂停',
statusPendingStorage: '待入库',
statusStored: '已入库',
statusStarted: '已开工'
}
},
GanttChart: {
Index: {
searchTimeLabel: '查询时间',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
deviceLabel: '设备',
devicePlaceholder: '请选择设备',
buttonSearch: '查询',
buttonReset: '重置'
},
CardView: {
legendScheduled: '已排产',
legendMerged: '已并工',
legendPaused: '暂停',
legendPendingStorage: '待入库',
legendStored: '已入库',
statPlanCount: '计划数',
statCapacity: '产能',
planCodeLabel: '计划编号:',
productLabel: '产品:',
planNumberLabel: '计划数量:',
deliveryDateLabel: '交货日期:',
startLabel: '开始:',
endLabel: '结束:',
emptyDescription: '暂无计划',
statusScheduled: '已排产',
statusMerged: '已并工',
statusPaused: '暂停',
statusPendingStorage: '待入库',
statusStored: '已入库',
statusUnknown: '未知'
},
GanttPanel: {
detailTitle: '计划信息',
deviceNameLabel: '设备名称',
deviceIdLabel: '设备ID',
capacityLabel: '产能',
dailyAvgLabel: '每日报工平均值',
dataCollectionCapacityLabel: '数据采集产能',
planCountLabel: '计划条数',
planDetailTitle: '计划明细',
planCodeColon: '计划编码:',
planNumberColon: '计划数量:',
deliveryDateColon: '交货日期:',
startColon: '开始:',
endColon: '结束:',
latestStartColon: '最晚开工:',
emptyDescription: '暂无计划信息',
adjustTaskTitle: '调整任务',
deviceLabel: '设备',
devicePlaceholder: '请选择设备',
startDateLabel: '开始日期',
startDatePlaceholder: '请选择开始日期',
durationLabel: '天数',
buttonCancel: '取消',
buttonConfirm: '确定',
editStartDateTitle: '修改开始时间',
startTimeLabel: '开始时间',
startTimePlaceholder: '请选择开始时间',
warningCompleteDeviceDate: '请完善设备和开始日期',
warningValidTime: '请选择有效的时间',
columnTaskName: '任务名称',
columnStartTime: '开始时间',
columnDays: '天数',
scaleMonthFormat: 'YYYY年M月',
tooltipTaskDetail: '任务明细',
tooltipTaskCode: '任务单:',
tooltipProduct: '产品:',
tooltipDetailId: '明细ID',
tooltipPlanNumber: '计划数量:',
tooltipStart: '开始:',
tooltipEnd: '结束:',
tooltipLatestStart: '最晚开工:',
tooltipSummary: '汇总',
tooltipDevice: '设备:',
tooltipDetailCount: '任务明细条数:',
tooltipTotalPlanNumber: '计划总数:',
tooltipEarliestStart: '最早计划开始:',
tooltipLatestEnd: '最晚计划结束:',
statusScheduled: '已排产',
statusStarted: '已开工',
statusPaused: '暂停',
statusPendingStorage: '待入库',
statusStored: '已入库'
}
}
}

@ -420,7 +420,7 @@ onMounted(async () => {
// TODO
// TODO
/** tab 切换 */
let activeName = '产品出库'
let activeName = '领料出库'
const handleTabClick = (tab: TabsPaneContext) => {
queryParams.outType = tab.paneName
handleQuery()

@ -2,7 +2,7 @@
<div class="schedule-preview-wrap">
<div ref="ganttContainerRef" class="schedule-gantt-container" :style="{ height }"></div>
<div class="schedule-detail-panel">
<div class="schedule-detail-title">计划信息</div>
<div class="schedule-detail-title">{{ t('GanttChart.GanttPanel.detailTitle') }}</div>
<div v-if="!editable" class="schedule-status-legend">
<div
v-for="item in sortedPlanStatusList"
@ -16,14 +16,14 @@
</div>
<template v-if="activePreviewDevice">
<el-descriptions :column="1" border size="small">
<el-descriptions-item label="设备名称">{{ activePreviewDevice.deviceName }}</el-descriptions-item>
<el-descriptions-item label="设备ID">{{ activePreviewDevice.deviceId }}</el-descriptions-item>
<el-descriptions-item label="产能">{{ activePreviewDevice.ratedCapacity ?? '-' }}</el-descriptions-item>
<el-descriptions-item v-if="'dailyAverageValue' in activePreviewDevice" label="每日报工平均值">{{ activePreviewDevice.dailyAverageValue ?? '-' }}</el-descriptions-item>
<el-descriptions-item v-if="'dataCollectionCapacity' in activePreviewDevice" label="数据采集产能">{{ activePreviewDevice.dataCollectionCapacity ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="计划条数">{{ activePreviewDevice.plans?.length ?? 0 }}</el-descriptions-item>
<el-descriptions-item :label="t('GanttChart.GanttPanel.deviceNameLabel')">{{ activePreviewDevice.deviceName }}</el-descriptions-item>
<el-descriptions-item :label="t('GanttChart.GanttPanel.deviceIdLabel')">{{ activePreviewDevice.deviceId }}</el-descriptions-item>
<el-descriptions-item :label="t('GanttChart.GanttPanel.capacityLabel')">{{ activePreviewDevice.ratedCapacity ?? '-' }}</el-descriptions-item>
<el-descriptions-item v-if="'dailyAverageValue' in activePreviewDevice" :label="t('GanttChart.GanttPanel.dailyAvgLabel')">{{ activePreviewDevice.dailyAverageValue ?? '-' }}</el-descriptions-item>
<el-descriptions-item v-if="'dataCollectionCapacity' in activePreviewDevice" :label="t('GanttChart.GanttPanel.dataCollectionCapacityLabel')">{{ activePreviewDevice.dataCollectionCapacity ?? '-' }}</el-descriptions-item>
<el-descriptions-item :label="t('GanttChart.GanttPanel.planCountLabel')">{{ activePreviewDevice.plans?.length ?? 0 }}</el-descriptions-item>
</el-descriptions>
<div class="schedule-plan-list-title">计划明细</div>
<div class="schedule-plan-list-title">{{ t('GanttChart.GanttPanel.planDetailTitle') }}</div>
<div class="schedule-plan-list">
<div
v-for="(plan, index) in activePreviewTask ? [activePreviewTask] : (activePreviewDevice?.plans ?? [])"
@ -40,60 +40,60 @@
{{ PLAN_STATUS_COLOR_MAP[plan.planStatus].label }}
</span>
</div>
<div>计划编码{{ plan.taskCode ?? '-' }}</div>
<div>计划数量{{ plan.planNumber ?? '-' }}</div>
<div>交货日期{{ plan.deliveryDateStr ?? '-' }}</div>
<div>开始{{ plan.planStartTimeStr || '-' }}</div>
<div>结束{{ plan.planEndTimeStr || '-' }}</div>
<div>最晚开工{{ plan.latestStartTimeStr || '-' }}</div>
<div>{{ t('GanttChart.GanttPanel.planCodeColon') }}{{ plan.taskCode ?? '-' }}</div>
<div>{{ t('GanttChart.GanttPanel.planNumberColon') }}{{ plan.planNumber ?? '-' }}</div>
<div>{{ t('GanttChart.GanttPanel.deliveryDateColon') }}{{ plan.deliveryDateStr ?? '-' }}</div>
<div>{{ t('GanttChart.GanttPanel.startColon') }}{{ plan.planStartTimeStr || '-' }}</div>
<div>{{ t('GanttChart.GanttPanel.endColon') }}{{ plan.planEndTimeStr || '-' }}</div>
<div>{{ t('GanttChart.GanttPanel.latestStartColon') }}{{ plan.latestStartTimeStr || '-' }}</div>
</div>
</div>
</template>
<el-empty v-else description="暂无计划信息" :image-size="80" />
<el-empty v-else :description="t('GanttChart.GanttPanel.emptyDescription')" :image-size="80" />
</div>
</div>
<el-dialog v-if="props.editable" v-model="taskAdjustDialogVisible" title="调整任务" width="420px" append-to-body>
<el-dialog v-if="props.editable" v-model="taskAdjustDialogVisible" :title="t('GanttChart.GanttPanel.adjustTaskTitle')" width="420px" append-to-body>
<el-form label-width="90px">
<el-form-item label="设备">
<el-select v-model="taskAdjustForm.deviceTaskId" placeholder="请选择设备" class="!w-full">
<el-form-item :label="t('GanttChart.GanttPanel.deviceLabel')">
<el-select v-model="taskAdjustForm.deviceTaskId" :placeholder="t('GanttChart.GanttPanel.devicePlaceholder')" class="!w-full">
<el-option v-for="item in previewDeviceOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="开始日期">
<el-form-item :label="t('GanttChart.GanttPanel.startDateLabel')">
<el-date-picker
v-model="taskAdjustForm.startDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择开始日期"
:placeholder="t('GanttChart.GanttPanel.startDatePlaceholder')"
class="!w-full"
/>
</el-form-item>
<el-form-item label="天数">
<el-form-item :label="t('GanttChart.GanttPanel.durationLabel')">
<el-input-number v-model="taskAdjustForm.duration" :min="1" :max="365" class="!w-full" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="taskAdjustDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleTaskAdjustSubmit"></el-button>
<el-button @click="taskAdjustDialogVisible = false">{{ t('GanttChart.GanttPanel.buttonCancel') }}</el-button>
<el-button type="primary" @click="handleTaskAdjustSubmit">{{ t('GanttChart.GanttPanel.buttonConfirm') }}</el-button>
</template>
</el-dialog>
<el-dialog v-if="props.editable" v-model="startDateEditorVisible" title="修改开始时间" width="360px" append-to-body>
<el-dialog v-if="props.editable" v-model="startDateEditorVisible" :title="t('GanttChart.GanttPanel.editStartDateTitle')" width="360px" append-to-body>
<el-form label-width="80px">
<el-form-item label="开始时间">
<el-form-item :label="t('GanttChart.GanttPanel.startTimeLabel')">
<el-date-picker
v-model="startDateEditorValue"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择开始时间"
:placeholder="t('GanttChart.GanttPanel.startTimePlaceholder')"
class="!w-full"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="startDateEditorVisible = false">取消</el-button>
<el-button type="primary" @click="handleStartDateEditorSubmit"></el-button>
<el-button @click="startDateEditorVisible = false">{{ t('GanttChart.GanttPanel.buttonCancel') }}</el-button>
<el-button type="primary" @click="handleStartDateEditorSubmit">{{ t('GanttChart.GanttPanel.buttonConfirm') }}</el-button>
</template>
</el-dialog>
</template>
@ -102,16 +102,19 @@
import dayjs from 'dayjs'
import { gantt } from 'dhtmlx-gantt'
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
import { useI18n } from '@/hooks/web/useI18n'
defineOptions({ name: 'ScheduleGanttPanel' })
const { t } = useI18n()
//
const PLAN_STATUS_COLOR_MAP = {
1: { label: '已排产', color: '#409eff', textColor: '#ffffff', sort: 1 },
8: { label: '已开工', color: '#67c23a', textColor: '#ffffff', sort: 2 },
3: { label: '暂停', color: '#e6a23c', textColor: '#ffffff', sort: 3 },
4: { label: '待入库', color: '#f56c6c', textColor: '#ffffff', sort: 4 },
5: { label: '已入库', color: '#8e7cc3', textColor: '#ffffff', sort: 5 },
1: { label: t('GanttChart.GanttPanel.statusScheduled'), color: '#409eff', textColor: '#ffffff', sort: 1 },
8: { label: t('GanttChart.GanttPanel.statusStarted'), color: '#67c23a', textColor: '#ffffff', sort: 2 },
3: { label: t('GanttChart.GanttPanel.statusPaused'), color: '#e6a23c', textColor: '#ffffff', sort: 3 },
4: { label: t('GanttChart.GanttPanel.statusPendingStorage'), color: '#f56c6c', textColor: '#ffffff', sort: 4 },
5: { label: t('GanttChart.GanttPanel.statusStored'), color: '#8e7cc3', textColor: '#ffffff', sort: 5 },
}
const sortedPlanStatusList = Object.entries(PLAN_STATUS_COLOR_MAP)
@ -339,25 +342,25 @@ const buildTaskTooltipHtml = (task: any, start?: Date, end?: Date) => {
const plan = task?._planData
if (plan) {
return `
<div><b>任务明细</b></div>
<div>任务单${plan.taskCode ?? '-'}</div>
<div>产品${plan.productCode ?? '-'} / ${plan.productName ?? '-'}</div>
<div>明细ID${plan.taskDetailId ?? '-'}</div>
<div>计划数量${plan.planNumber ?? '-'}</div>
<div>开始${formatTooltipDateTime(start ?? task?.start_date)}</div>
<div>结束${formatTooltipDateTime(end ?? task?.end_date)}</div>
<div>最晚开工${formatTooltipDateTime(plan.latestStartTimeStr)}</div>
<div><b>${t('GanttChart.GanttPanel.tooltipTaskDetail')}</b></div>
<div>${t('GanttChart.GanttPanel.tooltipTaskCode')}${plan.taskCode ?? '-'}</div>
<div>${t('GanttChart.GanttPanel.tooltipProduct')}${plan.productCode ?? '-'} / ${plan.productName ?? '-'}</div>
<div>${t('GanttChart.GanttPanel.tooltipDetailId')}${plan.taskDetailId ?? '-'}</div>
<div>${t('GanttChart.GanttPanel.tooltipPlanNumber')}${plan.planNumber ?? '-'}</div>
<div>${t('GanttChart.GanttPanel.tooltipStart')}${formatTooltipDateTime(start ?? task?.start_date)}</div>
<div>${t('GanttChart.GanttPanel.tooltipEnd')}${formatTooltipDateTime(end ?? task?.end_date)}</div>
<div>${t('GanttChart.GanttPanel.tooltipLatestStart')}${formatTooltipDateTime(plan.latestStartTimeStr)}</div>
`
}
const device = task?._deviceData
const summary = getDeviceTaskSummary(task)
return `
<div><b>汇总</b></div>
<div>设备${device?.deviceName ?? '-'}</div>
<div>任务明细条数${summary.planCount}</div>
<div>计划总数${summary.totalPlanNumber}</div>
<div>最早计划开始${summary.earliestStart}</div>
<div>最晚计划结束${summary.latestEnd}</div>
<div><b>${t('GanttChart.GanttPanel.tooltipSummary')}</b></div>
<div>${t('GanttChart.GanttPanel.tooltipDevice')}${device?.deviceName ?? '-'}</div>
<div>${t('GanttChart.GanttPanel.tooltipDetailCount')}${summary.planCount}</div>
<div>${t('GanttChart.GanttPanel.tooltipTotalPlanNumber')}${summary.totalPlanNumber}</div>
<div>${t('GanttChart.GanttPanel.tooltipEarliestStart')}${summary.earliestStart}</div>
<div>${t('GanttChart.GanttPanel.tooltipLatestEnd')}${summary.latestEnd}</div>
`
}
const getTaskByTooltipNode = (node: HTMLElement) => {
@ -617,7 +620,7 @@ const openTaskAdjustDialog = (task: any) => {
const handleTaskAdjustSubmit = () => {
if (!taskAdjustTaskId.value || !props.editable) return
if (!taskAdjustForm.deviceTaskId || !taskAdjustForm.startDate) {
message.warning('请完善设备和开始日期')
message.warning(t('GanttChart.GanttPanel.warningCompleteDeviceDate'))
return
}
const task = gantt.getTask(taskAdjustTaskId.value)
@ -643,7 +646,7 @@ const handleStartDateEditorSubmit = () => {
}
const newStart = dayjs(startDateEditorValue.value)
if (!newStart.isValid()) {
message.warning('请选择有效的时间')
message.warning(t('GanttChart.GanttPanel.warningValidTime'))
return
}
const duration = Math.max(Number(task.duration) || 1, 1)
@ -717,14 +720,14 @@ const initGanttPreview = () => {
gantt.config.columns = [
{
name: 'text',
label: '任务名称',
label: t('GanttChart.GanttPanel.columnTaskName'),
tree: true,
width: '*',
min_width: 200
},
{
name: 'start_date',
label: '开始时间',
label: t('GanttChart.GanttPanel.columnStartTime'),
align: 'center',
width: 210,
template: (task: any) =>
@ -734,7 +737,7 @@ const initGanttPreview = () => {
},
{
name: 'duration',
label: '天数',
label: t('GanttChart.GanttPanel.columnDays'),
align: 'center',
width: 60,
template: (task: any) =>
@ -750,7 +753,7 @@ const initGanttPreview = () => {
]
gantt.config.scales = [
{ unit: 'month', step: 1, format: (date) => dayjs(date).format('YYYY年M月') },
{ unit: 'month', step: 1, format: (date) => dayjs(date).format(t('GanttChart.GanttPanel.scaleMonthFormat')) },
{ unit: 'day', step: 1, format: (date) => dayjs(date).format('MM-DD') }
]

@ -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,43 +1,57 @@
<template>
<ContentWrap>
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="auto">
<el-form-item label="查询时间" prop="range">
<el-form-item :label="t('GanttChart.Index.searchTimeLabel')" prop="range">
<el-date-picker
v-model="queryRange"
type="datetimerange"
value-format="YYYY-MM-DD HH:mm:ss"
range-separator="-"
start-placeholder="开始时间"
end-placeholder="结束时间"
:start-placeholder="t('GanttChart.Index.startPlaceholder')"
:end-placeholder="t('GanttChart.Index.endPlaceholder')"
class="!w-360px"
/>
</el-form-item>
<el-form-item label="设备" prop="deviceIds">
<el-select v-model="queryParams.deviceIds" placeholder="请选择设备" clearable multiple collapse-tags class="!w-240px">
<el-form-item :label="t('GanttChart.Index.deviceLabel')" prop="deviceIds">
<el-select v-model="queryParams.deviceIds" :placeholder="t('GanttChart.Index.devicePlaceholder')" clearable multiple collapse-tags class="!w-240px">
<el-option v-for="item in deviceOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<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 @click="handleQuery"><Icon icon="ep:search" class="mr-5px" />{{ t('GanttChart.Index.buttonSearch') }}</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" />{{ t('GanttChart.Index.buttonReset') }}</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" @refresh="getList" />
</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'
import { useI18n } from '@/hooks/web/useI18n'
defineOptions({ name: 'MesGanttChart' })
const { t } = useI18n()
type UnifiedPlan = {
taskId: string | number
taskDetailId: string | number
@ -63,6 +77,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 +132,10 @@ const resetQuery = () => {
getList()
}
const toggleViewMode = () => {
viewMode.value = viewMode.value === 'gantt' ? 'card' : 'gantt'
}
onMounted(() => {
handleQuery()
})

@ -7,6 +7,7 @@
label-width="110px"
v-loading="formLoading"
>
<div class="section-title">计划</div>
<el-form-item :label="t('ProductionPlan.Plan.dialogTaskLabel')" prop="taskId">
<el-select
disabled
@ -146,6 +147,15 @@
<el-form-item :label="t('ProductionPlan.Plan.dialogRemarkLabel')" prop="remark">
<el-input type="textarea" v-model="formData.remark" :placeholder="t('ProductionPlan.Plan.dialogRemarkPlaceholder')" />
</el-form-item>
<div class="section-title">领料</div>
<el-form-item label="领料人" prop="workerId">
<el-select v-model="formData.workerId" clearable filterable placeholder="请选择领料人">
<el-option v-for="item in workerList" :key="item.id" :label="item.nickname" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="是否计算损耗" prop="isCalculateLoss">
<el-switch v-model="formData.isCalculateLoss" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading">{{ t('ProductionPlan.Plan.dialogSubmitButtonText') }}</el-button>
@ -169,11 +179,13 @@ import { PlanApi, PlanVO } from '@/api/mes/plan'
import { TaskApi, TaskDetailVO, TaskVO } from '@/api/mes/task'
import { DeviceLedgerApi, DeviceLedgerVO } from '@/api/mes/deviceledger'
import TableSelectDialog from '@/components/TableSelectDialog/TableSelectDialog.vue'
import * as UserApi from '@/api/system/user'
/** 生产计划 表单 */
defineOptions({ name: 'PlanForm' })
const taskList = ref<TaskVO[]>([]) //
const taskDetailList = ref<TaskDetailVO[]>([]) //
const workerList = ref<UserApi.UserVO[]>([])
const deviceSelectDialogRef = ref()
const selectedDeviceRows = ref<DeviceLedgerVO[]>([])
const maxUnplannedNumber = ref<number | undefined>(undefined)
@ -209,7 +221,8 @@ const formData = ref({
isCode: undefined,
latestStartTime: undefined,
finishDate: undefined,
deliveryDate: undefined
deliveryDate: undefined,
isCalculateLoss: true
})
const deliveryDateText = computed(() => {
const sourceValue = formData.value.finishDate || formData.value.deliveryDate
@ -231,7 +244,7 @@ const deviceColumns = [
{ label: '设备编号', prop: 'deviceCode', minWidth: 140 },
{ label: '设备名称', prop: 'deviceName', minWidth: 160 },
{ label: '设备型号', prop: 'deviceModel', minWidth: 140 },
{ label: '所属车间', prop: 'workshop', minWidth: 140 }
{ label: '所属车间', prop: 'workshopName', minWidth: 140 }
]
const latestStartDisabledDate = (date: Date) => {
if (!formData.value.finishDate) return false
@ -434,6 +447,7 @@ const open = async (type: string, id?: number,
}
}
taskList.value = await TaskApi.getPlanTaskList()
workerList.value = await UserApi.getSimpleUserList()
}
defineExpose({ open }) // open
@ -486,7 +500,8 @@ const resetForm = () => {
isCode: true,
latestStartTime: undefined,
finishDate: undefined,
deliveryDate: undefined
deliveryDate: undefined,
isCalculateLoss: true
}
maxUnplannedNumber.value = undefined
selectedDeviceRows.value = []
@ -513,4 +528,18 @@ const handlePlanEndTimeChange = () => {
}
</script>
<style scoped>
.section-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin: 16px 0 12px;
padding-left: 10px;
border-left: 3px solid #409eff;
}
.section-title:first-child {
margin-top: 0;
}
</style>

@ -73,34 +73,20 @@
<!-- 投料进度子表的列表 -->
<el-table-column type="expand">
<template #default="scope">
<el-tabs v-if="scope.row.status <= 1 && scope.row.status != 6" model-value="itemRequisitionDetail">
<el-tab-pane :label="t('ProductionPlan.Plan.detailItemRequisitionTabLabel')" name="itemRequisitionDetail">
<ItemRequisitionDetailList :item-requisition-id="scope.row.requisitionId" />
</el-tab-pane>
<!-- <el-tab-pane label="派工记录" name="paigongRecord">
<PaigongRecordList :plan-id="scope.row.id"/>
</el-tab-pane> -->
</el-tabs>
<!-- <el-tabs v-if="scope.row.status == 0" model-value="paigongRecord">
<el-tab-pane label="派工记录" name="paigongRecord">
<PaigongRecordList :plan-id="scope.row.id"/>
</el-tab-pane>
</el-tabs> -->
<el-tabs v-if="scope.row.status == 6" model-value="zj">
<el-tab-pane :label="t('ProductionPlan.Plan.detailInspectTabLabel')" name="zj">
<ZjProductPreList :ticket="scope.row.id"
@inspectable-change="(hasPending) => setInspectable(scope.row.id, hasPending)" />
</el-tab-pane>
</el-tabs>
<el-tabs v-if="scope.row.status >= 2 && scope.row.status != 6" model-value="itemRequisitionDetail">
<el-tabs model-value="itemRequisitionDetail">
<el-tab-pane :label="t('ProductionPlan.Plan.detailItemRequisitionTabLabel')" name="itemRequisitionDetail">
<ItemRequisitionDetailList :item-requisition-id="scope.row.requisitionId" />
</el-tab-pane>
<el-tab-pane :label="t('ProductionPlan.Plan.detailBaogongRecordTabLabel')" name="baogongRecord">
<BaogongRecordList :key="`${scope.row.id}-${baogongRecordRefreshKey}`" :plan-id="scope.row.id" />
</el-tab-pane>
<el-tab-pane :label="t('ProductionPlan.Plan.detailInspectTabLabel')" name="zj" v-if="scope.row.status == 8">
<ZjProductPreList :ticket="scope.row.id"
<el-tab-pane :label="t('ProductionPlan.Plan.detailInspectTabLabel')" name="zj" >
<ZjProductPreList :key="`${scope.row.id}-zj-${zjProductPreListRefreshKey}`" :ticket="scope.row.id"
@inspectable-change="(hasPending) => setInspectable(scope.row.id, hasPending)" />
</el-tab-pane>
</el-tabs>
@ -118,7 +104,21 @@
sortable />
<el-table-column :label="t('ProductionPlan.Plan.tableFinishNumberColumn')" align="center" prop="wangongNumber"
sortable />
<el-table-column :label="t('ProductionPlan.Plan.tablePassRateColumn')" align="center" prop="passRate" sortable />
<el-table-column :label="t('ProductionPlan.Plan.tablePassRateColumn')" align="center" min-width="60px" sortable="custom">
<template #default="scope">
<div class="pass-rate-cell">
<el-progress
type="circle"
:percentage="Number(scope.row.passRate ?? 0)"
:width="40"
:stroke-width="4"
:show-text="false"
:color="Number(scope.row.passRate ?? 0) === 100 ? '#67c23a' : undefined"
/>
<span class="pass-rate-text">{{ Number(scope.row.passRate ?? 0) }}%</span>
</div>
</template>
</el-table-column>
<!-- <el-table-column label="热压数量" align="center" prop="reyaNumber" /> -->
<el-table-column :label="t('ProductionPlan.Plan.tableStatusColumn')" align="center" prop="status" sortable>
<template #default="scope">
@ -168,8 +168,8 @@
v-hasPermi="['mes:plan:update']" v-if="scope.row.status === 1 && scope.row.isPreProduction === 1">
{{ t('ProductionPlan.Plan.actionTrialLabel') }}
</el-button>
<el-button link type="primary" @click.stop="handleInspectExpand(scope.row)"
v-if="scope.row.status === 6 && inspectableMap[scope.row.id] !== false">
<el-button link type="primary" @click.stop="openZjTaskForm(scope.row)"
v-if="scope.row.status === 6 && inspectableMap[scope.row.id] !== false || scope.row.status === 8">
{{ t('ProductionPlan.Plan.actionInspectLabel') }}
</el-button>
<!-- <el-button
@ -257,6 +257,9 @@
</template>
</Dialog>
<!-- 检验任务弹窗 -->
<ZjTaskForm ref="zjTaskFormRef" @success="handleZjTaskSuccess" />
</template>
<script setup lang="ts">
@ -274,6 +277,7 @@ import PlanDetail from "./components/PlanDetail.vue";
import Baogong from "./components/Baogong.vue";
import ItemRequisitionDetailList from "@/views/mes/itemrequisition/components/ItemRequisitionDetailList.vue";
import ZjProductPreList from "@/views/mes/zjproduct/components/ZjProductPreList.vue";
import ZjTaskForm from "@/views/mes/zjTask/ZjTaskForm.vue";
import BaogongRecordList from "@/views/mes/baogongrecord/components/BaogongRecordList.vue";
import { ZjTaskApi } from '@/api/mes/zjtask'
import { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse'
@ -293,6 +297,7 @@ const total = ref(0) // 列表的总页数
const tableRef = ref()
const inspectableMap = reactive<Record<number, boolean | undefined>>({})
const baogongRecordRefreshKey = ref(0)
const zjProductPreListRefreshKey = ref(0)
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
@ -433,6 +438,16 @@ const handleInspectExpand = (row: PlanVO) => {
tableRef.value?.toggleRowExpansion?.(row, true)
}
const handleZjTaskSuccess = async () => {
zjProductPreListRefreshKey.value += 1
await getList()
}
const zjTaskFormRef = ref()
const openZjTaskForm = (row: PlanVO) => {
zjTaskFormRef.value.open('create', undefined, { ticketType: 1, ticket: row.id, ticketName: row.code, productId: row.productId })
}
const setInspectable = (id: number | undefined, hasPending: boolean) => {
if (!id) return
inspectableMap[id] = hasPending
@ -542,3 +557,23 @@ const handleTabClick = (tab: TabsPaneContext) => {
handleQuery()
}
</script>
<style scoped>
.pass-rate-cell {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.pass-rate-cell :deep(.el-progress-circle) {
display: flex;
align-items: center;
justify-content: center;
}
.pass-rate-text {
font-size: 12px;
white-space: nowrap;
}
</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>

@ -1,30 +1,29 @@
<template>
<div class="card">
<!-- 卡片头部基本信息 -->
<div class="card-header">
<div class="header-left">
<div class="plan-id">{{ plan.code || `PLAN${plan.id}` }}</div>
<div class="plan-basic">
<span class="basic-item">
<span class="label">产品名称:</span>
<span class="label">{{ t('ProductionReport.PlanCard.labelProductName') }}:</span>
<span class="value">{{ plan.productName || '-' }}</span>
</span>
<span class="basic-item">
<span class="label">设备名称:</span>
<span class="label">{{ t('ProductionReport.PlanCard.labelDeviceName') }}:</span>
<span class="value">{{ plan.deviceName || '-' }}</span>
</span>
</div>
<div class="plan-basic">
<span class="basic-item">
<span class="label">计划开始时间:</span>
<span class="label">{{ t('ProductionReport.PlanCard.labelPlanStartTime') }}:</span>
<span class="value">{{ formatDateTime(plan.planStartTime) }}</span>
</span>
<span class="basic-item">
<span class="label">计划结束时间:</span>
<span class="label">{{ t('ProductionReport.PlanCard.labelPlanEndTime') }}:</span>
<span class="value">{{ formatDateTime(plan.planEndTime) }}</span>
</span>
<span class="basic-item">
<span class="label">最晚开始时间:</span>
<span class="label">{{ t('ProductionReport.PlanCard.labelLatestStartTime') }}:</span>
<span class="value">{{ formatDateTime(plan.latestStartTime) }}</span>
</span>
</div>
@ -33,34 +32,33 @@
<div class="header-right">
<div class="stat-group">
<div class="stat-item">
<span class="stat-label">计划数量</span>
<span class="stat-label">{{ t('ProductionReport.PlanCard.statPlanNumber') }}</span>
<span class="stat-value">{{ plan.planNumber || 0 }}</span>
</div>
<div class="stat-item">
<span class="stat-label">完工数量</span>
<span class="stat-label">{{ t('ProductionReport.PlanCard.statWangongNumber') }}</span>
<span class="stat-value">{{ plan.wangongNumber || 0 }}</span>
</div>
<div class="stat-item">
<span class="stat-label">合格数量</span>
<span class="stat-label">{{ t('ProductionReport.PlanCard.statPassNumber') }}</span>
<span class="stat-value">{{ plan.passNumber || 0 }}</span>
</div>
<div class="stat-item">
<span class="stat-label">不合格数量</span>
<span class="stat-label">{{ t('ProductionReport.PlanCard.statNoPassNumber') }}</span>
<span class="stat-value">{{ plan.noPassNumber || 0 }}</span>
</div>
</div>
<div class="pass-rate">
<span class="rate-label">合格率</span>
<span class="rate-label">{{ t('ProductionReport.PlanCard.statPassRate') }}</span>
<span class="rate-value">{{ plan.passRate }}%</span>
</div>
</div>
</div>
<!-- 卡片中部时间线 -->
<div class="card-timeline">
<ProductionPlanTimeline v-if="plan.planRecordList && plan.planRecordList.length" :plan-record-list="plan.planRecordList" :items-per-row="8" />
<el-empty v-else description="暂无数据" :image-size="60" />
<el-empty v-else :description="t('ProductionReport.PlanCard.emptyDescription')" :image-size="60" />
</div>
</div>
</template>
@ -69,16 +67,18 @@
import dayjs from 'dayjs'
import type { PlanVO } from '@/api/mes/plan'
import ProductionPlanTimeline from './ProductionPlanTimeline.vue'
import { useI18n } from '@/hooks/web/useI18n'
defineOptions({ name: 'ProductionPlanCard' })
const { t } = useI18n()
interface Props {
plan: PlanVO
}
defineProps<Props>()
//
const formatDateTime = (value: string | number | Date | null | undefined): string => {
if (!value) return '-'
return dayjs(value).format('YYYY-MM-DD HH:mm:ss')
@ -104,7 +104,6 @@ export default {}
}
}
//
.card-header {
display: flex;
justify-content: space-between;
@ -204,14 +203,12 @@ export default {}
}
}
// 线
.card-timeline {
padding: 16px 20px;
background: #fafafa;
border-bottom: 1px solid #f0f0f0;
}
//
@media (max-width: 1400px) {
.header-right {
flex-wrap: wrap;

@ -1,6 +1,5 @@
<template>
<div class="timeline-wrapper" ref="wrapperRef">
<!-- 时间线节点 -->
<div class="timeline-container">
<div
v-for="(record, index) in sortedRecords"
@ -8,19 +7,16 @@
class="timeline-node"
:style="getNodeStyle(index)"
>
<!-- 节点圆圈 -->
<div class="node-dot" :class="`status-${record.operateStatus}`">
<div class="node-number">{{ index + 1 }}</div>
</div>
<!-- 节点标签 -->
<div class="node-label">
<div class="status-badge">{{ getStatusLabel(record.operateStatus) }}</div>
<div class="node-time">{{ formatTime(record.operateTime) }}</div>
</div>
</div>
<!-- SVG 连接线 -->
<svg
v-if="sortedRecords.length > 1"
class="timeline-svg"
@ -45,13 +41,15 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import type { PlanRecordVO } from '@/api/mes/plan'
import { OPERATE_STATUS_MAP } from '@/api/mes/plan'
import { useI18n } from '@/hooks/web/useI18n'
defineOptions({ name: 'ProductionPlanTimeline' })
const { t } = useI18n()
interface Props {
planRecordList?: PlanRecordVO[]
itemsPerRow?: number // 6
itemsPerRow?: number
}
const props = withDefaults(defineProps<Props>(), {
@ -61,14 +59,20 @@ const props = withDefaults(defineProps<Props>(), {
const wrapperRef = ref<HTMLElement>()
//
const NODE_SIZE = 50 //
const NODE_SPACING_X = 140 //
const NODE_SPACING_Y = 240 //
const PADDING = 20 //
const LABEL_HEIGHT = 60 //
const NODE_SIZE = 50
const NODE_SPACING_X = 140
const NODE_SPACING_Y = 240
const PADDING = 20
const LABEL_HEIGHT = 60
const OPERATE_STATUS_I18N_MAP: Record<string, string> = {
'1': 'ProductionReport.Timeline.statusScheduled',
'3': 'ProductionReport.Timeline.statusPaused',
'4': 'ProductionReport.Timeline.statusPendingStorage',
'5': 'ProductionReport.Timeline.statusStored',
'8': 'ProductionReport.Timeline.statusStarted'
}
//
const sortedRecords = computed(() => {
if (!props.planRecordList || props.planRecordList.length === 0) {
return []
@ -76,13 +80,11 @@ const sortedRecords = computed(() => {
return [...props.planRecordList].sort((a, b) => a.operateTime - b.operateTime)
})
//
const rows = computed(() => {
if (sortedRecords.value.length === 0) return 1
return Math.ceil(sortedRecords.value.length / props.itemsPerRow)
})
// SVG viewBox
const containerWidth = computed(() => {
const nodesInRow = Math.min(sortedRecords.value.length, props.itemsPerRow)
return nodesInRow * NODE_SPACING_X + PADDING * 2
@ -92,7 +94,6 @@ const containerHeight = computed(() => {
return rows.value * NODE_SPACING_Y + PADDING * 2 + LABEL_HEIGHT
})
//
const getNodePosition = (index: number): { x: number; y: number } => {
const row = Math.floor(index / props.itemsPerRow)
const col = index % props.itemsPerRow
@ -102,7 +103,6 @@ const getNodePosition = (index: number): { x: number; y: number } => {
}
}
//
const getNodeStyle = (index: number): Record<string, string> => {
const virtualPos = getNodePosition(index)
const percentX = (virtualPos.x / containerWidth.value) * 100
@ -117,10 +117,9 @@ const getNodeStyle = (index: number): Record<string, string> => {
}
}
// 线SVG path 使线
const pathLines = computed(() => {
const paths: string[] = []
const radius = 30 //
const radius = 30
for (let i = 0; i < sortedRecords.value.length - 1; i++) {
const currentPos = getNodePosition(i)
@ -129,14 +128,12 @@ const pathLines = computed(() => {
const nextRow = Math.floor((i + 1) / props.itemsPerRow)
if (currentRow === nextRow) {
//
const startX = currentPos.x + NODE_SIZE / 2
const startY = currentPos.y
const endX = nextPos.x - NODE_SIZE / 2
const endY = nextPos.y
paths.push(`M ${startX} ${startY} L ${endX} ${endY}`)
} else {
// 使线
const startX = currentPos.x + NODE_SIZE / 2
const startY = currentPos.y
@ -148,11 +145,7 @@ const pathLines = computed(() => {
const isLastCol = currentCol === props.itemsPerRow - 1
if (isLastCol) {
// 使线
const rightX = currentPos.x + 80
// M:
// L: 线
// Q: 线
paths.push(
`M ${startX} ${startY} ` +
`L ${rightX - radius} ${startY} ` +
@ -165,7 +158,6 @@ const pathLines = computed(() => {
`
)
} else {
//
paths.push(
`M ${startX} ${startY} ` +
`L ${startX + radius} ${startY} ` +
@ -182,21 +174,18 @@ const pathLines = computed(() => {
return paths
})
//
const getStatusLabel = (status: string): string => {
return OPERATE_STATUS_MAP[status] || '未知'
const i18nKey = OPERATE_STATUS_I18N_MAP[status]
return i18nKey ? t(i18nKey) : t('ProductionReport.Timeline.unknown')
}
//
const formatTime = (timestamp: number): string => {
if (!timestamp) return '-'
return dayjs(timestamp).format('YYYY-MM-DD\nHH:mm:ss')
}
//
onMounted(() => {
if (wrapperRef.value) {
//
}
})
</script>
@ -221,7 +210,6 @@ export default {}
min-height: v-bind('`${containerHeight}px`');
}
// SVG
.timeline-svg {
position: absolute;
top: 0;
@ -231,7 +219,6 @@ export default {}
pointer-events: none;
}
// 线
.timeline-node {
display: flex;
flex-direction: column;
@ -239,7 +226,6 @@ export default {}
gap: 8px;
}
//
.node-dot {
width: 50px;
height: 50px;
@ -257,23 +243,23 @@ export default {}
z-index: 2;
&.status-1 {
background: #409eff; // -
background: #409eff;
}
&.status-3 {
background: #e6a23c; // -
background: #e6a23c;
}
&.status-4 {
background: #f56c6c; // -
background: #f56c6c;
}
&.status-5 {
background: #67c23a; // - 绿
background: #67c23a;
}
&.status-8 {
background: #409eff; // -
background: #409eff;
}
}
@ -282,7 +268,6 @@ export default {}
line-height: 1;
}
//
.node-label {
text-align: center;
width: 130px;
@ -308,7 +293,6 @@ export default {}
white-space: pre-line;
}
//
@media (max-width: 1200px) {
.timeline-wrapper {
overflow-x: auto;

@ -1,28 +1,51 @@
<template>
<div class="baogong-info-container">
<div class="action-bar">
<div class="action-bar-left">
<el-date-picker
v-model="baogongDateRange"
type="daterange"
:range-separator="t('ProductionReport.BaogongInfo.dateRangeSeparator')"
:start-placeholder="t('ProductionReport.BaogongInfo.startDatePlaceholder')"
:end-placeholder="t('ProductionReport.BaogongInfo.endDatePlaceholder')"
value-format="YYYY-MM-DD HH:mm:ss"
:default-time="[new Date(2000, 0, 1, 0, 0, 0), new Date(2000, 0, 1, 23, 59, 59)]"
@change="handleDateChange"
class="!w-280px"
/>
<el-button :type="activeShortcut === 'today' ? 'primary' : ''" @click="setShortcut('today')">{{ t('ProductionReport.BaogongInfo.buttonToday') }}</el-button>
<el-button :type="activeShortcut === '7d' ? 'primary' : ''" @click="setShortcut('7d')">{{ t('ProductionReport.BaogongInfo.buttonLast7Days') }}</el-button>
<el-button :type="activeShortcut === '30d' ? 'primary' : ''" @click="setShortcut('30d')">{{ t('ProductionReport.BaogongInfo.buttonLast30Days') }}</el-button>
<el-button @click="handleReset">
<Icon icon="ep:refresh-left" class="mr-5px" /> {{ t('ProductionReport.BaogongInfo.buttonReset') }}
</el-button>
</div>
<el-button @click="handleRefresh" :loading="loading">
<Icon icon="ep:refresh" class="mr-5px" /> {{ t('ProductionReport.BaogongInfo.buttonRefresh') }}
</el-button>
</div>
<el-table v-loading="loading" :data="baogongList" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="任务单编码" align="center" prop="taskCode" width="150px" />
<el-table-column label="计划编码" align="center" prop="planCode" width="150px" />
<el-table-column label="员工ID" align="center" prop="employeeId" width="120px" />
<el-table-column label="员工名称" align="center" prop="employeeName" width="120px" />
<el-table-column label="产品名称" align="center" prop="productName" />
<el-table-column label="产品编码" align="center" prop="productCode" width="120px" />
<el-table-column label="报工数量" align="center" prop="baogongNum" width="100px" />
<el-table-column label="合格数量" align="center" prop="passNum" width="100px" />
<el-table-column label="不合格数量" align="center" prop="noPassNum" width="100px" />
<el-table-column label="合格率%" align="center" width="100px">
<el-table-column :label="t('ProductionReport.BaogongInfo.tableTaskCode')" align="center" prop="taskCode" width="150px" />
<el-table-column :label="t('ProductionReport.BaogongInfo.tablePlanCode')" align="center" prop="planCode" width="150px" />
<el-table-column :label="t('ProductionReport.BaogongInfo.tableEmployeeId')" align="center" prop="employeeId" width="120px" />
<el-table-column :label="t('ProductionReport.BaogongInfo.tableEmployeeName')" align="center" prop="employeeName" width="120px" />
<el-table-column :label="t('ProductionReport.BaogongInfo.tableProductName')" align="center" prop="productName" />
<el-table-column :label="t('ProductionReport.BaogongInfo.tableProductCode')" align="center" prop="productCode" width="120px" />
<el-table-column :label="t('ProductionReport.BaogongInfo.tableBaogongNumber')" align="center" prop="baogongNum" width="100px" />
<el-table-column :label="t('ProductionReport.BaogongInfo.tablePassNumber')" align="center" prop="passNum" width="100px" />
<el-table-column :label="t('ProductionReport.BaogongInfo.tableNoPassNumber')" align="center" prop="noPassNum" width="100px" />
<el-table-column :label="t('ProductionReport.BaogongInfo.tablePassRate')" align="center" width="100px">
<template #default="scope">
{{ ((scope.row.passRate || 0) * 100).toFixed(2) }}
{{ scope.row.passRate }}
</template>
</el-table-column>
<el-table-column label="不合格原因" align="center" prop="reason" />
<el-table-column label="报工时间" align="center" prop="baogongTime" width="180px">
<el-table-column :label="t('ProductionReport.BaogongInfo.tableNoPassReason')" align="center" prop="reason" />
<el-table-column :label="t('ProductionReport.BaogongInfo.tableBaogongTime')" align="center" prop="baogongTime" width="180px">
<template #default="scope">
{{ formatDateTime(scope.row.baogongTime) }}
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
@ -35,9 +58,12 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { BaogongRecordApi } from '@/api/mes/baogongrecord'
import { useI18n } from '@/hooks/web/useI18n'
defineOptions({ name: 'ProductionReportBaogongInfo' })
const { t } = useI18n()
const props = defineProps<{
taskId?: number
}>()
@ -45,10 +71,14 @@ const props = defineProps<{
const loading = ref(false)
const baogongList = ref<any[]>([])
const total = ref(0)
const baogongDateRange = ref<[string, string] | null>(null)
const activeShortcut = ref<string>('')
const queryParams = reactive({
taskId: undefined as number | undefined,
pageNo: 1,
pageSize: 10
pageSize: 10,
beginBaogongTime: undefined as string | undefined,
endBaogongTime: undefined as string | undefined
})
const getList = async () => {
@ -67,7 +97,6 @@ const getList = async () => {
}
}
/** 监听任务ID变化 */
watch(
() => props.taskId,
(val: number | undefined) => {
@ -82,7 +111,54 @@ watch(
{ immediate: true }
)
/** 格式化日期时间 */
const handleRefresh = () => {
queryParams.pageNo = 1
getList()
}
const setShortcut = (type: string) => {
activeShortcut.value = type
const today = dayjs().startOf('day')
let start: dayjs.Dayjs
let end: dayjs.Dayjs
if (type === 'today') {
start = today
end = today.endOf('day')
} else if (type === '7d') {
start = today.subtract(6, 'day')
end = today.endOf('day')
} else if (type === '30d') {
start = today.subtract(29, 'day')
end = today.endOf('day')
} else {
return
}
baogongDateRange.value = [start.format('YYYY-MM-DD HH:mm:ss'), end.format('YYYY-MM-DD HH:mm:ss')]
queryParams.beginBaogongTime = start.format('YYYY-MM-DD HH:mm:ss')
queryParams.endBaogongTime = end.format('YYYY-MM-DD HH:mm:ss')
handleRefresh()
}
const handleDateChange = (val: [string, string] | null) => {
activeShortcut.value = ''
if (val) {
queryParams.beginBaogongTime = val[0]
queryParams.endBaogongTime = val[1]
} else {
queryParams.beginBaogongTime = undefined
queryParams.endBaogongTime = undefined
}
handleRefresh()
}
const handleReset = () => {
activeShortcut.value = ''
baogongDateRange.value = null
queryParams.beginBaogongTime = undefined
queryParams.endBaogongTime = undefined
handleRefresh()
}
const formatDateTime = (value: string | Date | null) => {
if (!value) return '-'
return dayjs(value).format('YYYY-MM-DD HH:mm:ss')
@ -90,6 +166,19 @@ const formatDateTime = (value: string | Date | null) => {
</script>
<style scoped>
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.action-bar-left {
display: flex;
align-items: center;
gap: 8px;
}
.section-title {
margin: 0 0 16px 0;
font-size: 14px;

@ -1,29 +1,32 @@
<template>
<div class="basic-info-container">
<!-- 任务单信息 -->
<div class="action-bar">
<el-button @click="handleRefresh" :loading="loading">
<Icon icon="ep:refresh" class="mr-5px" /> {{ t('ProductionReport.BasicInfo.buttonRefresh') }}
</el-button>
</div>
<el-table v-loading="loading" :data="taskDetailList" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="任务单编码" align="center" prop="taskCode" sortable />
<el-table-column label="产品编码" align="center" prop="barCode" sortable />
<el-table-column label="产品名称" align="center" prop="productName" sortable />
<el-table-column label="总数量" align="center" prop="number" />
<el-table-column label="已计划数量" align="center" prop="planNumber" />
<el-table-column label="未计划数量" align="center">
<el-table-column :label="t('ProductionReport.BasicInfo.tableTaskCode')" align="center" prop="taskCode" sortable />
<el-table-column :label="t('ProductionReport.BasicInfo.tableProductCode')" align="center" prop="barCode" sortable />
<el-table-column :label="t('ProductionReport.BasicInfo.tableProductName')" align="center" prop="productName" sortable />
<el-table-column :label="t('ProductionReport.BasicInfo.tableTotalNumber')" align="center" prop="number" />
<el-table-column :label="t('ProductionReport.BasicInfo.tablePlanNumber')" align="center" prop="planNumber" />
<el-table-column :label="t('ProductionReport.BasicInfo.tableUnplannedNumber')" align="center">
<template #default="scope">
<span style="color: #e66126">
{{ scope.row.number - scope.row.planNumber > 0 ? scope.row.number - scope.row.planNumber : 0 }}
</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="200px">
<el-table-column :label="t('ProductionReport.BasicInfo.tableOperate')" align="center" width="200px">
<template #default="scope">
<el-button link type="primary" @click="openProductInfo(scope.row)">
查看产品信息
{{ t('ProductionReport.BasicInfo.buttonViewProductInfo') }}
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 产品信息弹框 -->
<ProductInfoDialog ref="productInfoDialogRef" />
</div>
</template>
@ -31,11 +34,14 @@
<script setup lang="ts">
import { TaskApi } from '@/api/mes/task'
import ProductInfoDialog from './ProductInfoDialog.vue'
import { useI18n } from '@/hooks/web/useI18n'
defineOptions({ name: 'ProductionReportBasicInfo' })
const { t } = useI18n()
const props = defineProps<{
taskId?: number // task ID
taskId?: number
taskDeliveryDate?: string | number | Date
}>()
@ -43,7 +49,6 @@ const productInfoDialogRef = ref()
const loading = ref(false)
const taskDetailList = ref<any[]>([])
/** 查询任务单清单 */
const getTaskDetailList = async (taskId: number) => {
loading.value = true
try {
@ -57,7 +62,6 @@ const getTaskDetailList = async (taskId: number) => {
}
}
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.taskId,
(val: number) => {
@ -70,10 +74,13 @@ watch(
{ immediate: true, deep: true }
)
/** 打开产品信息对话框 */
const openProductInfo = (row: any) => {
if (row?.productId) {
productInfoDialogRef.value?.open(row.productId)
productInfoDialogRef.value?.open(row)
}
const handleRefresh = () => {
if (props.taskId) {
getTaskDetailList(props.taskId)
}
}
</script>
@ -85,6 +92,11 @@ const openProductInfo = (row: any) => {
gap: 20px;
}
.action-bar {
display: flex;
justify-content: flex-end;
}
.task-info-section {
padding: 16px;
background: #f5f7fa;

@ -1,40 +1,74 @@
<template>
<div class="quality-info-container">
<el-table v-loading="loading" :data="qualityList" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="质检编码" align="center" prop="code" width="150px" />
<el-table-column label="质检名称" align="center" prop="name" width="150px" />
<el-table-column label="质检类型" align="center" prop="type" width="100px">
<div class="action-bar">
<el-button @click="handleRefresh" :loading="loading">
<Icon icon="ep:refresh" class="mr-5px" /> {{ t('ProductionReport.QualityInfo.buttonRefresh') }}
</el-button>
</div>
<el-table v-loading="loading" :data="qualityList" :stripe="true" :show-overflow-tooltip="true" @expand-change="handleExpandChange">
<el-table-column type="expand">
<template #default="scope">
<dict-tag :type="DICT_TYPE.MES_QUALITY_TYPE" :value="scope.row.type" />
<div class="expand-content">
<el-table :data="scope.row.results" size="small" :show-header="true">
<el-table-column :label="t('ProductionReport.QualityInfo.expandTableName')" align="center" prop="name" min-width="150px" />
<el-table-column :label="t('ProductionReport.QualityInfo.expandTableMethod')" align="center" prop="tool" min-width="100px" />
<el-table-column :label="t('ProductionReport.QualityInfo.expandTableStandard')" align="center" prop="zjTypeName" min-width="100px" />
<el-table-column :label="t('ProductionReport.QualityInfo.expandTableUnit')" align="center" prop="unitName" min-width="80px" />
<el-table-column :label="t('ProductionReport.QualityInfo.expandTableUpperLimit')" align="center" prop="upperVal" min-width="80px" />
<el-table-column :label="t('ProductionReport.QualityInfo.expandTableLowerLimit')" align="center" prop="lowerVal" min-width="80px" />
<el-table-column :label="t('ProductionReport.QualityInfo.expandTableInputValue')" align="center" prop="textInput" min-width="100px" />
<el-table-column :label="t('ProductionReport.QualityInfo.expandTableImage')" align="center" prop="images" min-width="80px">
<template #default="scope">
<el-button v-if="scope.row.images" link type="primary" @click="handlePreviewImage(scope.row.images)">{{ t('ProductionReport.QualityInfo.expandTablePreview') }}</el-button>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column :label="t('ProductionReport.QualityInfo.expandTableResult')" align="center" prop="zjResult" min-width="100px">
<template #default="scope">
<el-tag :type="scope.row.zjResult === 1 ? 'success' : scope.row.zjResult === 2 ? 'danger' : 'info'">
{{ getResultLabel(scope.row.zjResult) }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="t('ProductionReport.QualityInfo.expandTableRemark')" align="center" prop="remark" />
</el-table>
</div>
</template>
</el-table-column>
<el-table-column label="方案名称" align="center" prop="schemaName" width="150px" />
<el-table-column label="状态" align="center" prop="status" width="100px">
<el-table-column :label="t('ProductionReport.QualityInfo.tableCode')" align="center" prop="code" min-width="140px" />
<el-table-column :label="t('ProductionReport.QualityInfo.tableCategory')" align="center" prop="type" min-width="100px">
<template #default="scope">
<dict-tag type="status" :value="scope.row.status" />
<DictTag type="mes_zj_task_type" :value="scope.row.type" />
</template>
</el-table-column>
<el-table-column label="结果" align="center" prop="result" width="100px">
<el-table-column :label="t('ProductionReport.QualityInfo.tableStatus')" align="center" prop="status" min-width="80px">
<template #default="scope">
<el-tag :type="scope.row.result === '通过' ? 'success' : 'danger'">
{{ scope.row.resultName || scope.row.result || '-' }}
</el-tag>
<DictTag :type="'job_status'" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="执行人" align="center" prop="executorName" width="100px" />
<el-table-column label="执行时间" align="center" prop="executeTime" width="180px">
<el-table-column :label="t('ProductionReport.QualityInfo.tableTicketCode')" align="center" prop="ticketCode" min-width="200px" />
<el-table-column :label="t('ProductionReport.QualityInfo.tableProcess')" align="center" prop="orgType" min-width="80px">
<template #default="scope">
<DictTag :type="DICT_TYPE.MES_ORG_TYPE" :value="scope.row.orgType" />
</template>
</el-table-column>
<el-table-column :label="t('ProductionReport.QualityInfo.tableSchema')" align="center" prop="schemaName" min-width="140px" />
<el-table-column :label="t('ProductionReport.QualityInfo.tableManager')" align="center" prop="managerName" min-width="100px" />
<el-table-column :label="t('ProductionReport.QualityInfo.tableExecutor')" align="center" prop="executorName" min-width="100px" />
<el-table-column :label="t('ProductionReport.QualityInfo.tableExecuteTime')" align="center" prop="executeTime" min-width="180px">
<template #default="scope">
{{ formatDateTime(scope.row.executeTime) }}
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180px">
<el-table-column :label="t('ProductionReport.QualityInfo.tableRemark')" align="center" prop="remark" min-width="150px" />
<el-table-column :label="t('ProductionReport.QualityInfo.tableResult')" align="center" prop="result" min-width="100px">
<template #default="scope">
{{ formatDateTime(scope.row.createTime) }}
<el-tag v-if="scope.row.result === '1' || scope.row.result === 1" type="success">{{ t('ProductionReport.QualityInfo.resultPass') }}</el-tag>
<el-tag v-else-if="scope.row.result === '2' || scope.row.result === 2" type="danger">{{ t('ProductionReport.QualityInfo.resultFail') }}</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" />
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
@ -46,17 +80,67 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { DICT_TYPE } from '@/utils/dict'
import { ZjTaskApi } from '@/api/mes/zjtask'
import { DictTag } from '@/components/DictTag'
import { DICT_TYPE } from '@/utils/dict'
import { useI18n } from '@/hooks/web/useI18n'
defineOptions({ name: 'ProductionReportQualityInfo' })
const { t } = useI18n()
interface ZjTaskResult {
id: number
zjType: number
zjTypeName: string
name: string
remark?: string
tool?: string
standardVal?: number
unit?: string
unitName?: string
upperVal?: number
lowerVal?: number
zjResult: number
images?: string
zjTime?: string
taskId: number
createTime: string
deviceId: number
textInput?: string
}
interface ZjTaskWithResults {
id: number
code: string
name?: string
type: string
schemaId: number
schemaName: string
remark?: string
ticket: number
ticketCode?: string
orgType: string
managerId?: number
managerName?: string
executorId?: number
executorName?: string
status: number
result?: string
resultName?: string
createTime: string
executeTime: string
cancelReason?: string
ticketType?: number
results: ZjTaskResult[]
}
const props = defineProps<{
taskId?: number
}>()
const loading = ref(false)
const qualityList = ref<any[]>([])
const qualityList = ref<ZjTaskWithResults[]>([])
const total = ref(0)
const queryParams = reactive({
ticket: undefined as number | undefined,
@ -80,7 +164,6 @@ const getList = async () => {
}
}
/** 监听任务ID变化 */
watch(
() => props.taskId,
(val: number | undefined) => {
@ -95,7 +178,27 @@ watch(
{ immediate: true }
)
/** 格式化日期时间 */
const handleRefresh = () => {
queryParams.pageNo = 1
getList()
}
const handleExpandChange = (_row: ZjTaskWithResults, _expandedRows: any[]) => {
}
const handlePreviewImage = (imagePath: string) => {
window.open(imagePath, '_blank')
}
const getResultLabel = (result: number): string => {
const labelMap: Record<number, string> = {
0: t('ProductionReport.QualityInfo.inspectionPending'),
1: t('ProductionReport.QualityInfo.inspectionPass'),
2: t('ProductionReport.QualityInfo.inspectionFail')
}
return labelMap[result] || t('ProductionReport.QualityInfo.unknown')
}
const formatDateTime = (value: string | Date | null) => {
if (!value) return '-'
return dayjs(value).format('YYYY-MM-DD HH:mm:ss')
@ -103,10 +206,20 @@ const formatDateTime = (value: string | Date | null) => {
</script>
<style scoped>
.action-bar {
display: flex;
justify-content: flex-end;
margin-bottom: 12px;
}
.section-title {
margin: 0 0 16px 0;
font-size: 14px;
font-weight: 600;
color: #333;
}
.expand-content {
padding: 12px;
}
</style>

@ -1,14 +1,17 @@
<template>
<div class="related-plan-container">
<!-- 加载中状态 -->
<div class="action-bar">
<el-button @click="handleRefresh" :loading="loading">
<Icon icon="ep:refresh" class="mr-5px" /> {{ t('ProductionReport.RelatedPlan.buttonRefresh') }}
</el-button>
</div>
<div v-if="loading" class="loading-wrapper">
<el-skeleton :rows="3" animated />
</div>
<!-- 空状态 -->
<el-empty v-else-if="planList.length === 0" description="暂无关联计划" />
<el-empty v-else-if="planList.length === 0" :description="t('ProductionReport.RelatedPlan.emptyDescription')" />
<!-- 卡片列表 -->
<div v-else class="card-list">
<ProductionPlanCard
v-for="plan in planList"
@ -17,7 +20,6 @@
/>
</div>
<!-- 分页 -->
<Pagination
v-if="total > 0"
:total="total"
@ -31,9 +33,12 @@
<script setup lang="ts">
import { PlanApi, type PlanVO } from '@/api/mes/plan'
import ProductionPlanCard from './ProductionPlanCard.vue'
import { useI18n } from '@/hooks/web/useI18n'
defineOptions({ name: 'ProductionReportRelatedPlan' })
const { t } = useI18n()
const props = defineProps<{
taskId?: number
}>()
@ -47,7 +52,6 @@ const queryParams = reactive({
pageSize: 10
})
//
const getList = async () => {
if (!queryParams.taskId) return
loading.value = true
@ -68,7 +72,6 @@ const getList = async () => {
}
}
// ID
watch(
() => props.taskId,
(val: number | undefined) => {
@ -83,10 +86,21 @@ watch(
},
{ immediate: true }
)
const handleRefresh = () => {
queryParams.pageNo = 1
getList()
}
</script>
<style scoped>
.action-bar {
display: flex;
justify-content: flex-end;
margin-bottom: 12px;
}
.section-title {
margin: 0 0 16px 0;
font-size: 14px;

@ -1,6 +1,5 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
@ -9,84 +8,82 @@
label-width="auto"
label-position="left"
>
<el-form-item label="任务单编码" prop="code">
<el-form-item :label="t('ProductionReport.Index.searchCodeLabel')" prop="code">
<el-input
v-model="queryParams.code"
placeholder="请输入任务单编码"
:placeholder="t('ProductionReport.Index.searchCodePlaceholder')"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="下达日期" prop="orderDate">
<el-form-item :label="t('ProductionReport.Index.searchOrderDateLabel')" prop="orderDate">
<el-date-picker
v-model="queryParams.orderDate"
@change="handleQuery"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:start-placeholder="t('ProductionReport.Index.startDatePlaceholder')"
:end-placeholder="t('ProductionReport.Index.endDatePlaceholder')"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="交货日期" prop="deliveryDate">
<el-form-item :label="t('ProductionReport.Index.searchDeliveryDateLabel')" prop="deliveryDate">
<el-date-picker
v-model="queryParams.deliveryDate"
value-format="YYYY-MM-DD HH:mm:ss"
@change="handleQuery"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:start-placeholder="t('ProductionReport.Index.startDatePlaceholder')"
:end-placeholder="t('ProductionReport.Index.endDatePlaceholder')"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-form-item :label="t('ProductionReport.Index.searchRemarkLabel')" prop="remark">
<el-input
v-model="queryParams.remark"
placeholder="请输入备注"
:placeholder="t('ProductionReport.Index.searchRemarkPlaceholder')"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-form-item :label="t('ProductionReport.Index.searchCreateTimeLabel')" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
@change="handleQuery"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:start-placeholder="t('ProductionReport.Index.startDatePlaceholder')"
:end-placeholder="t('ProductionReport.Index.endDatePlaceholder')"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> 搜索
<Icon icon="ep:search" class="mr-5px" /> {{ t('ProductionReport.Index.buttonSearch') }}
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" /> 重置
<Icon icon="ep:refresh" class="mr-5px" /> {{ t('ProductionReport.Index.buttonReset') }}
</el-button>
<el-button type="success" plain @click="handleExport" :loading="exportLoading">
<Icon icon="ep:download" class="mr-5px" /> 导出
<Icon icon="ep:download" class="mr-5px" /> {{ t('ProductionReport.Index.buttonExport') }}
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<!-- 状态Tabs -->
<el-tabs v-model="activeStatusTab" @tab-click="handleTabClick">
<el-tab-pane label="全部" name="" />
<el-tab-pane label="已下达" name="2" />
<el-tab-pane label="部分排产" name="7" />
<el-tab-pane label="待生产" name="8" />
<el-tab-pane label="生产中" name="9" />
<el-tab-pane label="已完成" name="10" />
<el-tab-pane :label="t('ProductionReport.Index.tabAll')" name="" />
<el-tab-pane :label="t('ProductionReport.Index.tabIssued')" name="2" />
<el-tab-pane :label="t('ProductionReport.Index.tabPartialScheduled')" name="7" />
<el-tab-pane :label="t('ProductionReport.Index.tabPendingProduction')" name="8" />
<el-tab-pane :label="t('ProductionReport.Index.tabInProduction')" name="9" />
<el-tab-pane :label="t('ProductionReport.Index.tabCompleted')" name="10" />
</el-tabs>
<el-table
v-loading="loading"
@ -96,42 +93,42 @@
highlight-current-row
@current-change="handleCurrentChange"
>
<el-table-column label="任务单编码" align="center" prop="code" width="200px" sortable />
<el-table-column label="下达日期" align="center" prop="orderDate" :formatter="dateFormatter2" sortable />
<el-table-column :label="t('ProductionReport.Index.tableCode')" align="center" prop="code" width="200px" sortable />
<el-table-column :label="t('ProductionReport.Index.tableOrderDate')" align="center" prop="orderDate" :formatter="dateFormatter2" sortable />
<el-table-column
label="交货日期"
:label="t('ProductionReport.Index.tableDeliveryDate')"
align="center"
prop="deliveryDate"
:formatter="deliveryDateFormatter"
sortable
/>
<el-table-column label="状态" align="center" prop="status" sortable>
<el-table-column :label="t('ProductionReport.Index.tableStatus')" align="center" prop="status" sortable>
<template #default="scope">
<dict-tag :type="DICT_TYPE.MES_TASK_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="是否排产" align="center">
<el-table-column :label="t('ProductionReport.Index.tableIsScheduled')" align="center">
<template #default="scope">
<el-tag :type="scope.row.isScheduled ? 'success' : 'info'">{{ scope.row.isScheduled ? '是' : '否' }}</el-tag>
<el-tag :type="scope.row.isScheduled ? 'success' : 'info'">{{ scope.row.isScheduled ? t('ProductionReport.Index.yes') : t('ProductionReport.Index.no') }}</el-tag>
</template>
</el-table-column>
<el-table-column label="生产进度" align="center" min-width="180px">
<el-table-column :label="t('ProductionReport.Index.tableProductionProgress')" align="center" min-width="60px">
<template #default="scope">
<div class="production-progress-cell">
<el-tooltip :content="getProductionProgressPercent(scope.row) + '%'" placement="top">
<el-progress
type="circle"
:percentage="getProductionProgressPercent(scope.row)"
:width="40"
:stroke-width="4"
:show-text="false"
:stroke-width="12"
class="production-progress-bar"
:color="getProductionProgressPercent(scope.row) === 100 ? '#67c23a' : undefined"
/>
</el-tooltip>
<span class="production-progress-text">{{ getProductionProgressPercent(scope.row) }}%</span>
</div>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column :label="t('ProductionReport.Index.tableRemark')" align="center" prop="remark" />
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
@ -140,23 +137,18 @@
/>
</ContentWrap>
<!-- 详情Tabs -->
<ContentWrap v-if="currentRow && currentRow.id">
<el-tabs v-model="activeTabName">
<!-- 基础信息 -->
<el-tab-pane label="基础信息" name="basicInfo">
<el-tab-pane :label="t('ProductionReport.Index.detailTabBasicInfo')" name="basicInfo">
<ProductionReportBasicInfo :task-id="currentRow.id" :task-delivery-date="currentRow.deliveryDate" />
</el-tab-pane>
<!-- 关联计划 -->
<el-tab-pane label="关联计划" name="relatedPlan">
<el-tab-pane :label="t('ProductionReport.Index.detailTabRelatedPlan')" name="relatedPlan">
<ProductionReportRelatedPlan :task-id="currentRow.id" />
</el-tab-pane>
<!-- 质检信息 -->
<el-tab-pane label="质检信息" name="qualityInfo">
<el-tab-pane :label="t('ProductionReport.Index.detailTabQualityInfo')" name="qualityInfo">
<ProductionReportQualityInfo :task-id="currentRow.id" />
</el-tab-pane>
<!-- 报工信息 -->
<el-tab-pane label="报工信息" name="baogongInfo">
<el-tab-pane :label="t('ProductionReport.Index.detailTabBaogongInfo')" name="baogongInfo">
<ProductionReportBaogongInfo :task-id="currentRow.id" />
</el-tab-pane>
</el-tabs>
@ -172,15 +164,16 @@ import ProductionReportBasicInfo from './components/ProductionReportBasicInfo.vu
import ProductionReportRelatedPlan from './components/ProductionReportRelatedPlan.vue'
import ProductionReportQualityInfo from './components/ProductionReportQualityInfo.vue'
import ProductionReportBaogongInfo from './components/ProductionReportBaogongInfo.vue'
import { useI18n } from '@/hooks/web/useI18n'
/** 生产报表 列表 */
defineOptions({ name: 'ProductionReport' })
const message = useMessage() //
const { t } = useI18n()
const message = useMessage()
const loading = ref(true) //
const list = ref<TaskVO[]>([]) //
const total = ref(0) //
const loading = ref(true)
const list = ref<TaskVO[]>([])
const total = ref(0)
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
@ -191,11 +184,11 @@ const queryParams = reactive({
remark: undefined,
createTime: []
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
const queryFormRef = ref()
const exportLoading = ref(false)
const activeTabName = ref('basicInfo') // tab
const activeStatusTab = ref('') // tab
const activeTabName = ref('basicInfo')
const activeStatusTab = ref('')
const deliveryDateFormatter = (_row: any, _column: any, value: any) => {
if (value) return dateFormatter2(_row, _column, value)
@ -210,7 +203,6 @@ const getProductionProgressPercent = (row: any) => {
return Math.max(0, Math.min(100, Number(rawPercent.toFixed(2))))
}
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
@ -222,35 +214,29 @@ const getList = async () => {
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
//
await message.exportConfirm()
//
exportLoading.value = true
const data = await TaskApi.exportTask(queryParams)
download.excel(data, '生产报表.xls')
download.excel(data, t('ProductionReport.Index.exportFilename') + '.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 选中行操作 */
const currentRow = ref<TaskVO>({} as TaskVO) //
const currentRow = ref<TaskVO>({} as TaskVO)
const handleCurrentChange = (row: TaskVO | null) => {
if (row) {
currentRow.value = row
@ -258,7 +244,6 @@ const handleCurrentChange = (row: TaskVO | null) => {
}
}
/** tab 切换 */
const handleTabClick = (tab: any) => {
queryParams.status = tab.paneName === '' ? undefined : tab.paneName
currentRow.value = {} as TaskVO
@ -266,7 +251,6 @@ const handleTabClick = (tab: any) => {
getList()
}
/** 初始化 **/
onMounted(() => {
getList()
})
@ -277,9 +261,17 @@ onMounted(() => {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.production-progress-bar {
width: 180px;
.production-progress-cell :deep(.el-progress-circle) {
display: flex;
align-items: center;
justify-content: center;
}
.production-progress-text {
font-size: 12px;
white-space: nowrap;
}
</style>

@ -1,5 +1,17 @@
<template>
<Dialog v-model="previewVisible" title="排产甘特图预览" width="100%" align-center>
<div class="preview-options">
<el-form :inline="true">
<el-form-item label="领料人">
<el-select v-model="scheduleOptions.workerId" clearable filterable placeholder="请选择领料人" style="width: 200px">
<el-option v-for="item in workerList" :key="item.id" :label="item.nickname" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="是否计算损耗">
<el-switch v-model="scheduleOptions.isCalculateLoss" />
</el-form-item>
</el-form>
</div>
<ScheduleGanttPanelEditable :schedule-list="previewScheduleList" height="800px" />
<template #footer>
<el-button type="primary" :loading="previewSaveLoading" @click="handlePreviewSave"></el-button>
@ -10,6 +22,7 @@
<script setup lang="ts">
import { PlanApi } from '@/api/mes/plan'
import * as UserApi from '@/api/system/user'
import ScheduleGanttPanelEditable from './ScheduleGanttPanelEditable.vue'
import dayjs from 'dayjs'
@ -28,11 +41,31 @@ const emit = defineEmits<{
const message = useMessage()
const previewSaveLoading = ref(false)
const workerList = ref<UserApi.UserVO[]>([])
const scheduleOptions = reactive({
workerId: undefined as number | undefined,
isCalculateLoss: true
})
const loadWorkerList = async () => {
try {
workerList.value = await UserApi.getSimpleUserList()
} catch {
workerList.value = []
}
}
const previewVisible = computed({
get: () => props.modelValue,
set: (value: boolean) => emit('update:modelValue', value)
})
watch(previewVisible, (visible) => {
if (visible && workerList.value.length === 0) {
loadWorkerList()
}
})
const previewScheduleList = computed(() => (Array.isArray(props.scheduleList) ? props.scheduleList : []))
const handlePreviewSave = async () => {
@ -56,7 +89,8 @@ const handlePreviewSave = async () => {
planStartTime: startValue.isValid() ? startValue.valueOf() : undefined,
planEndTime: endValue.isValid() ? endValue.valueOf() : undefined,
reyaNumber: Number(plan?.reyaNumber ?? plan?.planNumber ?? 0),
workerId: plan?.workerId,
workerId: scheduleOptions.workerId ?? plan?.workerId,
isCalculateLoss: scheduleOptions.isCalculateLoss,
feedingPipeline: plan?.feedingPipeline ?? device?.deviceId,
deviceId: device?.deviceId ?? plan?.deviceId,
deliveryDate: deliveryDateValue.isValid() ? deliveryDateValue.valueOf() : undefined,

@ -85,17 +85,18 @@ v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" hi
<el-tag :type="scope.row.isScheduled ? 'success' : 'info'">{{ scope.row.isScheduled ? '是' : '否' }}</el-tag>
</template>
</el-table-column>
<el-table-column :label="t('ProductionPlan.TaskSummary.tableProductionProgressColumn')" align="center" min-width="180px">
<el-table-column :label="t('ProductionPlan.TaskSummary.tableProductionProgressColumn')" align="center" min-width="60px">
<template #default="scope">
<div class="production-progress-cell">
<el-tooltip :content="getProductionProgressPercent(scope.row) + '%'" placement="top">
<el-progress
type="circle"
:percentage="getProductionProgressPercent(scope.row)"
:width="40"
:stroke-width="4"
:show-text="false"
:stroke-width="12"
class="production-progress-bar"
:color="getProductionProgressPercent(scope.row) === 100 ? '#67c23a' : undefined"
/>
</el-tooltip>
<span class="production-progress-text">{{ getProductionProgressPercent(scope.row) }}%</span>
</div>
</template>
</el-table-column>
@ -274,9 +275,17 @@ const handleTabClick = (tab: TabsPaneContext) => {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.production-progress-bar {
width: 180px;
.production-progress-cell :deep(.el-progress-circle) {
display: flex;
align-items: center;
justify-content: center;
}
.production-progress-text {
font-size: 12px;
white-space: nowrap;
}
</style>

@ -11,6 +11,18 @@
</el-col>
</el-row>
</el-form-item>
<el-form-item :label="t('QualityManagement.ZjTask.formTicketType')" prop="ticketType">
<el-radio-group v-model="formData.ticketType" @change="ticketTypeChange" :disabled="lockedFields.ticketType">
<el-radio :label="1">{{ t('QualityManagement.ZjTask.ticketTypeProcess') }}</el-radio>
<el-radio :label="2">{{ t('QualityManagement.ZjTask.ticketTypeStorage') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="formData.ticketType" :label="t('QualityManagement.ZjTask.formTicket')" prop="ticket">
<el-select v-model="formData.ticket" clearable filterable :disabled="lockedFields.ticket"
:placeholder="t('QualityManagement.ZjTask.placeholderFormTicket')" @change="fetchProductIdByTicket" @visible-change="handleTicketVisibleChange">
<el-option v-for="item in planList" :key="item.id" :label="item.code" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item :label="t('QualityManagement.ZjTask.formType')" prop="type">
<el-select v-model="formData.type" clearable filterable
:placeholder="t('QualityManagement.ZjTask.placeholderFormType')">
@ -18,7 +30,7 @@
:value="dict.value" />
</el-select>
</el-form-item>
<el-form-item :label="t('QualityManagement.ZjTask.formSchema')" prop="schemaId">
<el-form-item v-if="formData.ticket" :label="t('QualityManagement.ZjTask.formSchema')" prop="schemaId">
<el-row :gutter="8" class="w-full">
<el-col :span="22">
<el-input v-model="formData.schemaName" :placeholder="t('QualityManagement.ZjTask.placeholderFormSchema')"
@ -31,18 +43,6 @@
</el-col>
</el-row>
</el-form-item>
<el-form-item :label="t('QualityManagement.ZjTask.formTicketType')" prop="ticketType">
<el-radio-group v-model="formData.ticketType" @change="ticketTypeChange">
<el-radio :label="1">{{ t('QualityManagement.ZjTask.ticketTypeProcess') }}</el-radio>
<el-radio :label="2">{{ t('QualityManagement.ZjTask.ticketTypeStorage') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="t('QualityManagement.ZjTask.formTicket')" prop="ticket">
<el-select v-model="formData.ticket" clearable filterable
:placeholder="t('QualityManagement.ZjTask.placeholderFormTicket')">
<el-option v-for="item in planList" :key="item.id" :label="item.code" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item :label="t('QualityManagement.ZjTask.formOrgType')" prop="orgType" v-if="formData.ticketType === 1">
<el-select v-model="formData.orgType" clearable
:placeholder="t('QualityManagement.ZjTask.placeholderFormOrgType')">
@ -123,6 +123,7 @@ const dialogTitle = ref('')
const formLoading = ref(false)
const formType = ref('')
const planList = ref<PlanVO[]>([])
const lockedFields = ref<{ ticketType?: boolean; ticket?: boolean }>({})
const formData = ref<{ [key: string]: any }>({
id: undefined,
@ -165,6 +166,7 @@ const schemaDialogVisible = ref(false)
const schemaLoading = ref(false)
const schemaList = ref<ZjSchemaVO[]>([])
const selectedSchemaId = ref<number | undefined>(undefined)
const currentProductId = ref<number | undefined>(undefined)
const orgTypeOptions = getStrDictOptions(DICT_TYPE.MES_ORG_TYPE)
const userList = ref<UserApi.UserVO[]>([])
@ -192,10 +194,13 @@ const formatSchemaVal = (val: string | number | null | undefined) => {
}
const loadSchemaList = async () => {
if (schemaList.value.length) return
schemaLoading.value = true
try {
const data = await ZjSchemaApi.getZjSchemaList()
const params: any = {}
if (formData.value.ticketType === 1 && currentProductId.value) {
params.productId = currentProductId.value
}
const data = await ZjSchemaApi.getZjSchemaList(params)
schemaList.value = data || []
} finally {
schemaLoading.value = false
@ -204,6 +209,7 @@ const loadSchemaList = async () => {
const openSchemaDialog = async () => {
schemaDialogVisible.value = true
schemaList.value = []
await loadSchemaList()
}
@ -243,10 +249,12 @@ const resetForm = () => {
ticketType: undefined
}
selectedSchemaId.value = undefined
lockedFields.value = {}
currentProductId.value = undefined
formRef.value?.resetFields()
}
const open = async (type: string, record?: ZjTaskVO) => {
const open = async (type: string, record?: ZjTaskVO, preset?: { ticketType?: number; ticket?: number; ticketName?: string; productId?: number }) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
@ -269,6 +277,20 @@ const open = async (type: string, record?: ZjTaskVO) => {
}
selectedSchemaId.value = record.schemaId
}
if (preset) {
if (preset.ticketType !== undefined) {
formData.value.ticketType = preset.ticketType
lockedFields.value.ticketType = true
}
if (preset.ticket !== undefined) {
formData.value.ticket = preset.ticket
lockedFields.value.ticket = true
planList.value = [{ id: preset.ticket, code: preset.ticketName || '' }] as PlanVO[]
}
if (preset.productId !== undefined) {
currentProductId.value = preset.productId
}
}
}
const handleManagerChange = (value: number | undefined) => {
@ -304,10 +326,27 @@ const submitForm = async () => {
}
const ticketTypeChange = async () => {
formData.value.ticket = undefined
currentProductId.value = undefined
if (formData.value.ticketType != undefined) {
const type = formData.value.ticketType === 1 ? 8 : formData.value.ticketType
const data = await PlanApi.getPlanByTicketType(type)
planList.value = data || []
}
}
const fetchProductIdByTicket = async () => {
if (!formData.value.ticket) return
const plan = planList.value.find((item: any) => item.id === formData.value.ticket)
if (plan?.productId) {
currentProductId.value = plan.productId
}
}
const handleTicketVisibleChange = async (visible: boolean) => {
if (!visible || !formData.value.ticketType) return
const type = formData.value.ticketType === 1 ? 8 : formData.value.ticketType
const data = await PlanApi.getPlanByTicketType(type)
planList.value = data || []
}
</script>

@ -164,6 +164,7 @@ type="success" plain @click="handleExport" :loading="exportLoading"
type="primary"
@click.stop="openForm('update', scope.row)"
v-hasPermi="['mes:zj-task:update']"
v-if="scope.row.result !== '1' && scope.row.result !== '2'"
>
{{ t('action.update') }}
</el-button>

@ -48,6 +48,12 @@
</template>
</el-table-column>
</el-table>
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<ZjTaskResultDialog ref="resultDialogRef" @success="getList" />
@ -60,13 +66,12 @@ import { DictTag } from '@/components/DictTag'
import { DICT_TYPE } from '@/utils/dict'
import ZjTaskResultDialog from '@/views/mes/zjTask/components/ZjTaskResultDialog.vue'
/** 质量管理-质检参数 列表 */
defineOptions({ name: 'ZjProductPreList' })
type ZjTaskListRow = ZjTaskVO & { ticketCode?: string; executeTime?: string }
const props = defineProps<{
ticket?: number | string // id
ticket?: number | string
}>()
const emit = defineEmits<{
@ -75,22 +80,30 @@ const emit = defineEmits<{
const { t } = useI18n()
const loading = ref(false) //
const list = ref<ZjTaskListRow[]>([]) //
const loading = ref(false)
const list = ref<ZjTaskListRow[]>([])
const total = ref(0)
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
ticket: undefined as string | undefined
})
const resultDialogRef = ref()
/** 查询列表 */
const getList = async () => {
const ticket = props.ticket === undefined || props.ticket === null ? '' : String(props.ticket)
if (!ticket) {
list.value = []
total.value = 0
emit('inspectableChange', false)
return
}
loading.value = true
try {
const data = await ZjTaskApi.getZjTaskList({ ticket })
list.value = Array.isArray(data) ? data : data?.list ?? data?.data ?? []
queryParams.ticket = ticket
const data = await ZjTaskApi.getZjTaskPage(queryParams)
list.value = data.list ?? []
total.value = data.total ?? 0
const hasPending = list.value.some((item) => String(item?.status) === '0')
emit('inspectableChange', hasPending)
} finally {
@ -118,9 +131,11 @@ const handleRowClick = (row: ZjTaskListRow) => {
watch(
() => props.ticket,
() => {
queryParams.pageNo = 1
getList()
},
{ immediate: true }
)
defineExpose({ getList })
</script>

@ -515,6 +515,14 @@ const open = async (type: string, id?: number) => {
const data = await ZjSchemaApi.getZjSchema(id)
formData.value = data
initSelectedItemIds()
if (formData.value.product) {
const pIds = formData.value.product.toString().split(',').map((id: string) => Number(id)).filter((id: number) => !Number.isNaN(id))
productIds.value = pIds
try {
const allProducts = await ProductApi.getProductSimpleList({ categoryId: 2 })
productSelectedRows.value = (allProducts || []).filter((p: any) => pIds.includes(p.id))
} catch { productSelectedRows.value = [] }
}
if (formData.value.type === 'Sample') {
if (formData.value.sampleMethod === 'Rate') {
const n = Number(formData.value.val)

Loading…
Cancel
Save