mes-vue/src/views/biz/orderProcessPlan/components/ProcessCard.vue
shaoleiliu-netizen123 26bfafc358 提交代码
2026-08-14 16:38:45 +08:00

480 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="process-card-wrap">
<div class="process-card" :class="themeClass">
<div class="card-head">
<div class="card-title-block">
<span class="card-title">
<em v-if="process.operNumber != null" class="card-oper">{{ process.operNumber }}</em>
{{ process.processName || '-' }}
</span>
<span v-if="materialCode" class="card-material">{{ materialCode }}</span>
</div>
<n-tag size="small" :bordered="false" :type="statusTag.type">{{ statusTag.label }}</n-tag>
</div>
<div class="card-progress">
<n-progress
type="circle"
:percentage="progressPct"
:stroke-width="10"
:color="progressColor"
:rail-color="railColor"
style="width: 88px"
>
<span class="progress-text">{{ progressPct }}%</span>
</n-progress>
</div>
<div class="card-stats">
<div class="stat-row">
<span class="stat-label">完成</span>
<span class="stat-value">{{ reportedQuantity }} / {{ totalQty }}</span>
</div>
<div class="stat-row">
<span class="stat-label">合格 / 不合格</span>
<span class="stat-value">
<em class="ok">{{ completedQty }}</em>
<span class="sep">/</span>
<em class="bad">{{ scrapQty }}</em>
</span>
</div>
<div class="stat-row">
<span class="stat-label">合格率</span>
<span class="stat-value highlight">{{ passRate }}%</span>
</div>
</div>
<div class="card-flow">
<div class="flow-item">
<span>转入</span>
<strong>{{ process.transferInQty ?? 0 }}</strong>
</div>
<div class="flow-item">
<span>剩余物料</span>
<strong>{{ leftoverMaterials }}</strong>
</div>
<div class="flow-item">
<span>转出</span>
<strong>{{ process.transferOutQty ?? 0 }}</strong>
</div>
</div>
<div class="card-meta">
<div class="meta-line"><span>生产订单</span><em>{{ orderCode || '-' }}</em></div>
<div class="meta-line"><span>工单号</span><em>{{ process.workOrderCode || '-' }}</em></div>
<div class="meta-line"><span>人员</span><em>{{ personnel }}</em></div>
<div class="meta-line"><span>车间</span><em>{{ workshopText }}</em></div>
<div class="meta-line"><span>工位</span><em>{{ process.sectionName || '-' }}</em></div>
<div class="meta-line"><span>设备</span><em>{{ deviceText }}</em></div>
<div class="meta-line"><span>计划时间</span><em>{{ planTimeText }}</em></div>
<div class="meta-line"><span>实际开始</span><em>{{ process.actualStartTime || '-' }}</em></div>
</div>
<div class="card-actions">
<n-button
v-if="canAssign"
size="tiny"
type="warning"
ghost
@click="emit('assign', process)"
>派工</n-button>
<n-button
v-if="canSplit"
size="tiny"
type="primary"
ghost
@click="emit('split', process)"
>拆单</n-button>
<n-button
v-if="canEdit"
size="tiny"
ghost
@click="emit('edit', process)"
>编辑</n-button>
<n-button size="tiny" type="info" ghost @click="emit('viewModel', process)">模型</n-button>
<n-button
v-if="canOutsource"
size="tiny"
type="warning"
ghost
@click="emit('outsource', process)"
>委外</n-button>
<n-button
size="tiny"
type="info"
ghost
@click="emit('submitDetail', process)"
>汇报</n-button>
<n-button
:disabled="refillDisAbled"
size="tiny"
type="info"
ghost
@click="emit('handleLeftoverMaterial', process)"
>补料</n-button>
<n-button
size="tiny"
:type="process.processStatus === 4 ? 'success' : 'default'"
:ghost="process.processStatus !== 4"
disabled
>{{ process.processStatus === 4 ? '已完成' : '进行中' }}</n-button>
</div>
</div>
<div v-if="!isLast" class="card-chevron">
<n-icon size="28" color="#c0c4cc"><ChevronForwardOutline /></n-icon>
</div>
</div>
</template>
<script setup lang="ts">
import {computed, onMounted, ref} from 'vue'
import { NButton, NIcon, NProgress, NTag } from 'naive-ui'
import { ChevronForwardOutline } from '@vicons/ionicons5'
import { PROCESS_STATUS_MAP, type ProcessPlanItemVO } from '@/api/orderProcessPlan'
import { refillRecordApi } from '@/api/refillRecord.ts'
const props = defineProps<{
process: ProcessPlanItemVO
/** 物料编码订单头 */
materialCode?: string
/** 生产订单编号订单头 orderCode */
orderCode?: string
/** 订单计划开始订单头 beginTime工序无计划时间时回退 */
orderBeginTime?: string
/** 订单计划结束订单头 endTime */
orderEndTime?: string
/** 生产车间名称订单头 workshopName */
workshopName?: string
isLast?: boolean
canEdit?: boolean
canAssign?: boolean
canSplit?: boolean
canOutsource?: boolean
canSubmitDetail?: boolean
}>()
const emit = defineEmits<{
edit: [process: ProcessPlanItemVO]
assign: [process: ProcessPlanItemVO]
split: [process: ProcessPlanItemVO]
viewModel: [process: ProcessPlanItemVO]
outsource: [process: ProcessPlanItemVO]
submitDetail: [process: ProcessPlanItemVO]
handleLeftoverMaterial: [process: ProcessPlanItemVO]
}>()
const reportedQuantity = computed(() => props.process.reportedQuantity ?? 0)
const totalQty = computed(() => props.process.quantity ?? 0)
const completedQty = computed(() => props.process.completedQty ?? 0)
const scrapQty = computed(() => props.process.scrapQty ?? 0)
const qualifiedQty = computed(() => Math.max(0, completedQty.value - scrapQty.value))
const leftoverMaterials = computed(() => props.process.leftoverMaterials ?? 0) //剩余物料
const progressPct = computed(() => {
if (!totalQty.value) return 0
return Math.min(100, Math.round((completedQty.value / totalQty.value) * 100))
})
const passRate = computed(() => {
if (!completedQty.value) return progressPct.value
return Math.round((qualifiedQty.value / completedQty.value) * 100)
})
// const remainQty = computed(() => Math.max(0, totalQty.value - completedQty.value))
const statusTag = computed(() => {
const s = props.process.processStatus
if (s == null) return { label: '-', type: 'default' as const }
const m = PROCESS_STATUS_MAP[s]
return m ? { label: m.label, type: m.type } : { label: String(s), type: 'default' as const }
})
const themeClass = computed(() => {
const s = props.process.processStatus
if (s === 4) return 'theme-done'
if (s != null && s >= 2) return 'theme-active'
return 'theme-pending'
})
const progressColor = computed(() => {
if (themeClass.value === 'theme-done') return '#18a058'
if (themeClass.value === 'theme-active') return '#2080f0'
return '#d9d9d9'
})
const railColor = computed(() => {
if (themeClass.value === 'theme-done') return 'rgba(24,160,88,0.15)'
if (themeClass.value === 'theme-active') return 'rgba(32,128,240,0.12)'
return '#f0f0f0'
})
const personnel = computed(() => {
if (props.process.assignByName) return props.process.assignByName
const list = props.process.assignWorkList
if (list?.length) {
const names = list
.map((a: any) => a.userName || a.userId)
.filter(Boolean)
if (names.length) return names.join('、')
return `${list.length} 人派工`
}
return '-'
})
const workshopText = computed(() => {
return props.process.workshopName || props.workshopName || '-'
})
const deviceText = computed(() => {
const list = props.process.assignWorkList as any[] | undefined
if (!list?.length) return '-'
const devices = list
.map((a) => a.deviceName || a.deviceId)
.filter(Boolean)
return devices.length ? [...new Set(devices)].join('、') : '-'
})
const planTimeText = computed(() => {
// 工序计划时间优先;没有则回退订单头 beginTime/endTime
const start = props.process.planStartTime || props.process.beginTime || props.orderBeginTime
const end = props.process.planFinishTime || props.process.endTime || props.orderEndTime
if (!start && !end) return '-'
if (start && end) {
const startText = start.length >= 16 ? start.slice(0, 16) : start
const endText = end.length >= 16 ? end.slice(11, 16) : end
return `${startText} ~ ${endText}`
}
return start || end || '-'
})
const refillDisAbled = ref<Boolean>(false);
async function geRefillRecord() {
//查询补料记录
const res = await refillRecordApi.getRefillNumByProcessId(props.process.planId);
if (res >= props.process.scrapQty){
refillDisAbled.value = true
}
refillDisAbled.value = false
}
onMounted(()=>{
geRefillRecord()
})
</script>
<style scoped>
.process-card-wrap {
display: flex;
align-items: center;
flex-shrink: 0;
}
.process-card {
width: 220px;
border-radius: 8px;
border: 1px solid #e8e8e8;
background: #fff;
overflow: hidden;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
transition: box-shadow 0.2s, transform 0.2s;
}
.process-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
transform: translateY(-1px);
}
.theme-done {
border-color: rgba(24, 160, 88, 0.35);
}
.theme-done .card-head {
background: linear-gradient(135deg, #18a058 0%, #36ad6a 100%);
color: #fff;
}
.theme-done .card-head :deep(.n-tag) {
background: rgba(255, 255, 255, 0.25) !important;
color: #fff !important;
}
.theme-active .card-head {
background: linear-gradient(135deg, #2080f0 0%, #4098fc 100%);
color: #fff;
}
.theme-active .card-head :deep(.n-tag) {
background: rgba(255, 255, 255, 0.25) !important;
color: #fff !important;
}
.theme-pending .card-head {
background: #f5f5f5;
color: #595959;
}
.card-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 8px 10px;
min-height: 36px;
}
.card-title {
font-size: 13px;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-oper {
font-style: normal;
font-weight: 700;
margin-right: 4px;
opacity: 0.95;
}
.card-title-block {
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.card-material {
font-size: 11px;
font-weight: 500;
opacity: 0.85;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-progress {
display: flex;
justify-content: center;
padding: 12px 0 8px;
}
.progress-text {
font-size: 14px;
font-weight: 700;
color: #333;
}
.card-stats {
padding: 0 12px 8px;
display: flex;
flex-direction: column;
gap: 4px;
}
.stat-row {
display: flex;
justify-content: space-between;
font-size: 12px;
}
.stat-label {
color: #8c8c8c;
}
.stat-value {
color: #333;
font-weight: 500;
}
.stat-value .ok {
color: #18a058;
font-style: normal;
}
.stat-value .bad {
color: #d03050;
font-style: normal;
}
.stat-value .sep {
margin: 0 2px;
color: #bfbfbf;
}
.stat-value.highlight {
color: #18a058;
}
.card-flow {
display: flex;
margin: 0 12px 8px;
padding: 8px;
background: #fafafa;
border-radius: 6px;
gap: 8px;
}
.flow-item {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
font-size: 11px;
color: #8c8c8c;
}
.flow-item strong {
font-size: 14px;
color: #333;
}
.card-meta {
padding: 0 12px 8px;
display: flex;
flex-direction: column;
gap: 3px;
}
.meta-line {
display: flex;
justify-content: space-between;
font-size: 11px;
gap: 8px;
}
.meta-line span {
color: #8c8c8c;
flex-shrink: 0;
}
.meta-line em {
font-style: normal;
color: #595959;
text-align: right;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-actions {
display: flex;
flex-wrap: wrap;
gap: 4px;
padding: 8px 10px 10px;
border-top: 1px solid #f0f0f0;
}
.card-chevron {
padding: 0 4px;
display: flex;
align-items: center;
opacity: 0.7;
}
</style>