mes-vue/src/views/biz/orderProcessPlan/components/WorkOrderPrintPreview.vue
2026-08-27 10:58:25 +08:00

315 lines
9.6 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<n-modal
:show="show"
preset="card"
title="工单打印预览"
:mask-closable="false"
:style="{ width: 'min(980px, 96vw)' }"
@update:show="emit('update:show', $event)"
>
<n-spin :show="loading">
<div class="wo-preview-stage">
<div v-if="!order && !loading" class="wo-preview-empty">暂无工单数据</div>
<div v-else ref="sheetsRef" class="wo-preview-pages">
<section
v-for="page in pages"
:key="page.index"
class="wo-a4-sheet"
>
<div class="wo-a4-frame">
<header class="wo-a4-head" :class="{ 'is-cont': page.index > 1 }">
<div>
<h1 class="wo-a4-title">
{{ page.index === 1 ? '工序计划工单' : '工序计划工单(续)' }}
</h1>
<div class="wo-a4-id">工单号 <b>{{ workOrderCode || '未赋码' }}</b></div>
</div>
<div class="wo-a4-qr">
<div class="wo-a4-qr-box">
<img v-if="qrDataUrl" :src="qrDataUrl" alt="工单号二维码" />
<div v-else class="wo-a4-qr-empty">无码</div>
</div>
<div class="wo-a4-qr-cap">扫码</div>
</div>
</header>
<dl v-if="page.index === 1" class="wo-a4-meta">
<div class="wo-a4-meta-item">
<dt>生产令号</dt>
<dd>{{ text(order?.mainCode) }}</dd>
</div>
<div class="wo-a4-meta-item">
<dt>生产订单</dt>
<dd>{{ text(order?.orderCode) }}</dd>
</div>
<div class="wo-a4-meta-item">
<dt>计划数量</dt>
<dd>{{ order?.quantity ?? '-' }}</dd>
</div>
<div class="wo-a4-meta-item">
<dt>生产车间</dt>
<dd>{{ text(order?.workshopName) }}</dd>
</div>
<div class="wo-a4-meta-item is-wide">
<dt>物料</dt>
<dd>{{ materialText }}</dd>
</div>
<div class="wo-a4-meta-item">
<dt>计划开工</dt>
<dd>{{ formatTime(order?.beginTime) }}</dd>
</div>
<div class="wo-a4-meta-item">
<dt>计划完工</dt>
<dd>{{ formatTime(order?.endTime) }}</dd>
</div>
</dl>
<div class="wo-a4-table-wrap">
<table class="wo-a4-table">
<colgroup>
<col class="col-no" />
<col class="col-name" />
<col class="col-time" />
<col class="col-time" />
<col class="col-qty" />
</colgroup>
<thead>
<tr>
<th>工序号</th>
<th>工序名称</th>
<th>计划开始</th>
<th>计划结束</th>
<th>数量</th>
</tr>
</thead>
<tbody>
<tr
v-for="(row, idx) in page.rows"
:key="row.planId ?? `${page.index}-${idx}`"
>
<td>{{ row.operNumber ?? '-' }}</td>
<td class="is-left">{{ text(row.processName) }}</td>
<td>{{ formatTime(row.planStartTime || row.beginTime) }}</td>
<td>{{ formatTime(row.planFinishTime || row.endTime) }}</td>
<td>{{ row.quantity ?? '-' }}</td>
</tr>
</tbody>
</table>
<div v-if="!page.rows.length" class="wo-a4-empty">本工单暂无工序计划</div>
</div>
<footer v-if="page.index === pages.length" class="wo-a4-foot">
<div class="wo-a4-sign">
<div class="wo-a4-sign-cell">
<b>操作工</b>
<div class="wo-a4-sign-line" />
</div>
<div class="wo-a4-sign-cell">
<b>质检</b>
<div class="wo-a4-sign-line" />
</div>
<div class="wo-a4-sign-cell">
<b>班组确认</b>
<div class="wo-a4-sign-line" />
</div>
</div>
<div class="wo-a4-note">
<span>打印人 {{ printerName }} {{ printedAt }}</span>
<span>{{ page.index }} / {{ page.total }}</span>
</div>
</footer>
<div v-else class="wo-a4-note">
<span>续页 {{ workOrderCode || '-' }}</span>
<span>{{ page.index }} / {{ page.total }}</span>
</div>
</div>
</section>
</div>
</div>
</n-spin>
<template #footer>
<n-space justify="end">
<n-button @click="emit('update:show', false)">关闭</n-button>
<n-button type="primary" :disabled="!order || loading" @click="handlePrint">
打印
</n-button>
</n-space>
</template>
</n-modal>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { NButton, NModal, NSpace, NSpin, useMessage } from 'naive-ui'
import QRCode from 'qrcode'
import printCss from './workOrderPrint.css?inline'
import './workOrderPrint.css'
import {
type OrderProcessPlanVO,
type ProcessPlanItemVO,
} from '@/api/orderProcessPlan'
import { useUserStore } from '@/stores/user'
const FIRST_PAGE_ROWS = 16
const NEXT_PAGE_ROWS = 20
const props = defineProps<{
show: boolean
order: OrderProcessPlanVO | null
loading?: boolean
}>()
const emit = defineEmits<{
'update:show': [value: boolean]
}>()
const message = useMessage()
const userStore = useUserStore()
const sheetsRef = ref<HTMLElement | null>(null)
const qrDataUrl = ref('')
const printedAt = ref('')
const workOrderCode = computed(() => props.order?.workOrderCode?.trim() || '')
const printerName = computed(() => userStore.nickname || '-')
const materialText = computed(() => {
const name = props.order?.materialName || '-'
const code = props.order?.materialCode
return code ? `${name} ${code}` : name
})
const sortedProcesses = computed(() => {
return [...(props.order?.processList ?? [])].sort((a, b) => {
const oa = a.operNumber ?? 0
const ob = b.operNumber ?? 0
if (oa !== ob) return oa - ob
return (a.planId ?? 0) - (b.planId ?? 0)
})
})
const pages = computed(() => {
const list = sortedProcesses.value
if (!list.length) {
return [{ index: 1, total: 1, rows: [] as ProcessPlanItemVO[] }]
}
const chunks: ProcessPlanItemVO[][] = [list.slice(0, FIRST_PAGE_ROWS)]
for (let i = FIRST_PAGE_ROWS; i < list.length; i += NEXT_PAGE_ROWS) {
chunks.push(list.slice(i, i + NEXT_PAGE_ROWS))
}
return chunks.map((rows, i) => ({
index: i + 1,
total: chunks.length,
rows,
}))
})
function text(v?: string | null) {
return v && String(v).trim() ? String(v) : '-'
}
function formatTime(val?: string | null) {
if (!val) return '-'
return val.length > 16 ? val.slice(0, 16) : val
}
function nowText() {
const d = new Date()
const p = (n: number) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
}
watch(
() => [props.show, workOrderCode.value] as const,
async ([visible, code]) => {
if (!visible) return
printedAt.value = nowText()
if (!code) {
qrDataUrl.value = ''
return
}
try {
qrDataUrl.value = await QRCode.toDataURL(code, {
errorCorrectionLevel: 'M',
margin: 1,
width: 320,
color: { dark: '#000000', light: '#ffffff' },
})
} catch {
qrDataUrl.value = ''
}
},
{ immediate: true },
)
function handlePrint() {
const html = sheetsRef.value?.innerHTML
if (!html) {
message.warning('没有可打印的内容')
return
}
const iframe = document.createElement('iframe')
iframe.setAttribute('aria-hidden', 'true')
iframe.style.cssText = 'position:fixed;right:0;bottom:0;width:0;height:0;border:0;'
document.body.appendChild(iframe)
const doc = iframe.contentDocument
if (!doc) {
iframe.remove()
message.error('无法打开打印预览')
return
}
const title = workOrderCode.value || '工序计划工单'
doc.open()
doc.write(`<!DOCTYPE html><html><head><meta charset="utf-8"><title>${title}</title><style>${printCss}html,body{margin:0;background:#fff}</style></head><body>${html}</body></html>`)
doc.close()
const run = () => {
iframe.contentWindow?.focus()
iframe.contentWindow?.print()
window.setTimeout(() => iframe.remove(), 800)
}
const imgs = Array.from(doc.images)
if (!imgs.length) {
run()
return
}
Promise.all(
imgs.map(
(img) =>
new Promise<void>((resolve) => {
if (img.complete) resolve()
else {
img.onload = () => resolve()
img.onerror = () => resolve()
}
}),
),
).then(run)
}
</script>
<style scoped>
.wo-preview-stage {
max-height: 72vh;
overflow: auto;
padding: 12px;
background: #e8e8e8;
}
.wo-preview-empty {
padding: 48px 0;
text-align: center;
color: #666;
}
.wo-preview-pages {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.wo-preview-pages :deep(.wo-a4-sheet) {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
</style>