|
|
<template> <div> <div class="pda-container"> <div class="status-bar"> <div class="goBack" @click="goBack"><i class="el-icon-arrow-left"></i>上一页</div> <div class="goBack">{{ functionTitle }}</div> <div class="network" style="color: #fff" @click="$router.push({ path: '/' })">🏠首页</div> </div> <div style="overflow-y: auto"> <!-- 功能选择 --> <div class="function-selector" v-if="!selectedFunction"> <div class="function-card" @click="selectFunction('direct')"> <div class="function-icon">📦</div> <div class="function-title">直接发料</div> <div class="function-desc">输入工单号,扫描物料标签直接发料</div> </div>
<div class="function-card" @click="selectFunction('picking')"> <div class="function-icon">🏗️</div> <div class="function-title">拣选装托盘</div> <div class="function-desc">基于申请单创建托盘,扫描箱卷绑定</div> </div>
<div class="function-card" @click="selectFunction('request')"> <div class="function-icon">📋</div> <div class="function-title">申请单发料</div> <div class="function-desc">基于申请单扫描物料标签发料</div> </div> </div>
<!-- 直接发料 --> <div class="direct-issue" v-if="selectedFunction === 'direct'"> <!-- 工单输入 --> <div class="input-section" v-if="!workOrderMaterials.length"> <div class="input-group"> <label>工单号</label> <div class="input-with-scan"> <input v-model="directIssueForm.workOrderNo" placeholder="请输入或扫描工单号" @keyup.enter="loadWorkOrderMaterials" /> <button @click="loadWorkOrderMaterials" class="scan-btn">确认</button> </div> </div> </div>
<!-- 物料列表 --> <div class="materials-section" v-if="workOrderMaterials.length"> <div class="section-header"> <h3>工单物料 ({{ directIssueForm.workOrderNo }})</h3> <button @click="resetWorkOrder" class="reset-btn">重新选择</button> </div>
<div class="material-list"> <div v-for="(material, index) in workOrderMaterials" :key="index" class="material-item" :class="{ selected: (selectedMaterial?selectedMaterial.partNo:'') === (material&&material.partNo?material.partNo:'') }" @click="selectMaterial(material)" > <div class="material-info"> <div class="part-no">{{ material.partNo }}</div> <div class="part-desc">{{ material.partDesc }}</div> <div class="qty-info"> 需求: {{ material.requiredQty }} | 已发: {{ material.issuedQty }} | 剩余: {{ material.remainQty }} </div> </div> <div class="material-status"> <span v-if="material.remainQty > 0" class="status-pending">待发料</span> <span v-else class="status-complete">已完成</span> </div> </div> </div> </div>
<!-- 扫描标签 --> <div class="scan-section" v-if="selectedMaterial"> <div class="section-header"> <h3>扫描物料标签</h3> </div>
<div class="input-group"> <label>物料标签</label> <div class="input-with-scan"> <input v-model="scannedLabel" placeholder="请扫描物料标签" @keyup.enter="parseMaterialLabel" /> <button @click="parseMaterialLabel" class="scan-btn">解析</button> </div> </div>
<!-- 标签信息 --> <div class="label-info" v-if="labelInfo"> <div class="info-row"> <span class="label">物料编码:</span> <span class="value">{{ labelInfo.partNo }}</span> </div> <div class="info-row"> <span class="label">物料描述:</span> <span class="value">{{ labelInfo.partDesc }}</span> </div> <div class="info-row"> <span class="label">批次号:</span> <span class="value">{{ labelInfo.batchNo }}</span> </div> <div class="info-row"> <span class="label">库位:</span> <span class="value">{{ labelInfo.locationId }}</span> </div> <div class="info-row"> <span class="label">可用数量:</span> <span class="value">{{ labelInfo.availableQty }}</span> </div> </div>
<!-- 发料数量 --> <div class="qty-input" v-if="labelInfo"> <div class="input-group"> <label>发料数量</label> <input v-model="issueQty" type="number" :max="Math.min(selectedMaterial.remainQty, labelInfo.availableQty)" placeholder="请输入发料数量" /> </div>
<div class="input-group"> <label>备注</label> <input v-model="directIssueForm.remark" placeholder="可选" /> </div>
<button @click="confirmDirectIssue" class="confirm-btn" :disabled="!issueQty"> 确认发料 </button> </div> </div> </div>
<!-- 拣选装托盘 --> <div class="picking-pallet" v-if="selectedFunction === 'picking'"> <!-- 申请单输入 --> <div class="input-section" v-if="!currentPallet.palletId"> <div class="input-group"> <label>申请单号</label> <div class="input-with-scan"> <input v-model="palletForm.notifyNo" placeholder="请输入申请单号" @keyup.enter="createPallet" /> <button @click="createPallet" class="scan-btn">创建托盘</button> </div> </div> </div>
<!-- 托盘信息 --> <div class="pallet-info" v-if="currentPallet.palletId"> <div class="section-header"> <h3>托盘信息</h3> </div>
<div class="info-card"> <div class="info-row"> <span class="label">托盘ID:</span> <span class="value">{{ currentPallet.palletId }}</span> </div> <div class="info-row"> <span class="label">申请单号:</span> <span class="value">{{ currentPallet.notifyNo }}</span> </div> <div class="info-row"> <span class="label">已绑定单元:</span> <span class="value">{{ currentPallet.unitCount || 0 }}</span> </div> </div>
<!-- 扫描绑定 --> <div class="scan-bind-section"> <div class="input-group"> <label>扫描箱/卷</label> <div class="input-with-scan"> <input v-model="scannedUnit" placeholder="请扫描处理单元条码" @keyup.enter="bindUnit" /> <button @click="bindUnit" class="scan-btn">绑定</button> </div> </div> </div>
<!-- 已绑定单元列表 --> <div class="bound-units" v-if="currentPallet.scannedUnits.length"> <div class="section-header"> <h4>已绑定单元 ({{ currentPallet.scannedUnits.length }})</h4> </div> <div class="unit-list"> <div v-for="unit in currentPallet.scannedUnits" :key="unit" class="unit-item"> {{ unit }} </div> </div> </div>
<!-- 打印标签 --> <div class="print-section"> <div class="input-group"> <label>打印机</label> <select v-model="palletForm.printerName"> <option value="">请选择打印机</option> <option value="PRINTER_01">打印机01</option> <option value="PRINTER_02">打印机02</option> </select> </div>
<button @click="printPalletLabel" class="print-btn" :disabled="!palletForm.printerName"> 打印托盘标签 </button> </div> </div> </div>
<!-- 申请单发料 --> <div class="request-issue" v-if="selectedFunction === 'request'"> <!-- 申请单输入 --> <div class="input-section" v-if="!requestMaterials.length"> <div class="input-group"> <label>申请单号</label> <div class="input-with-scan"> <input v-model="requestIssueForm.notifyNo" placeholder="请输入申请单号" @keyup.enter="loadRequestMaterials" /> <button @click="loadRequestMaterials" class="scan-btn">确认</button> </div> </div> </div>
<!-- 申请单物料列表 --> <div class="materials-section" v-if="requestMaterials.length"> <div class="section-header"> <h3>申请单物料 ({{ requestIssueForm.notifyNo }})</h3> <button @click="resetRequest" class="reset-btn">重新选择</button> </div>
<div class="material-list"> <div v-for="material in requestMaterials" :key="`${material.partNo}-${material.itemNo}`" class="material-item" :class="{ selected: selectedRequestMaterial.itemNo === material.itemNo }" @click="selectRequestMaterial(material)" > <div class="material-info"> <div class="part-no">{{ material.partNo }}</div> <div class="part-desc">{{ material.partDesc }}</div> <div class="work-order">工单: {{ material.workOrderNo }}</div> <div class="qty-info"> 申请: {{ material.requestQty }} | 已发: {{ material.issuedQty }} | 剩余: {{ material.remainQty }} </div> </div> <div class="material-status"> <span v-if="material.remainQty > 0" class="status-pending">待发料</span> <span v-else class="status-complete">已完成</span> </div> </div> </div> </div>
<!-- 扫描标签和发料 --> <div class="scan-section" v-if="selectedRequestMaterial"> <div class="section-header"> <h3>扫描物料标签</h3> </div>
<div class="input-group"> <label>物料标签</label> <div class="input-with-scan"> <input v-model="scannedLabel" placeholder="请扫描物料标签" @keyup.enter="parseMaterialLabel" /> <button @click="parseMaterialLabel" class="scan-btn">解析</button> </div> </div>
<!-- 标签信息和发料确认 --> <div class="label-info" v-if="labelInfo"> <div class="info-row"> <span class="label">物料编码:</span> <span class="value">{{ labelInfo.partNo }}</span> </div> <div class="info-row"> <span class="label">批次号:</span> <span class="value">{{ labelInfo.batchNo }}</span> </div> <div class="info-row"> <span class="label">可用数量:</span> <span class="value">{{ labelInfo.availableQty }}</span> </div>
<div class="qty-input"> <div class="input-group"> <label>发料数量</label> <input v-model="issueQty" type="number" :max="Math.min(selectedRequestMaterial.remainQty, labelInfo.availableQty)" placeholder="请输入发料数量" /> </div>
<div class="input-group"> <label>备注</label> <input v-model="requestIssueForm.remark" placeholder="可选" /> </div>
<button @click="confirmRequestIssue" class="confirm-btn" :disabled="!issueQty"> 确认发料 (同步IFS) </button> </div> </div> </div> </div>
<!-- 加载提示 --> <div class="loading" v-if="loading"> <div class="loading-spinner"></div> <div class="loading-text">{{ loadingText }}</div> </div>
<!-- 消息提示 --> <div class="message" v-if="message" :class="messageType"> {{ message }} </div> </div> </div> </div></template>
<script>import { getWorkOrderMaterials, parseMaterialLabel, directIssue, getRequestMaterials, requestIssue, createPickingPallet, bindUnitsToPallet, printPalletLabel, getPalletInfo} from '@/api/production/production-issue'
export default { name: 'ProductionIssuePDA', data() { return { selectedFunction: null, loading: false, loadingText: '', message: '', messageType: 'info',
// 直接发料
directIssueForm: { site: this.$store.state.user.site, workOrderNo: '', operatorName: 'PDA_USER', remark: '', selectedMaterials: [] }, workOrderMaterials: [], selectedMaterial: { partNo: '', partDesc: '', requiredQty: 0, issuedQty: 0, remainQty: 0 },
// 申请单发料
requestIssueForm: { site: this.$store.state.user.site, notifyNo: '', workOrderNo: '', operatorName: 'PDA_USER', remark: '', selectedMaterials: [] }, requestMaterials: [], selectedRequestMaterial: null,
// 托盘拣选
palletForm: { site: this.$store.state.user.site, notifyNo: '', operatorName: 'PDA_USER', printerName: '', remark: '' }, currentPallet: {}, scannedUnit: '',
// 通用
scannedLabel: '', labelInfo: null, issueQty: null } }, computed: { functionTitle() { if (!this.selectedFunction) return '生产发料'; if (this.selectedFunction === 'direct') return '直接发料'; if (this.selectedFunction === 'picking') return '拣选装托盘'; if (this.selectedFunction === 'request') return '申请单发料'; return '生产发料'; } }, methods: { selectFunction(func) { this.selectedFunction = func this.resetAll() },
goBack() { if (!this.selectedFunction) { this.$router.push('/') } else { this.selectedFunction = null this.resetAll() } },
resetAll() { this.workOrderMaterials = [] this.requestMaterials = [] this.selectedMaterial = null this.selectedRequestMaterial = null this.currentPallet = {} this.scannedLabel = '' this.labelInfo = null this.issueQty = null this.message = '' },
// 直接发料相关方法
async loadWorkOrderMaterials() { if (!this.directIssueForm.workOrderNo) { this.$message.error('请输入工单号', 'error') return }
this.loading = true this.loadingText = '加载工单物料...'
try { const response = await getWorkOrderMaterials({ site: this.directIssueForm.site, workOrderNo: this.directIssueForm.workOrderNo })
if (response.data.code === 0) { this.workOrderMaterials = response.data.materials || [] if (this.workOrderMaterials.length === 0) { this.showMessage('该工单没有物料需求', 'warning') } } else { this.showMessage(response.data.msg, 'error') } } catch (error) { this.showMessage('加载工单物料失败', 'error') } finally { this.loading = false } },
selectMaterial(material) { if (material.remainQty <= 0) { this.showMessage('该物料已发料完成', 'warning') return } this.selectedMaterial = material this.scannedLabel = '' this.labelInfo = null this.issueQty = null },
resetWorkOrder() { this.directIssueForm.workOrderNo = '' this.workOrderMaterials = [] this.selectedMaterial = null this.scannedLabel = '' this.labelInfo = null this.issueQty = null },
async confirmDirectIssue() { if (!this.issueQty || this.issueQty <= 0) { this.showMessage('请输入有效的发料数量', 'error') return }
this.loading = true this.loadingText = '发料中...'
try { const issueData = { ...this.directIssueForm, selectedMaterials: [{ ...this.selectedMaterial, issueQty: this.issueQty }], scannedLabel: this.scannedLabel }
const response = await directIssue(issueData)
if (response.data.code === 0) { this.showMessage('发料成功', 'success') // 刷新物料列表
await this.loadWorkOrderMaterials() // 重置选择
this.selectedMaterial = null this.scannedLabel = '' this.labelInfo = null this.issueQty = null } else { this.showMessage(response.data.msg, 'error') } } catch (error) { this.showMessage('发料失败', 'error') } finally { this.loading = false } },
// 申请单发料相关方法
async loadRequestMaterials() { if (!this.requestIssueForm.notifyNo) { this.showMessage('请输入申请单号', 'error') return }
this.loading = true this.loadingText = '加载申请单物料...'
try { const response = await getRequestMaterials({ site: this.requestIssueForm.site, notifyNo: this.requestIssueForm.notifyNo })
if (response.data.code === 0) { this.requestMaterials = response.data.materials || [] if (this.requestMaterials.length === 0) { this.showMessage('该申请单没有物料需求', 'warning') } } else { this.showMessage(response.data.msg, 'error') } } catch (error) { this.showMessage('加载申请单物料失败', 'error') } finally { this.loading = false } },
selectRequestMaterial(material) { if (material.remainQty <= 0) { this.showMessage('该物料已发料完成', 'warning') return } this.selectedRequestMaterial = material this.requestIssueForm.workOrderNo = material.workOrderNo this.scannedLabel = '' this.labelInfo = null this.issueQty = null },
resetRequest() { this.requestIssueForm.notifyNo = '' this.requestIssueForm.workOrderNo = '' this.requestMaterials = [] this.selectedRequestMaterial = null this.scannedLabel = '' this.labelInfo = null this.issueQty = null },
async confirmRequestIssue() { if (!this.issueQty || this.issueQty <= 0) { this.showMessage('请输入有效的发料数量', 'error') return }
this.loading = true this.loadingText = '发料中,同步IFS...'
try { const issueData = { ...this.requestIssueForm, selectedMaterials: [{ ...this.selectedRequestMaterial, issueQty: this.issueQty }], scannedLabel: this.scannedLabel }
const response = await requestIssue(issueData)
if (response.data.code === 0) { this.showMessage('发料成功,已同步到IFS', 'success') // 刷新物料列表
await this.loadRequestMaterials() // 重置选择
this.selectedRequestMaterial = null this.scannedLabel = '' this.labelInfo = null this.issueQty = null } else { this.showMessage(response.data.msg, 'error') } } catch (error) { this.showMessage('发料失败', 'error') } finally { this.loading = false } },
// 托盘拣选相关方法
async createPallet() { if (!this.palletForm.notifyNo) { this.showMessage('请输入申请单号', 'error') return }
this.loading = true this.loadingText = '创建托盘...'
try { const response = await createPickingPallet({ ...this.palletForm, palletType: 'PALLET' })
if (response.data.code === 0) { this.currentPallet = { palletId: response.data.palletId, notifyNo: this.palletForm.notifyNo, scannedUnits: [], unitCount: 0 } this.showMessage('托盘创建成功: ' + response.data.palletId, 'success') } else { this.showMessage(response.data.msg, 'error') } } catch (error) { this.showMessage('创建托盘失败', 'error') } finally { this.loading = false } },
async bindUnit() { if (!this.scannedUnit) { this.showMessage('请扫描处理单元条码', 'error') return }
if (this.currentPallet.scannedUnits.includes(this.scannedUnit)) { this.showMessage('该处理单元已绑定', 'warning') this.scannedUnit = '' return }
this.loading = true this.loadingText = '绑定处理单元...'
try { const response = await bindUnitsToPallet({ site: this.palletForm.site, palletId: this.currentPallet.palletId, scannedUnits: [this.scannedUnit] })
if (response.data.code === 0) { this.currentPallet.scannedUnits.push(this.scannedUnit) this.currentPallet.unitCount = this.currentPallet.scannedUnits.length this.showMessage('绑定成功', 'success') this.scannedUnit = '' } else { this.showMessage(response.data.msg, 'error') } } catch (error) { this.showMessage('绑定失败', 'error') } finally { this.loading = false } },
async printPalletLabel() { if (!this.palletForm.printerName) { this.showMessage('请选择打印机', 'error') return }
this.loading = true this.loadingText = '打印标签...'
try { const response = await printPalletLabel({ site: this.palletForm.site, palletId: this.currentPallet.palletId, printerName: this.palletForm.printerName })
if (response.data.code === 0) { this.showMessage('标签打印成功', 'success') } else { this.showMessage(response.data.msg, 'error') } } catch (error) { this.showMessage('打印失败', 'error') } finally { this.loading = false } },
// 通用方法
async parseMaterialLabel() { if (!this.scannedLabel) { this.showMessage('请扫描物料标签', 'error') return }
this.loading = true this.loadingText = '解析标签...'
try { const response = await parseMaterialLabel({ site: this.directIssueForm.site || this.requestIssueForm.site, scannedLabel: this.scannedLabel })
if (response.data.code === 0 && response.data.labelInfo) { this.labelInfo = response.data.labelInfo this.issueQty = null // 重置发料数量
this.showMessage('标签解析成功', 'success') } else { this.showMessage('无法解析标签或标签无效', 'error') this.labelInfo = null } } catch (error) { this.showMessage('标签解析失败', 'error') this.labelInfo = null } finally { this.loading = false } },
showMessage(text, type = 'info') { this.message = text this.messageType = type setTimeout(() => { this.message = '' }, 3000) } }}</script>
<style scoped>.production-issue-pda { padding: 10px; font-family: Arial, sans-serif; background-color: #f5f5f5; min-height: 100vh;}
/* 功能选择 */.function-selector { display: flex; flex-direction: column; gap: 15px; padding: 20px 0;}
.function-card { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); cursor: pointer; transition: all 0.3s; text-align: center;}
.function-card:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.15);}
.function-icon { font-size: 48px; margin-bottom: 10px;}
.function-title { font-size: 18px; font-weight: bold; margin-bottom: 8px; color: #333;}
.function-desc { font-size: 14px; color: #666;}
/* 输入区域 */.input-section, .materials-section, .scan-section, .pallet-info, .print-section { background: white; border-radius: 8px; padding: 15px; margin-bottom: 15px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);}
.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #eee;}
.section-header h3, .section-header h4 { margin: 0; color: #333;}
.reset-btn { background: #17b3a3; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;}
.input-group { margin-bottom: 15px;}
.input-group label { display: block; margin-bottom: 5px; font-weight: bold; color: #333;}
.input-with-scan { display: flex; gap: 10px;}
.input-with-scan input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 16px;}
.scan-btn, .confirm-btn, .print-btn { background: #17b3a3; color: white; border: none; padding: 10px 15px; border-radius: 4px; cursor: pointer; font-size: 14px; white-space: nowrap;}
.scan-btn:hover, .confirm-btn:hover, .print-btn:hover { background: #13998c;}
.confirm-btn:disabled, .print-btn:disabled { background: #6c757d; cursor: not-allowed;}
/* 物料列表 */.material-list { display: flex; flex-direction: column; gap: 10px;}
.material-item { display: flex; justify-content: space-between; align-items: center; padding: 15px; border: 1px solid #ddd; border-radius: 6px; cursor: pointer; transition: all 0.3s;}
.material-item:hover { border-color: #007bff; background-color: #f8f9fa;}
.material-item.selected { border-color: #007bff; background-color: #e3f2fd;}
.material-info { flex: 1;}
.part-no { font-weight: bold; font-size: 16px; color: #333; margin-bottom: 4px;}
.part-desc { color: #666; font-size: 14px; margin-bottom: 4px;}
.work-order { color: #007bff; font-size: 12px; margin-bottom: 4px;}
.qty-info { font-size: 12px; color: #666;}
.material-status { text-align: right;}
.status-pending { background: #ffc107; color: #212529; padding: 4px 8px; border-radius: 12px; font-size: 12px;}
.status-complete { background: #28a745; color: white; padding: 4px 8px; border-radius: 12px; font-size: 12px;}
/* 标签信息 */.label-info, .info-card { background: #f8f9fa; border-radius: 6px; padding: 15px; margin-bottom: 15px;}
.info-row { display: flex; justify-content: space-between; margin-bottom: 8px; padding: 5px 0; border-bottom: 1px solid #eee;}
.info-row:last-child { border-bottom: none; margin-bottom: 0;}
.info-row .label { font-weight: bold; color: #666;}
.info-row .value { color: #333;}
/* 数量输入 */.qty-input { margin-top: 15px;}
.qty-input input[type="number"] { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 16px;}
/* 托盘相关 */.scan-bind-section { margin: 20px 0;}
.bound-units { margin-top: 20px;}
.unit-list { display: flex; flex-direction: column; gap: 8px; max-height: 200px; overflow-y: auto;}
.unit-item { background: #e9ecef; padding: 10px; border-radius: 4px; font-family: monospace; font-size: 14px;}
/* 加载和消息 */.loading { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0,0,0,0.8); color: white; padding: 20px; border-radius: 8px; text-align: center; z-index: 1000;}
.loading-spinner { width: 40px; height: 40px; border: 4px solid #f3f3f3; border-top: 4px solid #007bff; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 10px;}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); }}
.loading-text { font-size: 14px;}
.message { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); padding: 12px 20px; border-radius: 6px; font-weight: bold; z-index: 1001; max-width: 90%; text-align: center;}
.message.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb;}
.message.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb;}
.message.warning { background: #fff3cd; color: #856404; border: 1px solid #ffeaa7;}
.message.info { background: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb;}
/* 响应式设计 */@media (max-width: 768px) { .production-issue-pda { padding: 5px; }
.function-card { padding: 15px; }
.function-icon { font-size: 36px; }
.input-with-scan { flex-direction: column; }
.material-item { flex-direction: column; align-items: flex-start; gap: 10px; }
.material-status { align-self: flex-end; }}</style>
|