feat:添加生产报表页面

pull/1/head
黄伟杰 1 month ago
parent 75ab2d74d4
commit 20dee1da73

@ -49,5 +49,9 @@ export const BomApi = {
// 获得产品BOM明细列表
getBomDetailListByBomId: async (bomId) => {
return await request.get({ url: `/mes/bom/bom-detail/list-by-bom-id?bomId=` + bomId })
},
getBomByProductId: async (productId: number) => {
return await request.get({ url: `/mes/bom/getByProductId`, params: { productId } })
}
}

@ -1,6 +1,36 @@
import request from '@/config/axios'
import {ItemRequisitionVO} from "@/api/mes/itemrequisition";
// 计划记录状态枚举
export enum OperateStatusEnum {
SCHEDULED = '1', // 已排产
PAUSED = '3', // 暂停
PENDING_WAREHOUSE = '4', // 待入库
WAREHOUSED = '5', // 已入库
STARTED = '8' // 已开工
}
// 计划记录状态映射
export const OPERATE_STATUS_MAP: Record<string, string> = {
'1': '已排产',
'3': '暂停',
'4': '待入库',
'5': '已入库',
'8': '已开工'
}
// 计划记录 VO
export interface PlanRecordVO {
id: number // ID
taskId: number // 任务ID
planId: number // 计划ID
operateStatus: string // 操作状态 ('1' | '3' | '4' | '5' | '8')
operateTime: number // 操作时间戳(毫秒)
remark: string // 备注
isEnable: boolean // 是否启用
createTime: number // 创建时间戳
}
// 生产计划 VO
export interface PlanVO {
id: number // ID
@ -26,6 +56,10 @@ export interface PlanVO {
feedingPipelineName: string
wangongNumber: number
passRate: number
passNumber?: number // 合格数量
noPassNumber?: number // 不合格数量
deviceName?: string // 设备名称
planRecordList?: PlanRecordVO[] // 计划记录列表
}
export interface DevicePlanGanttPlanVO {
@ -139,5 +173,9 @@ export const PlanApi = {
},
getGanttByDevice: async (params: { startTime: string; endTime: string }) => {
return await request.get<DevicePlanGanttRespVO[]>({ url: `/mes/plan/gantt-by-device`, params })
},
getPlanPageByTask: async (params: any) => {
return await request.get({ url: `/mes/plan/page-by-task`, params })
}
}

@ -44,6 +44,10 @@ export const ZjTaskApi = {
return await request.get({ url: `/mes/zj-task/list`, params })
},
getZjTaskPageByTicket: async (params: any) => {
return await request.get({ url: `/mes/zj-task/page-by-ticket`, params })
},
createZjTask: async (data: ZjTaskVO) => {
return await request.post({ url: `/mes/zj-task/create`, data })
},

@ -0,0 +1,54 @@
<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-table>
</el-dialog>
</template>
<script setup lang="ts">
import { BomApi } from '@/api/mes/bom'
defineOptions({ name: 'ProductInfoDialog' })
const dialogVisible = ref(false)
const loading = ref(false)
const productList = ref<any[]>([])
const open = async (productId: number) => {
dialogVisible.value = true
loading.value = true
try {
const data = await BomApi.getBomByProductId(productId)
productList.value = data || []
} catch (error) {
console.error('Failed to fetch product info:', error)
productList.value = []
} finally {
loading.value = false
}
}
const handleClose = () => {
productList.value = []
}
defineExpose({
open
})
</script>
<style scoped></style>

@ -0,0 +1,247 @@
<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="value">{{ plan.productName || '-' }}</span>
</span>
<span class="basic-item">
<span class="label">设备名称:</span>
<span class="value">{{ plan.deviceName || '-' }}</span>
</span>
</div>
<div class="plan-basic">
<span class="basic-item">
<span class="label">计划开始时间:</span>
<span class="value">{{ formatDateTime(plan.planStartTime) }}</span>
</span>
<span class="basic-item">
<span class="label">计划结束时间:</span>
<span class="value">{{ formatDateTime(plan.planEndTime) }}</span>
</span>
<span class="basic-item">
<span class="label">最晚开始时间:</span>
<span class="value">{{ formatDateTime(plan.latestStartTime) }}</span>
</span>
</div>
</div>
<div class="header-right">
<div class="stat-group">
<div class="stat-item">
<span class="stat-label">计划数量</span>
<span class="stat-value">{{ plan.planNumber || 0 }}</span>
</div>
<div class="stat-item">
<span class="stat-label">完工数量</span>
<span class="stat-value">{{ plan.wangongNumber || 0 }}</span>
</div>
<div class="stat-item">
<span class="stat-label">合格数量</span>
<span class="stat-value">{{ plan.passNumber || 0 }}</span>
</div>
<div class="stat-item">
<span class="stat-label">不合格数量</span>
<span class="stat-value">{{ plan.noPassNumber || 0 }}</span>
</div>
</div>
<div class="pass-rate">
<span class="rate-label">合格率</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" />
</div>
</div>
</template>
<script setup lang="ts">
import dayjs from 'dayjs'
import type { PlanVO } from '@/api/mes/plan'
import ProductionPlanTimeline from './ProductionPlanTimeline.vue'
defineOptions({ name: 'ProductionPlanCard' })
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')
}
</script>
<script lang="ts">
export default {}
</script>
<style scoped lang="scss">
.card {
background: white;
border: 1px solid #ebeef5;
border-radius: 6px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08);
overflow: hidden;
margin-bottom: 16px;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.12);
}
}
//
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 20px;
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
}
.header-left {
flex: 1;
}
.plan-id {
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.plan-basic {
display: flex;
gap: 16px;
flex-wrap: wrap;
align-items: center;
}
.basic-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
.label {
color: #909399;
font-weight: 500;
}
.value {
color: #606266;
}
}
.header-right {
display: flex;
gap: 20px;
align-items: center;
}
.stat-group {
display: flex;
gap: 12px;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 8px 12px;
background: #f5f7fa;
border-radius: 4px;
min-width: 80px;
.stat-label {
font-size: 12px;
color: #909399;
}
.stat-value {
font-size: 16px;
font-weight: 600;
color: #333;
}
}
.pass-rate {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 8px 16px;
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
border-left: 3px solid #409eff;
border-radius: 4px;
min-width: 100px;
.rate-label {
font-size: 12px;
color: #666;
}
.rate-value {
font-size: 18px;
font-weight: 700;
color: #409eff;
}
}
// 线
.card-timeline {
padding: 16px 20px;
background: #fafafa;
border-bottom: 1px solid #f0f0f0;
}
//
@media (max-width: 1400px) {
.header-right {
flex-wrap: wrap;
gap: 12px;
}
.stat-group {
flex-wrap: wrap;
}
}
@media (max-width: 768px) {
.card-header {
flex-direction: column;
gap: 12px;
}
.plan-basic {
flex-direction: column;
align-items: flex-start;
}
.header-right {
width: 100%;
flex-direction: column;
}
.stat-group {
width: 100%;
justify-content: flex-start;
}
}
</style>

@ -0,0 +1,326 @@
<template>
<div class="timeline-wrapper" ref="wrapperRef">
<!-- 时间线节点 -->
<div class="timeline-container">
<div
v-for="(record, index) in sortedRecords"
:key="record.id"
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"
:viewBox="`0 0 ${containerWidth} ${containerHeight}`"
preserveAspectRatio="none"
>
<path
v-for="(pathStr, idx) in pathLines"
:key="`path-${idx}`"
:d="pathStr"
fill="none"
stroke="#d9d9d9"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</div>
</div>
</template>
<script setup lang="ts">
import dayjs from 'dayjs'
import type { PlanRecordVO } from '@/api/mes/plan'
import { OPERATE_STATUS_MAP } from '@/api/mes/plan'
defineOptions({ name: 'ProductionPlanTimeline' })
interface Props {
planRecordList?: PlanRecordVO[]
itemsPerRow?: number // 6
}
const props = withDefaults(defineProps<Props>(), {
planRecordList: () => [],
itemsPerRow: 6
})
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 sortedRecords = computed(() => {
if (!props.planRecordList || props.planRecordList.length === 0) {
return []
}
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
})
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
return {
x: col * NODE_SPACING_X + NODE_SPACING_X / 2 + PADDING,
y: row * NODE_SPACING_Y + PADDING + NODE_SIZE / 2
}
}
//
const getNodeStyle = (index: number): Record<string, string> => {
const virtualPos = getNodePosition(index)
const percentX = (virtualPos.x / containerWidth.value) * 100
const percentY = (virtualPos.y / containerHeight.value) * 100
return {
position: 'absolute',
left: `calc(${percentX}% - ${NODE_SIZE / 2}px)`,
top: `calc(${percentY}% - ${NODE_SIZE / 2}px)`,
width: `${NODE_SIZE}px`,
height: `${NODE_SIZE + LABEL_HEIGHT}px`
}
}
// 线SVG path 使线
const pathLines = computed(() => {
const paths: string[] = []
const radius = 30 //
for (let i = 0; i < sortedRecords.value.length - 1; i++) {
const currentPos = getNodePosition(i)
const nextPos = getNodePosition(i + 1)
const currentRow = Math.floor(i / props.itemsPerRow)
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
const nextX = nextPos.x - NODE_SIZE / 2
const nextY = nextPos.y
const midY = currentPos.y + NODE_SPACING_Y / 2
const currentCol = i % props.itemsPerRow
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} ` +
`Q ${rightX} ${startY} ${rightX} ${startY + radius} ` +
`L ${rightX} ${midY - radius} ` +
`Q ${rightX} ${midY} ${rightX - radius} ${midY} ` +
`L ${nextX + radius} ${midY} ` +
`Q ${nextX} ${midY} ${nextX} ${midY + radius} ` +
`L ${nextX} ${nextY}
`
)
} else {
//
paths.push(
`M ${startX} ${startY} ` +
`L ${startX + radius} ${startY} ` +
`Q ${startX + radius + 20} ${startY} ${startX + radius + 20} ${startY + radius} ` +
`L ${startX + radius + 20} ${midY - radius} ` +
`Q ${startX + radius + 20} ${midY} ${nextX} ${midY} ` +
`L ${nextX} ${nextY}
`
)
}
}
}
return paths
})
//
const getStatusLabel = (status: string): string => {
return OPERATE_STATUS_MAP[status] || '未知'
}
//
const formatTime = (timestamp: number): string => {
if (!timestamp) return '-'
return dayjs(timestamp).format('YYYY-MM-DD\nHH:mm:ss')
}
//
onMounted(() => {
if (wrapperRef.value) {
//
}
})
</script>
<script lang="ts">
export default {}
</script>
<style scoped lang="scss">
.timeline-wrapper {
position: relative;
width: 100%;
padding: 20px;
background: #fafafa;
border-radius: 4px;
overflow: auto;
}
.timeline-container {
position: relative;
width: 100%;
min-height: v-bind('`${containerHeight}px`');
}
// SVG
.timeline-svg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
// 线
.timeline-node {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
//
.node-dot {
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
color: white;
position: relative;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
background: #909399;
border: 3px solid white;
flex-shrink: 0;
z-index: 2;
&.status-1 {
background: #409eff; // -
}
&.status-3 {
background: #e6a23c; // -
}
&.status-4 {
background: #f56c6c; // -
}
&.status-5 {
background: #67c23a; // - 绿
}
&.status-8 {
background: #409eff; // -
}
}
.node-number {
font-size: 20px;
line-height: 1;
}
//
.node-label {
text-align: center;
width: 130px;
margin-top: 4px;
}
.status-badge {
display: inline-block;
padding: 4px 8px;
background: #f0f9ff;
color: #409eff;
border-radius: 3px;
font-size: 12px;
font-weight: 500;
margin-bottom: 4px;
white-space: nowrap;
}
.node-time {
font-size: 11px;
color: #909399;
line-height: 1.4;
white-space: pre-line;
}
//
@media (max-width: 1200px) {
.timeline-wrapper {
overflow-x: auto;
}
.node-label {
width: 120px;
font-size: 10px;
}
.node-time {
font-size: 10px;
}
}
</style>

@ -0,0 +1,99 @@
<template>
<div class="baogong-info-container">
<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">
<template #default="scope">
{{ ((scope.row.passRate || 0) * 100).toFixed(2) }}
</template>
</el-table-column>
<el-table-column label="不合格原因" align="center" prop="reason" />
<el-table-column label="报工时间" 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"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</div>
</template>
<script setup lang="ts">
import dayjs from 'dayjs'
import { BaogongRecordApi } from '@/api/mes/baogongrecord'
defineOptions({ name: 'ProductionReportBaogongInfo' })
const props = defineProps<{
taskId?: number
}>()
const loading = ref(false)
const baogongList = ref<any[]>([])
const total = ref(0)
const queryParams = reactive({
taskId: undefined as number | undefined,
pageNo: 1,
pageSize: 10
})
const getList = async () => {
if (!queryParams.taskId) return
loading.value = true
try {
const data = await BaogongRecordApi.getBaogongRecordStatPage(queryParams)
baogongList.value = data?.list || []
total.value = data?.total || 0
} catch (error) {
console.error('Failed to fetch baogong info:', error)
baogongList.value = []
total.value = 0
} finally {
loading.value = false
}
}
/** 监听任务ID变化 */
watch(
() => props.taskId,
(val: number | undefined) => {
if (!val) {
baogongList.value = []
return
}
queryParams.taskId = val as number
queryParams.pageNo = 1
getList()
},
{ immediate: true }
)
/** 格式化日期时间 */
const formatDateTime = (value: string | Date | null) => {
if (!value) return '-'
return dayjs(value).format('YYYY-MM-DD HH:mm:ss')
}
</script>
<style scoped>
.section-title {
margin: 0 0 16px 0;
font-size: 14px;
font-weight: 600;
color: #333;
}
</style>

@ -0,0 +1,100 @@
<template>
<div class="basic-info-container">
<!-- 任务单信息 -->
<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">
<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">
<template #default="scope">
<el-button link type="primary" @click="openProductInfo(scope.row)">
查看产品信息
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 产品信息弹框 -->
<ProductInfoDialog ref="productInfoDialogRef" />
</div>
</template>
<script setup lang="ts">
import { TaskApi } from '@/api/mes/task'
import ProductInfoDialog from './ProductInfoDialog.vue'
defineOptions({ name: 'ProductionReportBasicInfo' })
const props = defineProps<{
taskId?: number // task ID
taskDeliveryDate?: string | number | Date
}>()
const productInfoDialogRef = ref()
const loading = ref(false)
const taskDetailList = ref<any[]>([])
/** 查询任务单清单 */
const getTaskDetailList = async (taskId: number) => {
loading.value = true
try {
const data = await TaskApi.getTaskDetailPage({ taskId, pageNo: 1, pageSize: 100 })
taskDetailList.value = data.list || []
} catch (error) {
console.error('Failed to fetch task details:', error)
taskDetailList.value = []
} finally {
loading.value = false
}
}
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.taskId,
(val: number) => {
if (!val) {
taskDetailList.value = []
return
}
getTaskDetailList(val)
},
{ immediate: true, deep: true }
)
/** 打开产品信息对话框 */
const openProductInfo = (row: any) => {
if (row?.productId) {
productInfoDialogRef.value?.open(row.productId)
}
}
</script>
<style scoped>
.basic-info-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.task-info-section {
padding: 16px;
background: #f5f7fa;
border-radius: 4px;
}
.section-title {
margin: 0 0 16px 0;
font-size: 14px;
font-weight: 600;
color: #333;
}
</style>

@ -0,0 +1,112 @@
<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">
<template #default="scope">
<dict-tag :type="DICT_TYPE.MES_QUALITY_TYPE" :value="scope.row.type" />
</template>
</el-table-column>
<el-table-column label="方案名称" align="center" prop="schemaName" width="150px" />
<el-table-column label="状态" align="center" prop="status" width="100px">
<template #default="scope">
<dict-tag type="status" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="结果" align="center" prop="result" width="100px">
<template #default="scope">
<el-tag :type="scope.row.result === '通过' ? 'success' : 'danger'">
{{ scope.row.resultName || scope.row.result || '-' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="执行人" align="center" prop="executorName" width="100px" />
<el-table-column label="执行时间" align="center" prop="executeTime" width="180px">
<template #default="scope">
{{ formatDateTime(scope.row.executeTime) }}
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180px">
<template #default="scope">
{{ formatDateTime(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" />
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</div>
</template>
<script setup lang="ts">
import dayjs from 'dayjs'
import { DICT_TYPE } from '@/utils/dict'
import { ZjTaskApi } from '@/api/mes/zjtask'
defineOptions({ name: 'ProductionReportQualityInfo' })
const props = defineProps<{
taskId?: number
}>()
const loading = ref(false)
const qualityList = ref<any[]>([])
const total = ref(0)
const queryParams = reactive({
ticket: undefined as number | undefined,
pageNo: 1,
pageSize: 10
})
const getList = async () => {
if (!queryParams.ticket) return
loading.value = true
try {
const data = await ZjTaskApi.getZjTaskPageByTicket(queryParams)
qualityList.value = data?.list || []
total.value = data?.total || 0
} catch (error) {
console.error('Failed to fetch quality info:', error)
qualityList.value = []
total.value = 0
} finally {
loading.value = false
}
}
/** 监听任务ID变化 */
watch(
() => props.taskId,
(val: number | undefined) => {
if (!val) {
qualityList.value = []
return
}
queryParams.ticket = val as number
queryParams.pageNo = 1
getList()
},
{ immediate: true }
)
/** 格式化日期时间 */
const formatDateTime = (value: string | Date | null) => {
if (!value) return '-'
return dayjs(value).format('YYYY-MM-DD HH:mm:ss')
}
</script>
<style scoped>
.section-title {
margin: 0 0 16px 0;
font-size: 14px;
font-weight: 600;
color: #333;
}
</style>

@ -0,0 +1,118 @@
<template>
<div class="related-plan-container">
<!-- 加载中状态 -->
<div v-if="loading" class="loading-wrapper">
<el-skeleton :rows="3" animated />
</div>
<!-- 空状态 -->
<el-empty v-else-if="planList.length === 0" description="暂无关联计划" />
<!-- 卡片列表 -->
<div v-else class="card-list">
<ProductionPlanCard
v-for="plan in planList"
:key="plan.id"
:plan="plan"
/>
</div>
<!-- 分页 -->
<Pagination
v-if="total > 0"
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</div>
</template>
<script setup lang="ts">
import { PlanApi, type PlanVO } from '@/api/mes/plan'
import ProductionPlanCard from './ProductionPlanCard.vue'
defineOptions({ name: 'ProductionReportRelatedPlan' })
const props = defineProps<{
taskId?: number
}>()
const loading = ref(false)
const planList = ref<PlanVO[]>([])
const total = ref(0)
const queryParams = reactive({
taskId: undefined as number | undefined,
pageNo: 1,
pageSize: 10
})
//
const getList = async () => {
if (!queryParams.taskId) return
loading.value = true
try {
const data = await PlanApi.getPlanPageByTask({
taskId: queryParams.taskId,
pageNo: queryParams.pageNo,
pageSize: queryParams.pageSize
})
planList.value = data?.list || []
total.value = data?.total || 0
} catch (error) {
console.error('Failed to fetch plan list:', error)
planList.value = []
total.value = 0
} finally {
loading.value = false
}
}
// ID
watch(
() => props.taskId,
(val: number | undefined) => {
if (!val) {
planList.value = []
total.value = 0
return
}
queryParams.taskId = val as number
queryParams.pageNo = 1
getList()
},
{ immediate: true }
)
</script>
<style scoped>
.section-title {
margin: 0 0 16px 0;
font-size: 14px;
font-weight: 600;
color: #333;
}
.loading-wrapper {
background: white;
padding: 16px;
border-radius: 4px;
margin-bottom: 16px;
}
.card-list {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

@ -0,0 +1,285 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="auto"
label-position="left"
>
<el-form-item label="任务单编码" prop="code">
<el-input
v-model="queryParams.code"
placeholder="请输入任务单编码"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="下达日期" 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="结束日期"
: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-date-picker
v-model="queryParams.deliveryDate"
value-format="YYYY-MM-DD HH:mm:ss"
@change="handleQuery"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
: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-input
v-model="queryParams.remark"
placeholder="请输入备注"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="创建时间" 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="结束日期"
: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" /> 搜索
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" /> 重置
</el-button>
<el-button type="success" plain @click="handleExport" :loading="exportLoading">
<Icon icon="ep:download" class="mr-5px" /> 导出
</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-tabs>
<el-table
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
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="交货日期"
align="center"
prop="deliveryDate"
:formatter="deliveryDateFormatter"
sortable
/>
<el-table-column label="状态" 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">
<template #default="scope">
<el-tag :type="scope.row.isScheduled ? 'success' : 'info'">{{ scope.row.isScheduled ? '是' : '否' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="生产进度" align="center" min-width="180px">
<template #default="scope">
<div class="production-progress-cell">
<el-tooltip :content="getProductionProgressPercent(scope.row) + '%'" placement="top">
<el-progress
:percentage="getProductionProgressPercent(scope.row)"
:show-text="false"
:stroke-width="12"
class="production-progress-bar"
/>
</el-tooltip>
</div>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" />
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 详情Tabs -->
<ContentWrap v-if="currentRow && currentRow.id">
<el-tabs v-model="activeTabName">
<!-- 基础信息 -->
<el-tab-pane label="基础信息" name="basicInfo">
<ProductionReportBasicInfo :task-id="currentRow.id" :task-delivery-date="currentRow.deliveryDate" />
</el-tab-pane>
<!-- 关联计划 -->
<el-tab-pane label="关联计划" name="relatedPlan">
<ProductionReportRelatedPlan :task-id="currentRow.id" />
</el-tab-pane>
<!-- 质检信息 -->
<el-tab-pane label="质检信息" name="qualityInfo">
<ProductionReportQualityInfo :task-id="currentRow.id" />
</el-tab-pane>
<!-- 报工信息 -->
<el-tab-pane label="报工信息" name="baogongInfo">
<ProductionReportBaogongInfo :task-id="currentRow.id" />
</el-tab-pane>
</el-tabs>
</ContentWrap>
</template>
<script setup lang="ts">
import { DICT_TYPE } from '@/utils/dict'
import { dateFormatter2 } from '@/utils/formatTime'
import download from '@/utils/download'
import { TaskApi, TaskVO } from '@/api/mes/task'
import ProductionReportBasicInfo from './components/ProductionReportBasicInfo.vue'
import ProductionReportRelatedPlan from './components/ProductionReportRelatedPlan.vue'
import ProductionReportQualityInfo from './components/ProductionReportQualityInfo.vue'
import ProductionReportBaogongInfo from './components/ProductionReportBaogongInfo.vue'
/** 生产报表 列表 */
defineOptions({ name: 'ProductionReport' })
const message = useMessage() //
const loading = ref(true) //
const list = ref<TaskVO[]>([]) //
const total = ref(0) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
code: undefined,
orderDate: [],
deliveryDate: [],
status: undefined,
remark: undefined,
createTime: []
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
const activeTabName = ref('basicInfo') // tab
const activeStatusTab = ref('') // tab
const deliveryDateFormatter = (_row: any, _column: any, value: any) => {
if (value) return dateFormatter2(_row, _column, value)
return dateFormatter2(_row, _column, _row?.finishDate)
}
const getProductionProgressPercent = (row: any) => {
const storedPlanNumber = Number(row?.storedPlanNumber ?? 0)
const totalNumber = Number(row?.totalNumber ?? row?.number ?? 0)
if (!Number.isFinite(storedPlanNumber) || !Number.isFinite(totalNumber) || totalNumber <= 0) return 0
const rawPercent = (storedPlanNumber / totalNumber) * 100
return Math.max(0, Math.min(100, Number(rawPercent.toFixed(2))))
}
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await TaskApi.getPlanTaskPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
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')
} catch {
} finally {
exportLoading.value = false
}
}
/** 选中行操作 */
const currentRow = ref<TaskVO>({} as TaskVO) //
const handleCurrentChange = (row: TaskVO | null) => {
if (row) {
currentRow.value = row
activeTabName.value = 'basicInfo'
}
}
/** tab 切换 */
const handleTabClick = (tab: any) => {
queryParams.status = tab.paneName === '' ? undefined : tab.paneName
currentRow.value = {} as TaskVO
queryParams.pageNo = 1
getList()
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>
<style scoped>
.production-progress-cell {
display: flex;
align-items: center;
justify-content: center;
}
.production-progress-bar {
width: 180px;
}
</style>
Loading…
Cancel
Save