| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646 |
- <template>
- <el-dialog
- :model-value="modelValue"
- title="套打流程单"
- width="1100px"
- top="6vh"
- class="print-overprint-dialog"
- destroy-on-close
- append-to-body
- @close="onClose"
- >
- <div class="print-layout">
- <div class="print-left">
- <el-form :model="form" label-width="92px" size="default">
- <el-form-item v-if="!lockTemplate" label="套打模板">
- <el-select v-model="form.template_id" filterable placeholder="选择本厂识版模板" style="width:100%">
- <el-option v-for="item in templates" :key="item.id" :label="item.name + labelOf(item)" :value="item.id" />
- </el-select>
- <p v-if="!templates.length" class="empty-hint">还没有可套打模板。请先到「本厂模板」上传流程单识版。</p>
- </el-form-item>
- <el-form-item v-if="!lockBiz" label="打印对象">
- <el-radio-group v-model="form.biz_type" @change="onBizTypeChange">
- <el-radio label="workorder">工单</el-radio>
- <el-radio label="product">产品</el-radio>
- </el-radio-group>
- </el-form-item>
- <el-form-item v-else label="打印对象">
- <span>{{ form.biz_type === 'product' ? '产品' : '工单' }}</span>
- </el-form-item>
- <el-form-item :label="form.biz_type === 'product' ? '产品编号' : '工单编号'">
- <el-select
- v-model="form.no"
- filterable
- allow-create
- default-first-option
- clearable
- :disabled="lockBiz"
- :placeholder="form.biz_type === 'product' ? '选择已有产品,或直接输入编号' : '选择已有工单,或直接输入编号'"
- style="width:100%"
- @change="onNoChange"
- >
- <el-option
- v-for="item in bizOptions"
- :key="item.id"
- :label="optionLabel(item)"
- :value="optionValue(item)"
- />
- </el-select>
- <p class="field-hint">{{ isExisting ? '已匹配到本厂记录,将带出保存工艺' : '未匹配记录,可直接填写信息并临时添加工艺后打印' }}</p>
- </el-form-item>
- <el-form-item label="名称">
- <el-input v-model="form.name" :placeholder="form.biz_type === 'product' ? '产品名称' : '工单名称'" />
- </el-form-item>
- <el-form-item label="总数量">
- <el-input v-model="form.qty" placeholder="本次套打的计划总件数" />
- </el-form-item>
- <el-form-item label="每版数量">
- <el-input v-model="form.sheet_qty" placeholder="一版 = 一张流程单,仅本次套打使用" />
- </el-form-item>
- <el-form-item label="预计版数">
- <span class="plate-num">{{ plateTotal || '—' }}</span>
- <span v-if="sheetHint" class="field-hint inline">{{ sheetHint }}</span>
- </el-form-item>
- <el-form-item label="打印版数">
- <div class="range-row">
- <el-input-number v-model="form.sheet_from" :min="1" :max="plateMax" :disabled="!plateTotal" />
- <span class="range-sep">至</span>
- <el-input-number v-model="form.sheet_to" :min="form.sheet_from || 1" :max="plateMax" :disabled="!plateTotal" />
- </div>
- </el-form-item>
- <el-form-item label="整套份数">
- <el-input-number v-model="form.copies" :min="1" />
- </el-form-item>
- <el-form-item v-if="isExisting" label="保存工艺" class="process-item">
- <div v-if="savedProcesses.length" class="process-box">
- <div class="process-toolbar">
- <el-button text type="primary" @click="checkAllProcesses">全选</el-button>
- <el-button text @click="checkedProcessKeys = []">清空</el-button>
- <span class="field-hint inline">只打印勾选的工艺</span>
- </div>
- <el-checkbox-group v-model="checkedProcessKeys" class="process-checks">
- <el-checkbox
- v-for="(row, index) in savedProcesses"
- :key="index"
- :label="String(index)"
- >
- {{ processLabel(row) }}
- </el-checkbox>
- </el-checkbox-group>
- </div>
- <p v-else class="field-hint">该{{ form.biz_type === 'product' ? '产品' : '工单' }}还没有保存工艺,本次套打不带工序。</p>
- </el-form-item>
- <el-form-item v-else label="打印工艺" class="process-item">
- <div class="process-box">
- <p class="field-hint" style="margin:0 0 8px">直接输入的工单/产品只需填写工艺名称,仅用于本次打印。</p>
- <ProcessEditor v-model="draftProcesses" name-only />
- </div>
- </el-form-item>
- </el-form>
- <div v-if="preview" class="preview">
- <el-alert
- :title="'将打印第 ' + preview.sheet_from + '–' + preview.sheet_to + ' 版,共 ' + preview.total_sheets + ' 张(总版数 ' + (preview.plate_total || preview.total_sheets) + ')'"
- type="success"
- :closable="false"
- />
- <p>工序:{{ (preview.processes || []).map(item => item.name).join('、') || '无' }}</p>
- <el-table :data="preview.sheets" size="small" max-height="160">
- <el-table-column prop="no_text" label="页码" width="100" />
- <el-table-column prop="qty_text" label="本张数量" />
- </el-table>
- </div>
- </div>
- <div class="print-right">
- <div class="tpl-panel">
- <h4>{{ selectedTemplate ? selectedTemplate.name : '模板样式' }}</h4>
- <div class="cover" :style="coverBox(selectedTemplate)">
- <img v-if="selectedCover" class="cover-img" :src="selectedCover" alt="套打底图" />
- <div v-else class="cover-empty">{{ form.template_id ? '暂无底图' : '请先选择模板' }}</div>
- <div v-if="selectedCover" class="cells">
- <div
- v-for="cell in selectedCells"
- :key="cell.id || cell.key"
- class="cell"
- :style="cellStyle(cell)"
- :title="cell.label || cell.key || '空格'"
- />
- </div>
- </div>
- <p class="tpl-hint">右侧对照格子。预览会自动选 PDF 打印机以便按 A5 出纸;默认标签机最宽约 110mm,套不准也看不到字。打印时在预览里改选车间打印机。</p>
- </div>
- </div>
- </div>
- <template #footer>
- <InstallLodopButton type="warning" />
- <el-button @click="onClose">取消</el-button>
- <el-button @click="onPrepare" :loading="preparing">预览拆张</el-button>
- <el-button type="primary" @click="onConfirm" :loading="saving">确认套打</el-button>
- </template>
- </el-dialog>
- </template>
- <script setup>
- import { computed, reactive, ref, watch } from 'vue'
- import { ElMessage } from 'element-plus'
- import ProcessEditor from '@/components/ProcessEditor.vue'
- import InstallLodopButton from '@/components/InstallLodopButton.vue'
- import {
- printConfirm,
- printPrepare,
- productInfo,
- productList,
- templateList,
- workorderInfo,
- workorderList,
- } from '@/api/ps'
- import { handleLodopError, runLodop } from '@/utils/lodop'
- import { fileUrl } from '@/utils/file'
- import { sheetCount, sheetCountText } from '@/utils/sheet'
- const props = defineProps({
- modelValue: { type: Boolean, default: false },
- bizType: { type: String, default: '' },
- bizId: { type: [Number, String], default: '' },
- templateId: { type: [Number, String], default: '' },
- no: { type: String, default: '' },
- name: { type: String, default: '' },
- })
- const syncing = ref(false)
- const emit = defineEmits(['update:modelValue', 'done'])
- const templates = ref([])
- const workorders = ref([])
- const products = ref([])
- const preparing = ref(false)
- const saving = ref(false)
- const preview = ref(null)
- const savedProcesses = ref([])
- const checkedProcessKeys = ref([])
- const draftProcesses = ref([])
- const form = reactive({
- template_id: '',
- biz_type: 'workorder',
- biz_id: '',
- no: '',
- name: '',
- sheet_qty: '',
- qty: '',
- sheet_from: 1,
- sheet_to: 1,
- copies: 1,
- })
- const lockBiz = computed(() => !!props.bizId)
- const lockTemplate = computed(() => !!props.templateId)
- const isExisting = computed(() => Number(form.biz_id) > 0)
- const bizOptions = computed(() => (form.biz_type === 'product' ? products.value : workorders.value))
- const plateTotal = computed(() => sheetCount(form.qty, form.sheet_qty))
- const plateMax = computed(() => Math.max(1, plateTotal.value || 1))
- const sheetHint = computed(() => sheetCountText(form.qty, form.sheet_qty))
- const selectedTemplate = computed(() =>
- templates.value.find((item) => Number(item.id) === Number(form.template_id)) || null
- )
- const selectedCover = computed(() => {
- const row = selectedTemplate.value
- if (!row) {
- return ''
- }
- return fileUrl(row.cover_url || row.cover || (row.schema && (row.schema.background || row.schema.file_url)) || '')
- })
- const selectedCells = computed(() => cellsOf(selectedTemplate.value))
- function optionValue(item) {
- if (form.biz_type === 'workorder') {
- return item.no || ''
- }
- return item.code || item.name || ''
- }
- function pickName(row, fallback = '') {
- if (!row) {
- return String(fallback || '').trim()
- }
- return String(
- row.product_name || (row.product && row.product.name) || row.name || fallback || ''
- ).trim()
- }
- function optionLabel(item) {
- if (form.biz_type === 'workorder') {
- const name = pickName(item)
- return name ? (item.no + ' / ' + name) : (item.no || '')
- }
- return (item.code || '无编号') + ' / ' + (item.name || '')
- }
- function processLabel(row) {
- const seq = row.seq ? row.seq + '. ' : ''
- const code = row.code ? '(' + row.code + ')' : ''
- return seq + (row.name || '未命名工序') + code
- }
- function cellsOf(row) {
- const schema = row && row.schema && typeof row.schema === 'object' ? row.schema : {}
- const cells = Array.isArray(schema.cells) ? schema.cells : []
- if (cells.length) {
- return cells
- }
- const elements = Array.isArray(schema.elements) ? schema.elements : []
- return elements.filter((el) => el && (el.type === 'field' || el.role === 'fill' || el.key))
- }
- function coverBox(row) {
- const page = (row && row.schema && row.schema.page) || {}
- const w = Number(page.w) || 210
- const h = Number(page.h) || 297
- return { paddingTop: ((h / w) * 100) + '%' }
- }
- function cellStyle(cell) {
- return {
- left: (cell.x || 0) + '%',
- top: (cell.y || 0) + '%',
- width: Math.max(1.2, cell.w || 4) + '%',
- height: Math.max(1.2, cell.h || 3) + '%',
- }
- }
- function labelOf(item) {
- if (item.is_system == 1) {
- return '(官方)'
- }
- if (item.license_type === 'market_single') {
- return '(单次)'
- }
- if (item.license_type === 'market_buyout') {
- return '(买断)'
- }
- return ''
- }
- function applySavedProcesses(list) {
- savedProcesses.value = Array.isArray(list) ? list.slice() : []
- checkedProcessKeys.value = savedProcesses.value.map((_, index) => String(index))
- draftProcesses.value = []
- }
- function findRowByNo(no) {
- const key = String(no || '').trim()
- if (!key) {
- return null
- }
- if (form.biz_type === 'workorder') {
- return workorders.value.find((item) => String(item.no) === key) || null
- }
- return products.value.find((item) => String(item.code) === key || String(item.name) === key) || null
- }
- function syncPlateRange(prevTotal) {
- const n = plateTotal.value
- if (!(n > 0)) {
- form.sheet_from = 1
- form.sheet_to = 1
- return
- }
- if (form.sheet_from < 1 || form.sheet_from > n) {
- form.sheet_from = 1
- }
- const stretchEnd = !form.sheet_to || form.sheet_to > n || !prevTotal || form.sheet_to === prevTotal
- if (stretchEnd) {
- form.sheet_to = n
- }
- if (form.sheet_to < form.sheet_from) {
- form.sheet_to = n
- }
- }
- function checkAllProcesses() {
- checkedProcessKeys.value = savedProcesses.value.map((_, index) => String(index))
- }
- function onBizTypeChange() {
- form.biz_id = ''
- form.no = ''
- form.name = ''
- form.qty = ''
- form.sheet_qty = ''
- form.sheet_from = 1
- form.sheet_to = 1
- savedProcesses.value = []
- checkedProcessKeys.value = []
- draftProcesses.value = []
- preview.value = null
- }
- async function onNoChange(val) {
- if (syncing.value || lockBiz.value) {
- return
- }
- preview.value = null
- const key = String(val || '').trim()
- if (!key) {
- form.biz_id = ''
- savedProcesses.value = []
- checkedProcessKeys.value = []
- return
- }
- const row = findRowByNo(key)
- if (row) {
- await loadBizDetail(row.id)
- return
- }
- form.biz_id = ''
- savedProcesses.value = []
- checkedProcessKeys.value = []
- }
- async function loadBizDetail(id) {
- if (!id) {
- return
- }
- syncing.value = true
- try {
- if (form.biz_type === 'workorder') {
- const row = await workorderInfo(id)
- form.biz_id = row.id
- form.no = row.no || form.no || ''
- form.name = pickName(row, form.name)
- const procs = row.processes && row.processes.length
- ? row.processes
- : ((row.product && row.product.processes) || [])
- applySavedProcesses(procs)
- } else {
- const row = await productInfo(id)
- form.biz_id = row.id
- form.no = row.code || form.no || ''
- form.name = pickName(row, form.name)
- applySavedProcesses(row.processes || [])
- }
- syncPlateRange()
- } finally {
- syncing.value = false
- }
- }
- function selectedProcesses() {
- if (isExisting.value) {
- return savedProcesses.value.filter((_, index) => checkedProcessKeys.value.includes(String(index)))
- }
- return (draftProcesses.value || []).filter((row) => String(row.name || '').trim())
- }
- function payload() {
- return {
- template_id: form.template_id,
- biz_type: form.biz_type,
- biz_id: Number(form.biz_id) || 0,
- no: form.no,
- code: form.biz_type === 'product' ? form.no : '',
- name: form.name,
- sheet_qty: form.sheet_qty,
- qty: form.qty,
- sheet_from: form.sheet_from,
- sheet_to: form.sheet_to,
- copies: form.copies,
- processes: selectedProcesses(),
- }
- }
- function validate() {
- if (!form.template_id) {
- ElMessage.warning('请选择套打模板')
- return false
- }
- if (!String(form.no || '').trim() && !String(form.name || '').trim()) {
- ElMessage.warning('请填写工单或产品编号、名称')
- return false
- }
- if (!(Number(form.qty) > 0) || !(Number(form.sheet_qty) > 0)) {
- ElMessage.warning('请填写总数量和每版数量')
- return false
- }
- if (form.sheet_from < 1 || form.sheet_to < form.sheet_from || form.sheet_to > plateMax.value) {
- ElMessage.warning('请选择正确的打印起始、结束版数')
- return false
- }
- return true
- }
- async function onPrepare() {
- if (!validate()) {
- return
- }
- preparing.value = true
- try {
- preview.value = await printPrepare(payload())
- } finally {
- preparing.value = false
- }
- }
- async function onConfirm() {
- if (!validate()) {
- return
- }
- saving.value = true
- try {
- const data = await printConfirm(payload())
- ElMessage.success('已确认,流水号 ' + data.print_no + ',共 ' + data.total_sheets + ' 张')
- if (data.lodop) {
- try {
- await runLodop(data.lodop)
- } catch (e) {
- await handleLodopError(e)
- }
- }
- emit('done', data)
- onClose()
- } finally {
- saving.value = false
- }
- }
- function onClose() {
- emit('update:modelValue', false)
- }
- watch(
- () => props.modelValue,
- async (open) => {
- if (!open) {
- return
- }
- preview.value = null
- savedProcesses.value = []
- checkedProcessKeys.value = []
- draftProcesses.value = []
- syncing.value = true
- form.template_id = Number(props.templateId || 0) || ''
- form.biz_type = props.bizType || 'workorder'
- form.biz_id = Number(props.bizId || 0) || ''
- form.no = String(props.no || '').trim()
- form.name = String(props.name || '').trim()
- form.sheet_qty = ''
- form.qty = ''
- form.sheet_from = 1
- form.sheet_to = 1
- form.copies = 1
- try {
- const [t, w, p] = await Promise.all([
- templateList({ printable: 1, limit: 100 }),
- workorderList({ limit: 100 }),
- productList({ limit: 100 }),
- ])
- templates.value = t.rows || []
- workorders.value = w.rows || []
- products.value = p.rows || []
- if (form.biz_id) {
- const listed = (form.biz_type === 'product' ? products.value : workorders.value).find(
- (item) => Number(item.id) === Number(form.biz_id)
- )
- if (listed) {
- form.no = form.no || (form.biz_type === 'product' ? listed.code : listed.no) || ''
- form.name = form.name || pickName(listed)
- }
- await loadBizDetail(form.biz_id)
- }
- } finally {
- syncing.value = false
- }
- }
- )
- watch(plateTotal, (n, old) => {
- if (!props.modelValue) {
- return
- }
- syncPlateRange(old)
- })
- </script>
- <style scoped>
- .print-layout {
- display: flex;
- gap: 20px;
- align-items: stretch;
- min-height: 480px;
- }
- .print-left {
- flex: 1;
- min-width: 0;
- }
- .print-right {
- width: 380px;
- flex-shrink: 0;
- }
- .field-hint {
- margin: 6px 0 0;
- color: #7a8794;
- font-size: 12px;
- line-height: 1.4;
- }
- .field-hint.inline {
- margin: 0 0 0 8px;
- }
- .plate-num {
- font-size: 18px;
- font-weight: 600;
- color: #1d4f91;
- }
- .range-row {
- display: flex;
- align-items: center;
- gap: 8px;
- }
- .range-sep { color: #7a8794; }
- .process-item :deep(.el-form-item__content) { display: block; }
- .process-box {
- width: 100%;
- padding: 10px 12px;
- background: #f6f8fb;
- border-radius: 8px;
- }
- .process-toolbar {
- display: flex;
- align-items: center;
- gap: 4px;
- margin-bottom: 8px;
- }
- .process-checks {
- display: flex;
- flex-direction: column;
- gap: 4px;
- max-height: 180px;
- overflow: auto;
- }
- .preview { margin-top: 12px; }
- .preview p { color: #7a8794; margin: 8px 0; }
- .tpl-panel {
- position: sticky;
- top: 0;
- padding: 12px;
- background: #f4f6fb;
- border-radius: 10px;
- }
- .tpl-panel h4 {
- margin: 0 0 10px;
- font-size: 14px;
- font-weight: 600;
- }
- .cover {
- position: relative;
- border-radius: 8px;
- overflow: hidden;
- background: #fff;
- border: 1px solid #e5e9ef;
- }
- .cover-img,
- .cover-empty,
- .cells {
- position: absolute;
- inset: 0;
- width: 100%;
- height: 100%;
- }
- .cover-img { object-fit: fill; display: block; }
- .cover-empty {
- color: #8a97a6;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 13px;
- }
- .cells { pointer-events: none; }
- .cell {
- position: absolute;
- box-sizing: border-box;
- border: 1px solid rgba(64, 158, 255, 0.95);
- background: rgba(64, 158, 255, 0.18);
- }
- .tpl-hint {
- margin: 10px 0 0;
- color: #7a8794;
- font-size: 12px;
- line-height: 1.5;
- }
- .empty-hint { margin: 6px 0 0; color: #c45656; font-size: 12px; }
- @media (max-width: 960px) {
- .print-layout { flex-direction: column; }
- .print-right { width: 100%; }
- }
- </style>
- <style>
- .print-overprint-dialog .el-dialog__body {
- max-height: 74vh;
- overflow: auto;
- padding-top: 12px;
- }
- </style>
|