315 lines
9.6 KiB
Vue
315 lines
9.6 KiB
Vue
<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>
|