Browse Source

领料

master
shenzhouyu 6 months ago
parent
commit
333cef5bca
  1. 4
      src/api/production/production-issue.js
  2. 6
      src/router/index.js
  3. 3
      src/views/common/login.vue
  4. 363
      src/views/modules/outsourcing-issue/DirectIssue.vue
  5. 50
      src/views/modules/outsourcing-issue/MoveIssue.vue
  6. 71
      src/views/modules/outsourcing-issue/PickingIssue.vue
  7. 58
      src/views/modules/outsourcing-issue/ReturnIssue.vue
  8. 283
      src/views/modules/outsourcing-issue/index.vue
  9. 402
      src/views/modules/outsourcing-issue/outsourcingPicking.vue
  10. 1030
      src/views/modules/outsourcing-issue/outsourcingPickingDetail.vue
  11. 358
      src/views/modules/outsourcing-issue/pick.vue
  12. 165
      src/views/modules/production-issue/production.vue
  13. 489
      src/views/modules/production-issue/productionIssuePda.vue
  14. 399
      src/views/modules/production-issue/productionPicking.vue
  15. 984
      src/views/modules/production-issue/productionPickingDetail.vue

4
src/api/production/production-issue.js

@ -27,4 +27,8 @@ export const validateWorkOrder = data => createAPI(`/pda/production/issue/valida
export const validateNotify = data => createAPI(`/pda/production/issue/validateNotify`,'post',data)
// 扫描材料是否存在
export const scanMaterialLabel = data => createAPI(`/pda/production/issue/scanMaterialLabel`,'post',data)
// 获取工单列表
export const getIssureNotifyByNo = data => createAPI(`/pda/production/issue/getIssureNotifyByNo`,'post',data)
// 获取工单列表
export const getIssureNotifyListByNo = data => createAPI(`/pda/production/issue/getIssureNotifyListByNo`,'post',data)

6
src/router/index.js

@ -34,7 +34,9 @@ const globalRoutes = [
// handlingunit
{path: "/handlingunit",name: "handlingunit", component: resolve => require(["@/views/modules/handling-unit/handling-unit-management.vue"], resolve), meta: { transition: 'instant' ,preload: true,keepAlive: true}},
// 生产发料
{path: "/productionissue",name: "productionissue", component: resolve => require(["@/views/modules/production-issue/productionIssuePda.vue"], resolve), meta: { transition: 'instant' ,preload: true,keepAlive: true}},
{path: "/productionissue",name: "productionissue", component: resolve => require(["@/views/modules/production-issue/production.vue"], resolve), meta: { transition: 'instant' ,preload: true,keepAlive: true}},
{ path: "/productionPicking", name: "productionPicking", component: resolve => require(["@/views/modules/production-issue/productionPicking.vue"], resolve), meta: { transition: 'instant', preload: true, keepAlive: true } },
{ path: "/productionPickingDetail/:outboundNo", name: "productionPickingDetail", component: resolve => require(["@/views/modules/production-issue/productionPickingDetail.vue"], resolve), meta: { transition: 'instant', preload: true, keepAlive: true } },
//生产退料
{path: "/productionreturn",name: "productionreturn", component: resolve => require(["@/views/modules/production-issue/productionReturnPDA.vue"], resolve), meta: { transition: 'instant' ,preload: true,keepAlive: true}},
{path: '/production-issue/pick/:orderNo',
@ -45,6 +47,8 @@ const globalRoutes = [
// 委外
{path: "/outsource",name: "outsource", component: resolve => require(["@/views/modules/outsourcing-issue/index.vue"], resolve), meta: { transition: 'instant' ,preload: true,keepAlive: true}},
{ path: "/outsourcingPicking", name: "outsourcingPicking", component: resolve => require(["@/views/modules/outsourcing-issue/outsourcingPicking.vue"], resolve), meta: { transition: 'instant', preload: true, keepAlive: true } },
{ path: "/outsourcingPickingDetail/:outsourcingNo", name: "outsourcingPickingDetail", component: resolve => require(["@/views/modules/outsourcing-issue/outsourcingPickingDetail.vue"], resolve), meta: { transition: 'instant', preload: true, keepAlive: true } },
// 客户订单发货
{path: "/saleshipping",name: "saleshipping", component: resolve => require(["@/views/modules/sales-delivery/index.vue"], resolve), meta: { transition: 'instant' ,preload: true,keepAlive: true}},

3
src/views/common/login.vue

@ -151,6 +151,9 @@ export default {
this.$cookie.set('token', data.token);
localStorage.setItem('userName', this.dataForm.userName);
localStorage.setItem('site', this.selectedSite);
this.$store.commit('user/updateSite', this.selectedSite);
this.$router.replace({ name: 'home' });
} else {
this.$alert("用户名或密码错误", '错误', {

363
src/views/modules/outsourcing-issue/DirectIssue.vue

@ -1,363 +0,0 @@
<template>
<div>
<div class="status-bar">
<div class="goBack" @click="handleBack"><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 class="input-section">
<!-- PO号输入 -->
<div v-if="processFlag === 1">
<div class="input-group">
<div class="input-group">
<label>PO号</label>
<div class="input-with-scan">
<input v-model="poNo" placeholder="请输入或扫描PO号" @keyup.enter="loadMaterials" />
</div>
</div>
<div class="input-group">
<button @click="loadMaterials" class="scan-btn">确认</button>
</div>
</div>
<div class="materials-section" v-if="materialList.length">
<div class="material-list">
<div v-for="(material, index) in materialList" :key="index" class="material-item"
:class="{ selected: selectedMaterial && selectedMaterial.partNo === material.partNo }"
@click="goToDetail(material)">
<div class="material-info">
<div class="part-no">{{ material.partNo }}</div>
<div class="part-desc">{{ material.desc }}</div>
<div class="qty-info">
需求: {{ material.qty }} | 已发: {{ material.recvQty }} | 剩余: {{ material.thisRecvQty }}
</div>
</div>
<div class="material-status">
<span v-if="material.thisRecvQty > 0" class="status-pending">待发料</span>
<span v-else class="status-complete">已完成</span>
</div>
</div>
</div>
</div>
</div>
<!-- 扫描标签/发料详情 -->
<div class="scan-section" v-if="processFlag === 2">
<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>
<div class="qty-input" v-if="labelInfo">
<div class="input-group">
<label>发料数量</label>
<input v-model="issueQty" type="number"
:max="Math.min(selectedMaterial.thisRecvQty, labelInfo.availableQty)" placeholder="请输入发料数量" />
</div>
<div class="input-group">
<label>备注</label>
<el-input type="textarea" v-model="remark" placeholder="可选" />
</div>
<button @click="confirmIssue" class="confirm-btn" :disabled="!issueQty">确认发料</button>
</div>
</div>
<!-- 消息提示 -->
<div class="message" v-if="message" :class="messageType">{{ message }}</div>
</div>
</div>
</template>
<script>
import { getPoList } from '@/api/po/po.js'
export default {
name: 'DirectIssue',
props: {
functionTitle: {
type: String,
default: '',
},
},
data() {
return {
processFlag: 1, // 1=PO, 2=, 3=/
poNo: '',
materialList: [],
selectedMaterial: null,
scannedLabel: '',
labelInfo: null,
issueQty: null,
remark: '',
message: '',
messageType: 'info',
}
},
methods: {
handleBack() {
if (this.processFlag === 2) {
this.processFlag = 1
} else if (this.processFlag === 1) {
this.$emit('back')
}
},
async loadMaterials() {
if (!this.poNo) {
this.showMessage('请输入PO号', 'error')
return
}
// APIPO
try {
const { data } = await getPoList({
poNumber: this.poNo,
site: localStorage.getItem('site'),
})
if (data.code === 0 && data.rows && data.rows.length > 0) {
this.materialList = data.rows
} else {
this.showMessage(data.msg || '未找到PO物料', 'warning')
}
} catch (e) {
this.showMessage('网络错误', 'error')
}
},
goToDetail(material) {
if (material.thisRecvQty <= 0) {
this.showMessage('该物料已发料完成', 'warning')
return
}
this.selectedMaterial = material
this.scannedLabel = ''
this.labelInfo = {
partNo: this.selectedMaterial.partNo,
batchNo: 'BATCH001',
availableQty: 100,
}
this.issueQty = null
this.processFlag = 2
},
resetPO() {
this.poNo = ''
this.materialList = []
this.selectedMaterial = null
this.scannedLabel = ''
this.labelInfo = null
this.issueQty = null
this.processFlag = 1
},
parseMaterialLabel() {
// TODO: APIlabelInfo
this.labelInfo = {
partNo: this.selectedMaterial.partNo,
batchNo: 'BATCH001',
availableQty: 100,
}
this.issueQty = null
this.showMessage('标签解析成功', 'success')
},
confirmIssue() {
if (!this.issueQty || this.issueQty <= 0) {
this.showMessage('请输入有效的发料数量', 'error')
return
}
// TODO: API
this.showMessage('发料成功', 'success')
//
this.loadMaterials()
this.selectedMaterial = null
this.scannedLabel = ''
this.labelInfo = null
this.issueQty = null
this.processFlag = 1
},
showMessage(text, type = 'info') {
this.message = text
this.messageType = type
setTimeout(() => {
this.message = ''
}, 2000)
},
},
}
</script>
<style scoped>
.input-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;
}
.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 {
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 {
background: #13998c;
}
.confirm-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.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;
}
.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 {
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;
}
.message {
margin-top: 10px;
padding: 8px 12px;
border-radius: 4px;
font-weight: bold;
}
.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;
}
.confirm-btn {
width: 100%;
}
.scan-btn {
width: 100%;
}
</style>

50
src/views/modules/outsourcing-issue/MoveIssue.vue

@ -1,50 +0,0 @@
<template>
<div class="input-section">
<!-- 申请单/PO号输入 -->
<div class="input-group">
<label>申请单/PO号</label>
<div class="input-with-scan">
<input v-model="orderNo" placeholder="请输入申请单号或PO号" />
<button class="scan-btn">确认</button>
</div>
</div>
<!-- 选择库位 -->
<div class="input-group">
<label>目标库位</label>
<div class="input-with-scan">
<input v-model="location" placeholder="请输入目标库位" />
<button class="scan-btn">选择</button>
</div>
</div>
<!-- 生成移库记录 -->
<div class="input-group">
<button class="confirm-btn">生成移库记录</button>
</div>
</div>
</template>
<script>
export default {
name: 'MoveIssue',
data() {
return {
orderNo: '',
location: ''
}
}
}
</script>
<style scoped>
.input-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; }
.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 { 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 { background: #13998c; }
.confirm-btn:disabled { background: #6c757d; cursor: not-allowed; }
.confirm-btn {
width: 100%;
}
</style>

71
src/views/modules/outsourcing-issue/PickingIssue.vue

@ -1,71 +0,0 @@
<template>
<div class="input-section">
<!-- 申请单号输入 -->
<div class="input-group">
<label>申请单号</label>
<div class="input-with-scan">
<input v-model="notifyNo" placeholder="请输入申请单号" />
<button class="scan-btn">创建托盘</button>
</div>
</div>
<!-- 扫描箱卷绑定 -->
<div class="input-group">
<label>扫描箱/</label>
<div class="input-with-scan">
<input v-model="scannedUnit" placeholder="请扫描处理单元条码" />
<button class="scan-btn">绑定</button>
</div>
</div>
<!-- 已绑定单元列表 -->
<div class="bound-units">
<div class="section-header">
<h4>已绑定单元 (0)</h4>
</div>
<div class="unit-list">
<!-- TODO: 列表渲染绑定单元 -->
</div>
</div>
<!-- 打印托盘标签 -->
<div class="print-section">
<div class="input-group">
<label>打印机</label>
<el-select v-model="selectedPrinter" placeholder="请选择打印机" style="width: 100%">
<el-option value="">请选择打印机</el-option>
<el-option value="PRINTER_01">打印机01</el-option>
<el-option value="PRINTER_02">打印机02</el-option>
</el-select>
</div>
<button class="print-btn">打印托盘标签</button>
</div>
</div>
</template>
<script>
export default {
name: 'PickingIssue',
data() {
return {
notifyNo: '',
scannedUnit: '',
selectedPrinter: '',
}
}
}
</script>
<style scoped>
.input-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; }
.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, .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, .print-btn:hover { background: #13998c; }
.print-btn:disabled { background: #6c757d; cursor: not-allowed; }
.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; }
.print-btn {
width: 100%;
}
</style>

58
src/views/modules/outsourcing-issue/ReturnIssue.vue

@ -1,58 +0,0 @@
<template>
<div class="input-section">
<!-- 退料类型选择 -->
<div class="input-group">
<label>退料类型</label>
<select v-model="returnType">
<option value="">请选择退料类型</option>
<option value="over">多发退料</option>
<option value="quality">质量退料</option>
<option value="other">其他退料</option>
</select>
</div>
<!-- 退料数量 -->
<div class="input-group">
<label>退料数量</label>
<input v-model="returnQty" type="number" placeholder="请输入退料数量" />
<!-- 生成退料记录 -->
</div>
<button class="scan-btn">生成退料记录</button>
</div>
</template>
<script>
export default {
name: 'ReturnIssue',
data() {
return {
returnType: '',
returnQty: ''
}
}
}
</script>
<style scoped>
.input-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; }
.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-group select, .input-group input { width: 100%; 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;
width: 100%;
}
.scan-btn:hover, .confirm-btn:hover, .print-btn:hover {
background: #13998c;
}
.confirm-btn:disabled { background: #6c757d; cursor: not-allowed; }
</style>

283
src/views/modules/outsourcing-issue/index.vue

@ -1,168 +1,193 @@
<template>
<div>
<div class="pda-container">
<div class="status-bar" v-if="selectedFunction != 'direct'">
<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 class="pda-container">
<div class="header-bar">
<div class="header-left" @click="$router.back()">
<i class="el-icon-arrow-left"></i>
<span>委外发料</span>
</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('move')">
<div class="function-icon">🚚</div>
<div class="function-title">移库发料</div>
<div class="function-desc">输入申请单/PO号选择库位生成移库记录</div>
</div>
<div class="function-card" @click="selectFunction('return')">
<div class="function-icon"></div>
<div class="function-title">退料</div>
<div class="function-desc">选择退料类型录入退料数量生成退料记录</div>
</div>
</div>
<!-- 直接发料骨架 -->
<direct-issue v-if="selectedFunction === 'direct'" functionTitle="直接发料" @back="resetAll" />
<!-- 拣选装托盘骨架 -->
<div v-if="selectedFunction === 'picking'">
<picking-issue @back="resetAll" />
</div>
<!-- 移库发料骨架 -->
<div v-if="selectedFunction === 'move'">
<move-issue @back="resetAll" />
</div>
<!-- 退料骨架 -->
<div v-if="selectedFunction === 'return'">
<return-issue @back="resetAll" />
<div class="header-right" @click="$router.push({ path: '/' })">首页</div>
</div>
<!-- 功能菜单 -->
<div class="menu-grid">
<div
class="menu-item"
v-for="(btn, index) in buttons"
:key="index"
:class="{ disabled: btn.disabled }"
@click="handleButtonClick(btn)"
>
<div class="menu-icon" :class="btn.iconClass">
<van-icon :name="btn.icon" size="24" />
</div>
<div class="menu-text">{{ btn.label }}</div>
</div>
</div>
</div>
</template>
<script>
import DirectIssue from './DirectIssue.vue'
import PickingIssue from './PickingIssue.vue'
import MoveIssue from './MoveIssue.vue'
import ReturnIssue from './ReturnIssue.vue'
export default {
name: 'OutsourcingIssuePDA',
components: {
DirectIssue,
PickingIssue,
MoveIssue,
ReturnIssue,
},
data() {
return {
selectedFunction: null,
}
},
computed: {
functionTitle() {
if (!this.selectedFunction) return '委外发料'
if (this.selectedFunction === 'direct') return '直接发料'
if (this.selectedFunction === 'picking') return '拣选装托盘'
if (this.selectedFunction === 'move') return '移库发料'
if (this.selectedFunction === 'return') return '退料'
return '委外发料'
},
buttons: [
{
icon: "scan",
label: "直接发料",
iconClass: "direct",
to: "outsourcingPicking",
disabled: false,
},
{
icon: "records",
label: "申请单发料",
iconClass: "picking",
to: "outsourcingPicking",
disabled: false,
},
{
icon: "logistics",
label: "移库发料",
iconClass: "move",
to: "outsourcingPicking",
disabled: true,
},
{
icon: "revoke",
label: "退料",
iconClass: "return",
to: "outsourcingPicking",
disabled: true,
},
],
};
},
methods: {
selectFunction(func) {
this.selectedFunction = func
},
resetAll() {
this.selectedFunction = null
},
goBack() {
if (!this.selectedFunction) {
this.$router.push('/')
handleButtonClick(btn) {
if (btn.disabled) {
this.$message.warning("正在开发中,敬请期待...");
} else {
this.selectedFunction = null
this.$router.push(btn.to);
}
},
},
}
};
</script>
<style scoped>
.outsourcing-issue {
padding: 10px;
font-family: Arial, sans-serif;
background-color: #f5f5f5;
min-height: 100vh;
<style>
:root {
--columns: 3;
--button-size: calc(100vw / var(--columns) - 20px);
}
.function-selector {
/* 头部栏 */
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 16px;
background: #17b3a3;
color: white;
height: 40px;
min-height: 40px;
max-height: 40px;
}
.header-left {
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
font-size: 16px;
font-weight: 500;
}
.header-left i {
margin-right: 8px;
font-size: 18px;
}
.header-right {
cursor: pointer;
font-size: 16px;
font-weight: 500;
}
.menu-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
padding: 20px 0;
padding: 20px;
justify-content: center;
align-content: center;
width: 100%;
}
.function-card {
.menu-item {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: all 0.3s;
border-radius: 12px;
padding: 12px 6px;
text-align: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s;
cursor: pointer;
}
.function-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
.menu-item:active {
transform: scale(0.95);
}
.function-icon {
font-size: 48px;
margin-bottom: 10px;
.menu-item.disabled {
opacity: 0.6;
position: relative;
}
.function-title {
font-size: 18px;
.menu-item.disabled::after {
content: "开发中";
position: absolute;
top: 8px;
right: 8px;
background: #ff9500;
color: white;
font-size: 8px;
padding: 2px 4px;
border-radius: 8px;
font-weight: bold;
margin-bottom: 8px;
color: #333;
}
.function-desc {
font-size: 14px;
color: #666;
}
/* 响应式设计 */
@media (max-width: 768px) {
.production-issue-pda {
padding: 5px;
}
.menu-icon {
width: 38px;
height: 38px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 6px;
color: white;
}
.function-card {
padding: 15px;
}
.menu-icon.direct {
background: linear-gradient(135deg, #17b3a3 0%, #1dc5ef 100%);
}
.function-icon {
font-size: 36px;
}
.menu-icon.picking {
background: linear-gradient(135deg, #17b3a3 0%, #1dc5ef 100%);
}
.input-with-scan {
flex-direction: column;
}
.menu-icon.move {
background: linear-gradient(135deg, #17b3a3 0%, #1dc5ef 100%);
}
.material-item {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.menu-icon.return {
background: linear-gradient(135deg, #17b3a3 0%, #1dc5ef 100%);
}
.material-status {
align-self: flex-end;
}
.menu-text {
font-size: 10px;
color: #333;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-top: 2px;
}
</style>

402
src/views/modules/outsourcing-issue/outsourcingPicking.vue

@ -0,0 +1,402 @@
<template>
<div class="pda-container">
<!-- 头部栏 -->
<div class="header-bar">
<div class="header-left" @click="$router.back()">
<i class="el-icon-arrow-left"></i>
<span>委外发料</span>
</div>
<div class="header-right" @click="$router.push({ path: '/' })">
首页
</div>
</div>
<!-- 搜索框 -->
<div class="search-container">
<el-input clearable
v-model="searchCode"
placeholder="请扫描委外订单或申请单号"
prefix-icon="el-icon-search"
@keyup.enter.native="handleSearch"
ref="searchInput"
/>
</div>
<!-- 委外发料单列表 -->
<div class="content-area">
<div
v-for="(item, index) in outsourcingList"
:key="index"
class="outsourcing-card"
@click="goToPickingPage(item)"
>
<div class="card-title">
<span class="title-label">委外订单号</span>
<span class="title-value">{{ item.outsourcingNo }}</span>
</div>
<div class="card-details">
<div class="detail-item">
<div class="detail-label">申请单号</div>
<div class="detail-value">{{ item.requestNo }}</div>
</div>
<div class="detail-item">
<div class="detail-label">标签张数</div>
<div class="detail-value">
<span class="qualified">{{ item.pickedLabels }}</span><span class="total">{{ item.totalLabels }}</span>
</div>
</div>
<div class="detail-item">
<div class="detail-label">物料总数</div>
<div class="detail-value">
<span class="qualified">{{ item.requestQty }}</span><span class="total">{{ item.remainQty }}</span>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="outsourcingList.length === 0 && !loading" class="empty-state">
<i class="el-icon-box"></i>
<p>暂无待发料委外订单</p>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-state">
<i class="el-icon-loading"></i>
<p>加载中...</p>
</div>
</div>
</div>
</template>
<script>
import { getPoList } from '@/api/po/po.js'
import moment from 'moment';
export default {
data() {
return {
searchCode: '',
outsourcingList: [],
loading: false
};
},
methods: {
formatDate(date) {
return date ? moment(date).format('YYYY-MM-DD') : '';
},
//
handleSearch() {
if (this.searchCode.trim()) {
this.searchOutsourcingList(this.searchCode.trim());
} else {
this.loadOutsourcingList();
}
},
//
loadOutsourcingList() {
this.loading = true;
const params = {
site: this.$store.state.user.site,
status: '待发料',
type: 'outsourcing'
}
console.log('params', params);
// 使 PO API
getPoList(params).then(({ data }) => {
this.loading = false;
if (data && data.code === 0) {
//
this.outsourcingList = (data.rows || []).map(item => ({
outsourcingNo: item.poNumber || item.notifyNo,
requestNo: item.workOrderNo || item.relatedNo,
pickedLabels: item.pickedLabels || 0,
totalLabels: item.totalLabels || 0,
requestQty: item.requestQty || item.qty,
remainQty: item.remainQty || item.thisRecvQty
}));
} else {
this.$message.error(data.msg || '获取数据失败');
}
}).catch(error => {
this.loading = false;
console.error('获取委外发料单列表失败:', error);
this.$message.error('获取数据失败');
});
},
//
searchOutsourcingList(searchCode) {
this.loading = true;
const params = {
poNumber: searchCode,
site: this.$store.state.user.site,
status: '待发料',
type: 'outsourcing'
};
getPoList(params).then(({ data }) => {
this.loading = false;
if (data && data.code === 0) {
if (data.rows.length === 0) {
this.$message.warning('未找到匹配的委外订单');
}
//
this.outsourcingList = (data.rows || []).map(item => ({
outsourcingNo: item.poNumber || item.notifyNo,
requestNo: item.workOrderNo || item.relatedNo,
pickedLabels: item.pickedLabels || 0,
totalLabels: item.totalLabels || 0,
requestQty: item.requestQty || item.qty,
remainQty: item.remainQty || item.thisRecvQty
}));
} else {
this.$message.error(data.msg || '查询失败');
}
}).catch(error => {
this.loading = false;
this.$message.error('查询失败');
});
},
//
goToPickingPage(item) {
this.$router.push({
name: 'outsourcingPickingDetail',
params: {
outsourcingNo: item.outsourcingNo,
}
});
}
},
mounted() {
//
this.$nextTick(() => {
if (this.$refs.searchInput) {
this.$refs.searchInput.focus();
}
});
//
this.loadOutsourcingList();
}
};
</script>
<style scoped>
.pda-container {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
background: #f5f5f5;
}
/* 头部栏 */
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 16px;
background: #17B3A3;
color: white;
height: 40px;
min-height: 40px;
}
.header-left {
display: flex;
align-items: center;
cursor: pointer;
font-size: 16px;
font-weight: 500;
}
.header-left i {
margin-right: 8px;
font-size: 18px;
}
.header-right {
cursor: pointer;
font-size: 16px;
font-weight: 500;
}
/* 搜索容器 */
.search-container {
padding: 12px 16px;
background: white;
}
/* 内容区域 */
.content-area {
flex: 1;
overflow-y: auto;
padding: 12px 16px;
}
/* 委外卡片 */
.outsourcing-card {
background: white;
border-radius: 8px;
margin-bottom: 12px;
padding: 16px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: all 0.2s ease;
}
.outsourcing-card:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
transform: translateY(-1px);
}
.outsourcing-card:active {
transform: translateY(0);
}
/* 卡片标题 */
.card-title {
margin-bottom: 12px;
}
.title-label {
font-size: 12px;
color: #666;
display: block;
margin-bottom: 4px;
}
.title-value {
font-size: 16px;
font-weight: bold;
color: #333;
margin-left: 20px;
}
/* 卡片详情 */
.card-details {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 4px;
}
.detail-item {
flex: 1;
text-align: center;
min-width: 60px;
max-width: 60px;
}
.detail-label {
font-size: 11px;
color: #666;
margin-bottom: 4px;
line-height: 1.2;
margin-left: -12px;
}
.detail-value {
font-size: 13px;
color: #333;
line-height: 1.2;
margin-left: -12px;
}
.detail-value .qualified {
color: #17B3A3;
font-weight: 500;
}
.detail-value .total {
color: #333;
font-weight: 500;
}
.detail-value .total::before {
content: '/';
color: #333;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #999;
}
.empty-state i {
font-size: 48px;
margin-bottom: 16px;
}
.empty-state p {
font-size: 14px;
margin: 0;
}
/* 加载状态 */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #17B3A3;
}
.loading-state i {
font-size: 24px;
margin-bottom: 12px;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.loading-state p {
font-size: 14px;
margin: 0;
}
/* 响应式设计 */
@media (max-width: 360px) {
.header-bar {
padding: 8px 12px;
}
.search-container {
padding: 8px 12px;
}
.content-area {
padding: 8px 12px;
}
.outsourcing-card {
padding: 12px;
}
.card-details {
flex-wrap: wrap;
gap: 6px;
}
.detail-item {
flex: 0 0 48%;
margin-bottom: 6px;
min-width: 50px;
}
}
</style>

1030
src/views/modules/outsourcing-issue/outsourcingPickingDetail.vue
File diff suppressed because it is too large
View File

358
src/views/modules/outsourcing-issue/pick.vue

@ -1,358 +0,0 @@
<template>
<div class="pick-container">
<van-nav-bar title="委外拣料" left-arrow @click-left="$router.back()" />
<!-- 订单信息 -->
<div class="order-info">
<div class="info-header">
<div class="order-no">{{ orderInfo.orderNo }}</div>
<div class="order-status">{{ getStatusText(orderInfo.status) }}</div>
</div>
<van-cell-group>
<van-cell title="供应商" :value="orderInfo.supplier" />
<van-cell title="委外产品" :value="orderInfo.productName" />
<van-cell title="委外数量" :value="orderInfo.outsourcingQuantity" />
<van-cell title="交货日期" :value="orderInfo.deliveryDate" />
<van-cell title="联系人" :value="orderInfo.contactPerson" />
</van-cell-group>
</div>
<!-- 物料清单 -->
<div class="material-section">
<div class="section-title">发料清单</div>
<div
v-for="(item, index) in materialList"
:key="index"
class="material-item"
>
<div class="material-info">
<div class="material-name">{{ item.materialCode }} - {{ item.materialName }}</div>
<div class="material-spec">规格{{ item.specification }}</div>
<div class="material-location">
<van-tag type="primary" size="small">{{ item.locationCode }}</van-tag>
<span class="stock-info">库存{{ item.stock }}</span>
</div>
<div class="material-quantity">
需求{{ item.requiredQuantity }} | 已发{{ item.issuedQuantity }}
</div>
</div>
<div class="pick-input">
<van-stepper
v-model="item.currentPick"
:min="0"
:max="Math.min(item.requiredQuantity - item.issuedQuantity, item.stock)"
integer
/>
</div>
</div>
</div>
<!-- 供应商确认 -->
<div class="supplier-section">
<div class="section-title">供应商确认</div>
<van-field
v-model="supplierInfo.contactPerson"
label="接收人"
placeholder="请输入供应商接收人"
/>
<van-field
v-model="supplierInfo.contactPhone"
label="联系电话"
placeholder="请输入联系电话"
/>
<van-field
v-model="supplierInfo.deliveryAddress"
label="送货地址"
placeholder="请输入送货地址"
/>
</div>
<!-- 运输信息 -->
<div class="transport-section">
<div class="section-title">运输信息</div>
<van-field
v-model="transportInfo.transportMethod"
label="运输方式"
placeholder="请选择运输方式"
readonly
is-link
@click="showTransportPicker = true"
/>
<van-field
v-model="transportInfo.vehicleNo"
label="车牌号"
placeholder="请输入车牌号"
/>
<van-field
v-model="transportInfo.driverName"
label="司机姓名"
placeholder="请输入司机姓名"
/>
</div>
<!-- 备注 -->
<div class="remark-section">
<van-field
v-model="remark"
label="备注"
type="textarea"
placeholder="请输入发料备注"
rows="3"
autosize
/>
</div>
<!-- 底部按钮 -->
<div class="bottom-actions">
<van-button
type="primary"
block
:loading="submitting"
@click="handleSubmit"
>
确认发料
</van-button>
</div>
<!-- 运输方式选择器 -->
<van-popup v-model="showTransportPicker" position="bottom">
<van-picker
:columns="transportColumns"
@confirm="onTransportConfirm"
@cancel="showTransportPicker = false"
/>
</van-popup>
</div>
</template>
<script>
export default {
name: 'OutsourcingIssuePick',
data() {
return {
orderInfo: {
orderNo: 'OS202401001',
supplier: '委外供应商A',
productName: '委外产品A',
outsourcingQuantity: 500,
deliveryDate: '2024-01-25',
contactPerson: '张经理',
status: 0
},
materialList: [
{
materialCode: 'MAT001',
materialName: '原材料A',
specification: '100*50*20mm',
locationCode: 'A01-01-01',
stock: 500,
requiredQuantity: 300,
issuedQuantity: 0,
currentPick: 0
},
{
materialCode: 'MAT002',
materialName: '原材料B',
specification: '200*100*30mm',
locationCode: 'A01-02-01',
stock: 400,
requiredQuantity: 200,
issuedQuantity: 0,
currentPick: 0
}
],
supplierInfo: {
contactPerson: '',
contactPhone: '',
deliveryAddress: ''
},
transportInfo: {
transportMethod: '',
vehicleNo: '',
driverName: ''
},
remark: '',
submitting: false,
showTransportPicker: false,
transportColumns: [
'自提',
'物流配送',
'专车配送',
'快递'
]
}
},
mounted() {
this.loadOrderData()
},
methods: {
loadOrderData() {
const orderNo = this.$route.params.orderNo
console.log('加载委外订单数据:', orderNo)
},
onTransportConfirm(value) {
this.transportInfo.transportMethod = value
this.showTransportPicker = false
},
async handleSubmit() {
//
const hasPick = this.materialList.some(item => item.currentPick > 0)
if (!hasPick) {
this.$toast('请输入发料数量')
return
}
//
if (!this.supplierInfo.contactPerson) {
this.$toast('请输入供应商接收人')
return
}
//
if (!this.transportInfo.transportMethod) {
this.$toast('请选择运输方式')
return
}
//
const insufficientStock = this.materialList.find(item => item.currentPick > item.stock)
if (insufficientStock) {
this.$toast(`${insufficientStock.materialName} 库存不足`)
return
}
this.submitting = true
try {
await new Promise(resolve => setTimeout(resolve, 2000))
this.$toast.success('发料成功')
this.$router.back()
} catch (error) {
this.$toast.fail('发料失败')
} finally {
this.submitting = false
}
},
getStatusText(status) {
const statusMap = {
0: '待发料',
1: '部分发料',
2: '已完成'
}
return statusMap[status] || '未知'
}
}
}
</script>
<style scoped>
.pick-container {
min-height: 100vh;
background-color: #f7f8fa;
padding-bottom: 80px;
}
.order-info {
background: white;
margin-bottom: 10px;
}
.info-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #ebedf0;
}
.order-no {
font-size: 18px;
font-weight: bold;
color: #323233;
}
.order-status {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
color: white;
background-color: #ff976a;
}
.material-section,
.supplier-section,
.transport-section,
.remark-section {
background: white;
margin-bottom: 10px;
}
.section-title {
padding: 16px;
font-size: 16px;
font-weight: bold;
color: #323233;
border-bottom: 1px solid #ebedf0;
}
.material-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #ebedf0;
}
.material-item:last-child {
border-bottom: none;
}
.material-info {
flex: 1;
}
.material-name {
font-size: 16px;
font-weight: bold;
color: #323233;
margin-bottom: 4px;
}
.material-spec {
font-size: 12px;
color: #969799;
margin-bottom: 6px;
}
.material-location {
display: flex;
align-items: center;
margin-bottom: 4px;
}
.stock-info {
font-size: 12px;
color: #646566;
margin-left: 8px;
}
.material-quantity {
font-size: 14px;
color: #646566;
}
.pick-input {
margin-left: 16px;
}
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 16px;
background: white;
border-top: 1px solid #ebedf0;
}
</style>

165
src/views/modules/production-issue/production.vue

@ -0,0 +1,165 @@
<template>
<div class="pda-container">
<div class="header-bar">
<div class="header-left" @click="$router.back()">
<i class="el-icon-arrow-left"></i>
<span>生产领料</span>
</div>
<div class="header-right" @click="$router.push({ path: '/' })">
首页
</div>
</div>
<!-- 功能菜单 -->
<div class="menu-grid">
<div
class="menu-item"
v-for="(btn, index) in buttons"
:key="index"
:class="{ 'disabled': btn.disabled }"
@click="handleButtonClick(btn)"
>
<div class="menu-icon" :class="btn.iconClass">
<van-icon :name="btn.icon" size="24" />
</div>
<div class="menu-text">{{ btn.label }}</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
buttons: [
{ icon: 'scan', label: '直接领料', iconClass: 'purchase', to: 'productionPicking', disabled: true },
{ icon: 'records', label: '申请单领料', iconClass: 'qualified', to: 'productionPicking', disabled: false },
]
}
},
methods: {
handleButtonClick(btn) {
if (btn.disabled) {
this.$message.warning('正在开发中,敬请期待...');
} else {
this.$router.push(btn.to);
}
}
}
}
</script>
<style>
:root {
--columns: 3;
--button-size: calc(100vw / var(--columns) - 20px);
}
/* 头部栏 */
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 16px;
background: #17B3A3;
color: white;
height: 40px;
min-height: 40px;
max-height: 40px;
}
.header-left {
display: flex;
align-items: center;
cursor: pointer;
font-size: 16px;
font-weight: 500;
}
.header-left i {
margin-right: 8px;
font-size: 18px;
}
.header-right {
cursor: pointer;
font-size: 16px;
font-weight: 500;
}
.menu-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
padding: 20px;
justify-content: center; /* 水平居中 */
align-content: center; /* 垂直居中 */
width: 100%; /* 确保占满容器宽度 */
}
.menu-item {
background: white;
border-radius: 12px;
padding: 12px 6px;
text-align: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s;
cursor: pointer;
}
.menu-item:active {
transform: scale(0.95);
}
.menu-item.disabled {
opacity: 0.6;
position: relative;
}
.menu-item.disabled::after {
content: '开发中';
position: absolute;
top: 8px;
right: 8px;
background: #ff9500;
color: white;
font-size: 8px;
padding: 2px 4px;
border-radius: 8px;
font-weight: bold;
}
.menu-icon {
width: 38px;
height: 38px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 6px;
color: white;
}
.menu-icon.purchase {
background: linear-gradient(135deg, #17b3a3 0%, #1dc5ef 100%);
}
.menu-icon.inspection {
background: linear-gradient(135deg, #17b3a3 0%, #1dc5ef 100%);
}
.menu-icon.qualified {
background: linear-gradient(135deg, #17b3a3 0%, #1dc5ef 100%);
}
.menu-text {
font-size: 10px;
color: #333;
font-weight: bold; /* 加粗字体 */
white-space: nowrap; /* 防止文字换行 */
overflow: hidden;
text-overflow: ellipsis;
margin-top: 2px;
}
</style>

489
src/views/modules/production-issue/productionIssuePda.vue

@ -146,84 +146,41 @@
<!-- 申请单输入 -->
<div class="input-section">
<div class="input-group">
<label>申请单号</label>
<!-- <label>申请单号</label> -->
<div class="input-with-scan">
<input v-model="requestIssueForm.notifyNo" placeholder="请输入申请单号" @keyup.enter="loadRequestMaterials" />
<button @click="loadRequestMaterials" class="scan-btn">确认</button>
<input v-model="requestIssueForm.notifyNo" placeholder="请输入申请单号"
@keyup.enter="loadRequestMaterials(false)" />
<!-- <button @click="loadRequestMaterials" class="scan-btn">确认</button> -->
</div>
</div>
</div>
<!-- 申请单物料列表 -->
<div class="materials-section" v-if="requestMaterials.length">
<div class="material-list">
<!-- 扫描标签和发料 -->
<!-- <div class="scan-section">
<div class="section-header">
<h3>扫描物料标签</h3>
</div>
<div class="input-group">
<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>
<div class="input-with-scan">
<textarea v-model="requestIssueForm.remark" placeholder="可选" />
</div>
<!-- 申请发料单列表 -->
<div class="materials-section" v-if="requestList.length">
<!-- <div class="section-header">
<h3>申请发料单列表</h3>
</div> -->
<div class="material-list" @scroll.passive="handleRequestListScroll"
style="overflow-y:auto;max-height:70vh;">
<div v-for="request in requestList" :key="request.requestId" class="material-item"
@click="selectRequest(request)">
<div class="material-info">
<div class="part-no">申请单: {{ request.notifyNo }}</div>
<div class="part-desc">{{ request.requestDesc }}</div>
<div class="work-order">工单: {{ request.workOrderNo }}</div>
<div class="qty-info">
申请数量: {{ request.requestQty }} |
已发数量: {{ request.issuedQty }} |
剩余数量: {{ request.remainQty }}
</div>
<button @click="confirmRequestIssue" class="confirm-btn" :disabled="!issueQty">
确认发料 (同步IFS)
</button>
</div>
</div>
</div>
<div v-for="material in requestMaterials" :key="`${material.partNo}-${material.itemNo}`"
class="material-item" @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 class="material-status">
<span v-if="request.remainQty > 0" class="status-pending">待发料</span>
<span v-else class="status-complete">已完成</span>
</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 v-if="requestLoading" class="loading-text">加载中...</div>
<div v-if="!requestHasMore && requestList.length" class="loading-text">没有更多了</div>
</div>
</div>
</div>
@ -243,55 +200,66 @@
<!-- 扫描标签页面 -->
<div v-if="processFlag === 3" class="scan-label-page">
<div class="scan-content">
<div class="input-section">
<div class="label-info" v-if="scanLabelData.labelInfo">
<div class="info-row"><span class="label">申请单号:</span><span
class="value">{{ scanLabelData.labelInfo.notifyNo }}</span></div>
<div class="info-row"><span class="label">工单号:</span><span
class="value">{{ scanLabelData.labelInfo.workOrderNo }}</span></div>
<div class="info-row"><span class="label">可用数量:</span><span
class="value">{{ scanLabelData.labelInfo.availableQty }}</span></div>
</div>
<div class="input-section">
<button @click="confirmScanLabelIssue" class="confirm-btn">确认发料</button>
<div class="label-info" v-if="scanLabelData.labelInfo">
<div class="info-row"><span class="label">物料编码:</span><span
class="value">{{ scanLabelData.labelInfo.partNo }}</span></div>
<div class="info-row"><span class="label">批次号:</span><span
class="value">{{ scanLabelData.labelInfo.batchNo }}</span></div>
<div class="info-row"><span class="label">可用数量:</span><span
class="value">{{ scanLabelData.labelInfo.availableQty }}</span></div>
</div>
<!-- <div class="qty-input" v-if="scanLabelData.labelInfo">
<div class="input-group">
<label>发料数量</label>
<input v-model="scanLabelData.issueQty" type="number" :max="scanLabelData.labelInfo.availableQty"
placeholder="请输入发料数量" />
<button @click="showMaterialListDialog" class="scan-btn" style="margin-bottom: 10px;">查看物料列表</button>
<div class="input-with-scan">
<input v-model="scanTableInput" placeholder="请扫描材料编号" @keyup.enter="handleScanTableInput" />
<!-- <button @click="handleScanTableInput" class="scan-btn">添加</button> -->
</div>
</div>
<div class="input-group">
<label>备注</label>
<el-input type="textarea" v-model="scanLabelData.remark" placeholder="可选" />
<el-table :data="scanTableData" border highlight-current-row ref="mainTable" style="width: 100%; ">
<el-table-column v-for="(item,index) in columnList" :key="index" :sortable="item.columnSortable"
:prop="item.columnProp" :header-align="item.headerAlign"
:show-overflow-tooltip="item.showOverflowTooltip" :align="item.align"
:fixed="item.fixed==''?false:item.fixed" :min-width="item.columnWidth" :label="item.columnLabel">
<template slot-scope="scope">
<span v-if="!item.columnHidden"> {{ scope.row[item.columnProp] }}</span>
<span v-if="item.columnImage"><img :src="scope.row[item.columnProp]"
style="width: 100px; height: 80px" /></span>
</template>
</el-table-column>
<el-table-column fixed="left" header-align="center" align="center" width="60" label="操作">
<template slot-scope="scope">
<a type="text" size="small" @click="deleteScanTalbeData(scope.row)">删除</a>
</template>
</el-table-column>
</el-table>
</div>
</div> -->
<div class="input-group">
<div class="input-with-scan">
<input v-model="scanTableInput" placeholder="请扫描材料编号" @keyup.enter="handleScanTableInput" />
<button @click="handleScanTableInput" class="scan-btn">添加</button>
</div>
</div>
<div class="input-group">
<el-table :data="scanTableData" border highlight-current-row ref="mainTable" style="width: 100%; ">
<el-table-column v-for="(item,index) in columnList" :key="index" :sortable="item.columnSortable"
:prop="item.columnProp" :header-align="item.headerAlign"
:show-overflow-tooltip="item.showOverflowTooltip" :align="item.align"
:fixed="item.fixed==''?false:item.fixed" :min-width="item.columnWidth" :label="item.columnLabel">
<template slot-scope="scope">
<span v-if="!item.columnHidden"> {{ scope.row[item.columnProp] }}</span>
<span v-if="item.columnImage"><img :src="scope.row[item.columnProp]"
style="width: 100px; height: 80px" /></span>
</template>
</el-table-column>
</el-table>
<div class="message" v-if="scanLabelData.message" :class="scanLabelData.messageType">
{{ scanLabelData.message }}</div>
</div>
<div class="message" v-if="scanLabelData.message" :class="scanLabelData.messageType">
{{ scanLabelData.message }}</div>
</div>
<!-- 固定在底部的确认按钮 -->
<div class="scan-bottom-actions">
<button @click="confirmScanLabelIssue" class="confirm-btn">确认发料</button>
</div>
</div>
<!-- 材料列表弹窗 -->
<el-dialog title="物料列表" :visible.sync="materialListDialogVisible" :append-to-body='true' width="100%">
<el-table :data="materialListData" border highlight-current-row style="width: 100%;">
<el-table-column prop="partNo" label="材料编号"></el-table-column>
<el-table-column prop="batchNo" label="批号"></el-table-column>
<el-table-column prop="qty" label="数量" align="right" header-align="left"></el-table-column>
<el-table-column prop="location" label="已扫数量"></el-table-column>
</el-table>
<span slot="footer" class="dialog-footer">
<el-button @click="materialListDialogVisible = false">关闭</el-button>
</span>
</el-dialog>
</div>
</div>
</template>
@ -308,6 +276,7 @@ import {
printPalletLabel,
getPalletInfo,
scanMaterialLabel,
getIssureNotifyByNo,
} from '@/api/production/production-issue'
export default {
@ -347,8 +316,12 @@ export default {
remark: '',
selectedMaterials: [],
},
requestMaterials: [],
selectedRequestMaterial: null,
requestList: [], //
selectedRequest: null, //
requestPageNum: 1,
requestPageSize: 5,
requestHasMore: true,
requestLoading: false,
//
palletForm: {
@ -380,7 +353,7 @@ export default {
columnProp: 'partNo',
headerAlign: 'center',
align: 'center',
columnLabel: '材料单号',
columnLabel: '物料号',
columnHidden: false,
columnImage: false,
columnSortable: false,
@ -398,14 +371,14 @@ export default {
columnProp: 'desc',
headerAlign: 'center',
align: 'center',
columnLabel: '材料描述',
columnLabel: '批号',
columnHidden: false,
columnImage: false,
columnSortable: false,
sortLv: 0,
status: true,
fixed: '',
columnWidth: 100,
columnWidth: 50,
},
{
userId: this.$store.state.user.name,
@ -423,7 +396,31 @@ export default {
sortLv: 0,
status: true,
fixed: '',
columnWidth: 100,
columnWidth: 60,
},
],
materialListDialogVisible: false,
materialListData: [
{
partNo: 'MAT001',
partDesc: '材料描述1',
batchNo: 'BATCH001',
qty: 100,
location: 'A-01-01',
},
{
partNo: 'MAT002',
partDesc: '材料描述2',
batchNo: 'BATCH002',
qty: 50,
location: 'A-01-02',
},
{
partNo: 'MAT003',
partDesc: '材料描述3',
batchNo: 'BATCH003',
qty: 200,
location: 'B-02-01',
},
],
}
@ -442,6 +439,10 @@ export default {
if (this.processFlag === 3) {
this.processFlag = 2
this.showScanLabel = false
//
this.requestPageNum = 1
this.requestHasMore = true
this.loadRequestMaterials(false)
} else if (this.processFlag === 2) {
this.processFlag = 1
this.selectedFunction = null
@ -480,27 +481,19 @@ export default {
this.showScanLabel = true
this.processFlag = 3
},
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
selectRequest(request) {
this.selectedRequest = request
this.scanLabelContext = {
type: 'request',
notifyNo: this.requestIssueForm.notifyNo,
partNo: material.partNo,
material,
notifyNo: request.notifyNo,
workOrderNo: request.workOrderNo,
request,
}
this.scanLabelData = {
scannedLabel: '',
labelInfo: {
partNo: this.partNo,
batchNo: 'BATCH001',
notifyNo: request.notifyNo,
workOrderNo: request.workOrderNo,
availableQty: 100,
},
issueQty: null,
@ -516,14 +509,17 @@ export default {
},
resetAll() {
this.workOrderMaterials = []
this.requestMaterials = []
this.requestList = []
this.selectedMaterial = null
this.selectedRequestMaterial = null
this.selectedRequest = null
this.currentPallet = {}
this.scannedLabel = ''
this.labelInfo = null
this.issueQty = null
this.message = ''
this.requestPageNum = 1
this.requestHasMore = true
this.requestList = []
},
//
@ -600,101 +596,75 @@ export default {
},
//
async loadRequestMaterials() {
/* if (!this.requestIssueForm.notifyNo) {
async loadRequestMaterials(isLoadMore) {
if (!this.requestIssueForm.notifyNo) {
this.showMessage('请输入申请单号', 'error')
return
}
this.loading = true
this.loadingText = '加载申请单物料...'
if (this.requestLoading) return
this.requestLoading = true
try {
const response = await getRequestMaterials({
const response = await getIssureNotifyByNo({
site: this.requestIssueForm.site,
notifyNo: this.requestIssueForm.notifyNo,
pageNum: this.requestPageNum,
pageSize: this.requestPageSize,
})
if (response.data.code === 0) {
this.requestMaterials = response.data.materials || []
if (this.requestMaterials.length === 0) {
this.showMessage('该申请单没有物料需求', 'warning')
const records = response.data.page.records || []
console.log(isLoadMore)
if (isLoadMore) {
this.requestList = this.requestList.concat(records)
} else {
this.requestList = []
this.requestList = records
}
this.requestHasMore = records.length === this.requestPageSize
if (!isLoadMore && this.requestList.length === 0) {
this.showMessage('该申请单没有发料需求', 'warning')
}
} else {
this.showMessage(response.data.msg, 'error')
}
} catch (error) {
this.showMessage('加载申请单物料失败', 'error')
this.showMessage('加载申请发料单失败', 'error')
} finally {
this.loading = false
} */
let materials = [
{
partNo: 'PART001',
partDesc: '物料描述1',
workOrderNo: 'WO001',
requestQty: 100,
issuedQty: 50,
remainQty: 50,
itemNo: 'ITEM001',
},
{
partNo: 'PART002',
partDesc: '物料描述2',
workOrderNo: 'WO002',
requestQty: 200,
issuedQty: 100,
remainQty: 100,
itemNo: 'ITEM002',
},
]
//this.requestMaterials = materials
this.$set(this, 'requestMaterials', materials)
},
async confirmRequestIssue() {
if (!this.issueQty || this.issueQty <= 0) {
this.showMessage('请输入有效的发料数量', 'error')
return
this.requestLoading = false
}
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')
},
handleRequestListScroll(e) {
const el = e.target
if (el.scrollTop + el.clientHeight >= el.scrollHeight - 10) {
if (this.requestHasMore && !this.requestLoading) {
this.requestPageNum += 1
this.loadRequestMaterials(true)
}
} catch (error) {
this.showMessage('发料失败', 'error')
} finally {
this.loading = false
}
},
//
selectRequest(request) {
this.selectedRequest = request
this.scanLabelContext = {
type: 'request',
notifyNo: request.notifyNo,
workOrderNo: request.workOrderNo,
request,
}
this.scanLabelData = {
scannedLabel: '',
labelInfo: {
notifyNo: request.notifyNo,
workOrderNo: request.workOrderNo,
availableQty: 100,
},
issueQty: null,
remark: '',
message: '',
messageType: 'info',
}
this.showScanLabel = true
this.processFlag = 3
},
//
async createPallet() {
@ -849,11 +819,6 @@ export default {
},
async confirmScanLabelIssue() {
if (!this.scanLabelData.issueQty || this.scanLabelData.issueQty <= 0) {
this.scanLabelData.message = '请输入有效的发料数量'
this.scanLabelData.messageType = 'error'
return
}
this.loading = true
this.scanLabelData.message = ''
try {
@ -900,7 +865,7 @@ export default {
if (response.data.code === 0) {
this.scanLabelData.message = '发料成功'
this.scanLabelData.messageType = 'success'
await this.loadRequestMaterials()
await this.loadRequestMaterials(false)
setTimeout(() => {
this.showScanLabel = false
this.processFlag = 2
@ -923,25 +888,38 @@ export default {
let params = {
scanTableInput: this.scanTableInput,
}
scanMaterialLabel(params).then(({data}) => {
if (data.code === 0) {
let material = {
partNo: data.labelInfo.partNo ,
desc: data.labelInfo.partDesc || '材料描述-' + this.scanTableInput,
qty: 1, // 1
scanMaterialLabel(params)
.then(({ data }) => {
if (data.code === 0) {
let material = {
partNo: data.labelInfo.partNo,
desc:
data.labelInfo.partDesc || '材料描述-' + this.scanTableInput,
qty: 1, // 1
}
this.scanTableData.push(material)
this.scanTableInput = ''
} else {
this.$message.error(data.msg || '扫描失败')
return
}
this.scanTableData.push(material)
this.scanTableInput = ''
} else {
this.$message.error(data.msg || '扫描失败')
})
.catch((error) => {
this.$message.error('扫描异常: ' + error.message)
return
}
}).catch(error => {
this.$message.error('扫描异常: ' + error.message)
return
})
})
},
showMaterialListDialog() {
this.materialListDialogVisible = true
},
selectMaterialFromList(material) {
this.scanTableInput = material.partNo
this.materialListDialogVisible = false
this.handleScanTableInput()
},
deleteScanTalbeData(row) {
this.scanTableData = this.scanTableData.filter(item => item.partNo !== row.partNo)
this.$message.success('删除成功')
},
},
}
@ -1057,6 +1035,18 @@ export default {
font-size: 16px;
}
/* 申请单号输入框绿色边框 */
.request-issue .input-with-scan input {
border: 2px solid #17b3a3;
border-radius: 6px;
}
.request-issue .input-with-scan input:focus {
border-color: #13998c;
outline: none;
box-shadow: 0 0 0 2px rgba(23, 179, 163, 0.2);
}
.scan-btn,
.confirm-btn,
.print-btn {
@ -1332,4 +1322,41 @@ export default {
.confirm-btn {
width: 100%;
}
/* 扫描标签页面固定底部按钮样式 */
.scan-label-page {
display: flex;
flex-direction: column;
height: 100vh;
position: relative;
}
.scan-content {
flex: 1;
overflow-y: auto;
padding: 10px;
padding-bottom: 80px; /* 为底部按钮留出空间 */
}
.scan-bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
padding: 15px;
border-top: 1px solid #eee;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
z-index: 100;
}
.materials-section .material-list .material-item {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.18);
padding: 18px 20px;
margin-bottom: 1px;
display: flex;
flex-direction: column;
gap: 8px;
}
</style>

399
src/views/modules/production-issue/productionPicking.vue

@ -0,0 +1,399 @@
<template>
<div class="pda-container">
<!-- 头部栏 -->
<div class="header-bar">
<div class="header-left" @click="$router.back()">
<i class="el-icon-arrow-left"></i>
<span>生产领料</span>
</div>
<div class="header-right" @click="$router.push({ path: '/' })">
首页
</div>
</div>
<!-- 搜索框 -->
<div class="search-container">
<el-input clearable
v-model="searchCode"
placeholder="请扫描出库单或关联单号"
prefix-icon="el-icon-search"
@keyup.enter.native="handleSearch"
ref="searchInput"
/>
</div>
<!-- 出库单列表 -->
<div class="content-area">
<div
v-for="(item, index) in outboundList"
:key="index"
class="outbound-card"
@click="goToPickingPage(item)"
>
<div class="card-title">
<span class="title-label">出库单号</span>
<span class="title-value">{{ item.notifyNo }}</span>
</div>
<div class="card-details">
<div class="detail-item">
<div class="detail-label">关联单号</div>
<div class="detail-value">{{ item.workOrderNo }}</div>
</div>
<div class="detail-item">
<div class="detail-label">标签张数</div>
<div class="detail-value">
<span class="qualified">{{ item.pickedLabels }}</span><span class="total">{{ item.totalLabels }}</span>
</div>
</div>
<div class="detail-item">
<div class="detail-label">物料总数</div>
<div class="detail-value">
<span class="qualified">{{ item.requestQty }}</span><span class="total">{{ item.remainQty }}</span>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="outboundList.length === 0 && !loading" class="empty-state">
<i class="el-icon-box"></i>
<p>暂无待领料出库单</p>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-state">
<i class="el-icon-loading"></i>
<p>加载中...</p>
</div>
</div>
</div>
</template>
<script>
import {
getIssureNotifyByNo,
getIssureNotifyListByNo,
} from '@/api/production/production-issue'
import { getCurrentWarehouse } from '@/utils'
import moment from 'moment';
export default {
data() {
return {
searchCode: '',
outboundList: [],
loading: false
};
},
methods: {
formatDate(date) {
return date ? moment(date).format('YYYY-MM-DD') : '';
},
//
handleSearch() {
if (this.searchCode.trim()) {
this.searchOutboundList(this.searchCode.trim());
} else {
this.loadOutboundList();
}
},
//
loadOutboundList() {
/* const currentWarehouse = getCurrentWarehouse();
if (!currentWarehouse) {
this.$message.error('请先选择仓库');
return;
} */
this.loading = true;
const params = {
site: this.$store.state.user.site,
status: '待出库',
}
console.log('params', params);
getIssureNotifyListByNo(params).then(({ data }) => {
this.loading = false;
if (data && data.code === 0) {
this.outboundList = data.list || [];
} else {
this.$message.error(data.msg || '获取数据失败');
}
}).catch(error => {
this.loading = false;
console.error('获取生产出库单列表失败:', error);
this.$message.error('获取数据失败');
});
},
//
searchOutboundList(searchCode) {
/* const currentWarehouse = getCurrentWarehouse();
if (!currentWarehouse) {
this.$message.error('请先选择仓库');
return;
} */
this.loading = true;
const params = {
notifyNo: searchCode,
site: this.$store.state.user.site,
status: 'ISSUE'
};
getIssureNotifyListByNo(params).then(({ data }) => {
this.loading = false;
if (data && data.code === 0) {
if (data.list.length === 0) {
this.$message.warning('未找到匹配的出库单');
}
this.outboundList = data.list || [];
} else {
this.$message.error(data.msg || '查询失败');
}
}).catch(error => {
this.loading = false;
this.$message.error('查询失败');
});
},
//
goToPickingPage(item) {
this.$router.push({
name: 'productionPickingDetail',
params: {
outboundNo: item.notifyNo,
}
});
}
},
mounted() {
//
this.$nextTick(() => {
if (this.$refs.searchInput) {
this.$refs.searchInput.focus();
}
});
//
this.loadOutboundList();
}
};
</script>
<style scoped>
.pda-container {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
background: #f5f5f5;
}
/* 头部栏 */
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 16px;
background: #17B3A3;
color: white;
height: 40px;
min-height: 40px;
}
.header-left {
display: flex;
align-items: center;
cursor: pointer;
font-size: 16px;
font-weight: 500;
}
.header-left i {
margin-right: 8px;
font-size: 18px;
}
.header-right {
cursor: pointer;
font-size: 16px;
font-weight: 500;
}
/* 搜索容器 */
.search-container {
padding: 12px 16px;
background: white;
}
/* 内容区域 */
.content-area {
flex: 1;
overflow-y: auto;
padding: 12px 16px;
}
/* 出库卡片 */
.outbound-card {
background: white;
border-radius: 8px;
margin-bottom: 12px;
padding: 16px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: all 0.2s ease;
}
.outbound-card:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
transform: translateY(-1px);
}
.outbound-card:active {
transform: translateY(0);
}
/* 卡片标题 */
.card-title {
margin-bottom: 12px;
}
.title-label {
font-size: 12px;
color: #666;
display: block;
margin-bottom: 4px;
}
.title-value {
font-size: 16px;
font-weight: bold;
color: #333;
margin-left: 20px;
}
/* 卡片详情 */
.card-details {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 4px;
}
.detail-item {
flex: 1;
text-align: center;
min-width: 60px;
max-width: 60px;
}
.detail-label {
font-size: 11px;
color: #666;
margin-bottom: 4px;
line-height: 1.2;
margin-left: -12px;
}
.detail-value {
font-size: 13px;
color: #333;
line-height: 1.2;
margin-left: -12px;
}
.detail-value .qualified {
color: #17B3A3;
font-weight: 500;
}
.detail-value .total {
color: #333;
font-weight: 500;
}
.detail-value .total::before {
content: '/';
color: #333;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #999;
}
.empty-state i {
font-size: 48px;
margin-bottom: 16px;
}
.empty-state p {
font-size: 14px;
margin: 0;
}
/* 加载状态 */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #17B3A3;
}
.loading-state i {
font-size: 24px;
margin-bottom: 12px;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.loading-state p {
font-size: 14px;
margin: 0;
}
/* 响应式设计 */
@media (max-width: 360px) {
.header-bar {
padding: 8px 12px;
}
.search-container {
padding: 8px 12px;
}
.content-area {
padding: 8px 12px;
}
.outbound-card {
padding: 12px;
}
.card-details {
flex-wrap: wrap;
gap: 6px;
}
.detail-item {
flex: 0 0 48%;
margin-bottom: 6px;
min-width: 50px;
}
}
</style>

984
src/views/modules/production-issue/productionPickingDetail.vue

@ -0,0 +1,984 @@
<template>
<div class="pda-container">
<!-- 头部栏 -->
<div class="header-bar">
<div class="header-left" @click="$router.back()">
<i class="el-icon-arrow-left"></i>
<span>生产领料</span>
</div>
<div class="header-right" @click="$router.push({ path: '/' })">
首页
</div>
</div>
<!-- 搜索框 -->
<div class="search-container">
<el-input clearable class="compact-input"
v-model="scanCode"
placeholder="请扫描标签条码"
prefix-icon="el-icon-search"
@keyup.enter.native="handleScan"
ref="scanInput"
/>
<div class="mode-switch">
<el-switch
class="custom-switch"
v-model="isRemoveMode"
active-color="#ff4949"
inactive-color="#13ce66">
</el-switch>
<span v-if="isRemoveMode" class="switch-text">{{ '移除' }}</span>
<span v-else class="switch-text2">{{ '添加' }}</span>
</div>
</div>
<!-- 出库单信息卡片 -->
<div class="material-info-card" v-if="outboundInfo.outboundNo">
<div class="card-title">
<span class="title-label">出库单号</span>
<span class="title-value">{{ outboundInfo.outboundNo }}</span>
</div>
<div class="card-details">
<div class="detail-item">
<div class="detail-label">关联单号</div>
<div class="detail-value">{{ outboundInfo.relatedNo }}</div>
</div>
<div class="detail-item">
<div class="detail-label">标签张数</div>
<div class="detail-value">
<span class="qualified">{{ outboundInfo.pickedLabels }}</span><span class="total">{{ outboundInfo.totalLabels }}</span>
</div>
</div>
<div class="detail-item">
<div class="detail-label">物料总数</div>
<div class="detail-value">
<span class="qualified">{{ outboundInfo.pickedQty }}</span><span class="total">{{ outboundInfo.totalQty }}</span>
</div>
</div>
</div>
</div>
<!-- 出库信息确认标题 -->
<div class="section-title">
<div class="title-left">
<i class="el-icon-circle-check"></i>
<span>出库信息确认</span>
</div>
<div class="title-right">
<span class="material-list-link" @click="showMaterialListDialog">物料清单</span>
</div>
</div>
<!-- 标签列表 -->
<div class="label-list">
<div class="list-header">
<div class="col-no">NO.</div>
<div class="col-label">标签条码</div>
<div class="col-part">物料编码</div>
<div class="col-unit">单位</div>
<div class="col-qty">标签数量</div>
</div>
<div
v-for="(label, index) in labelList"
:key="label.id"
class="list-item"
>
<div class="col-no">{{ labelList.length - index }}</div>
<div class="col-label">{{ label.labelCode }}</div>
<div class="col-part">{{ label.partNo }}</div>
<div class="col-unit">{{ label.unit || '个' }}</div>
<div class="col-qty">{{ label.quantity }}</div>
</div>
<!-- 空状态 -->
<div v-if="labelList.length === 0" class="empty-labels">
<p>暂无扫描标签</p>
</div>
</div>
<!-- 底部操作按钮 -->
<div class="bottom-actions">
<button class="action-btn secondary" @click="confirmOutbound">
确定
</button>
<button class="action-btn secondary" style="margin-left: 10px;" @click="printLabels">
打印
</button>
<button class="action-btn secondary" style="margin-left: 10px;" @click="cancelOutbound">
取消
</button>
</div>
<!-- 物料清单弹窗 -->
<div v-if="showMaterialDialog" class="material-overlay">
<div class="material-modal">
<div class="modal-header">
<span class="modal-title">物料清单</span>
<i class="el-icon-close close-btn" @click="closeMaterialDialog"></i>
</div>
<div class="modal-body">
<!-- 加载状态 -->
<div v-if="materialListLoading" class="loading-container">
<i class="el-icon-loading"></i>
<span>加载中...</span>
</div>
<!-- 物料表格 -->
<div v-else-if="materialList.length > 0" class="material-table">
<div class="table-header">
<div class="col-no">NO.</div>
<div class="col-material-code">物料编码</div>
<div class="col-required-qty">需求数量</div>
<div class="col-picked-qty">已领数量</div>
</div>
<div class="table-body">
<div
v-for="(item, index) in materialList"
:key="index"
class="table-row"
>
<div class="col-no">{{ index + 1 }}</div>
<div class="col-material-code">{{ item.materialCode || item.partNo }}</div>
<div class="col-required-qty">{{ item.requiredQty || 0 }}</div>
<div class="col-picked-qty">{{ item.pickedQty || 0 }}</div>
</div>
</div>
</div>
<!-- 空数据状态 -->
<div v-else class="empty-material">
<i class="el-icon-document"></i>
<p>暂无物料数据</p>
</div>
</div>
<div class="modal-footer">
<button class="btn-close" @click="closeMaterialDialog">关闭</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { scanMaterialLabel,getRequestMaterials } from '@/api/production/production-issue';
import moment from 'moment';
import { notify } from 'node-notifier';
export default {
data() {
return {
scanCode: '',
outboundInfo: {},
labelList: [],
outboundNo: '',
buNo: '',
showMaterialDialog: false,
materialList: [],
materialListLoading: false,
isRemoveMode: false //
};
},
methods: {
formatDate(date) {
return date ? moment(date).format('YYYY-MM-DD') : '';
},
//
handleScan() {
if (!this.scanCode.trim()) {
return;
}
if (this.isRemoveMode) {
this.removeLabelByCode(this.scanCode.trim());
} else {
this.validateAndAddLabel(this.scanCode.trim());
}
this.scanCode = '';
},
//
validateAndAddLabel(labelCode) {
const params = {
labelCode: labelCode,
notifyNo: this.outboundNo,
site: this.$store.state.user.site,
};
scanMaterialLabel(params).then(({ data }) => {
if (data && data.code === 0) {
//
const exists = this.labelList.find(item => item.labelCode === labelCode);
if (exists) {
this.$message.warning('该标签已扫描,请勿重复扫描');
return;
}
//
this.labelList.push({
id: Date.now(),
labelCode: labelCode,
partNo: data.labelInfo.partNo,
quantity: data.labelInfo.quantity,
unit: data.labelInfo.unit,
batchNo: data.labelInfo.batchNo
});
this.$message.success('操作成功');
} else {
this.$message.error(data.msg || '该标签与出库单不符,请检查');
}
}).catch(error => {
this.$message.error('操作失败');
});
},
//
removeLabelByCode(labelCode) {
const index = this.labelList.findIndex(item => item.labelCode === labelCode);
if (index !== -1) {
this.labelList.splice(index, 1);
this.$message.success('操作成功');
} else {
this.$message.warning('未找到该标签');
}
},
//
confirmOutbound() {
if (this.labelList.length === 0) {
this.$message.warning('请先扫描标签');
return;
}
const params = {
site: this.outboundInfo.site,
outboundNo: this.outboundNo,
labels: this.labelList.map(label => ({
labelCode: label.labelCode,
quantity: label.quantity,
batchNo: label.batchNo,
partNo: label.partNo
}))
};
confirmProductionPicking(params).then(({ data }) => {
if (data && data.code === 0) {
this.$message.success('操作成功');
this.$router.back();
} else {
this.$message.error(data.msg || '操作失败');
}
}).catch(error => {
console.error('出库确认失败:', error);
this.$message.error('操作失败');
});
},
//
printLabels() {
if (this.labelList.length === 0) {
this.$message.warning('暂无标签可打印');
return;
}
this.$message.warning('打印功能开发中...');
},
//
cancelOutbound() {
if (this.labelList.length > 0) {
this.$confirm('取消后将清空已扫描的标签,确定取消吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '继续操作',
type: 'warning'
}).then(() => {
this.$router.back();
}).catch(() => {
//
});
} else {
this.$router.back();
}
},
//
showMaterialListDialog() {
this.showMaterialDialog = true;
this.loadMaterialList();
},
//
loadMaterialList() {
console.log('加载物料清单', this.outboundInfo, this.outboundNo);
if (!this.$store.state.user.site || !this.outboundNo) {
this.$message.error('缺少必要参数,无法获取物料清单');
return;
}
this.materialListLoading = true;
const params = {
site: this.$store.state.user.site,
outboundNo: this.outboundNo
};
getRequestMaterials(params).then(({ data }) => {
this.materialListLoading = false;
if (data && data.code === 0) {
this.materialList = data.materials || [];
} else {
this.$message.error(data.msg || '获取物料清单失败');
this.materialList = [];
}
}).catch(error => {
this.materialListLoading = false;
this.$message.error('获取物料清单失败');
this.materialList = [];
});
},
//
closeMaterialDialog() {
this.showMaterialDialog = false;
},
//
loadOutboundDetails() {
const params = {
outboundNo: this.outboundNo,
site: this.$store.state.user.site,
};
console.log('加载出库单详情参数:', params);
/* getOutboundDetails(params).then(({ data }) => {
if (data && data.code === 0) {
this.outboundInfo = data.data;
} else {
this.$message.error(data.msg || '获取出库单详情失败');
}
}).catch(error => {
console.error('获取出库单详情失败:', error);
this.$message.error('获取出库单详情失败');
}); */
}
},
mounted() {
//
this.outboundNo = this.$route.params.outboundNo;
console.log("11111",this.outboundNo);
if (!this.outboundNo) {
this.$message.error('参数错误');
this.$router.back();
return;
}
//
this.$nextTick(() => {
if (this.$refs.scanInput) {
this.$refs.scanInput.focus();
}
});
//
this.loadOutboundDetails();
}
};
</script>
<style scoped>
/* 复用入库页面的样式,只修改必要的部分 */
.pda-container {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
background: #f5f5f5;
}
/* 头部栏 */
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 16px;
background: #17B3A3;
color: white;
height: 40px;
min-height: 40px;
}
.header-left {
display: flex;
align-items: center;
cursor: pointer;
font-size: 16px;
font-weight: 500;
}
.header-left i {
margin-right: 8px;
font-size: 18px;
}
.header-right {
cursor: pointer;
font-size: 16px;
font-weight: 500;
}
/* 搜索容器 */
.search-container {
padding: 12px 16px;
background: white;
display: flex;
align-items: center;
gap: 12px;
}
.search-container .el-input {
width: 240px;
margin-right: 12px;
}
/* 紧凑型输入框样式 */
.compact-input ::v-deep .el-input__inner {
height: 36px;
padding: 0 12px 0 35px;
font-size: 14px;
}
.compact-input ::v-deep .el-input__prefix {
left: 10px;
}
.compact-input ::v-deep .el-input__suffix {
right: 30px;
}
/* 模式切换开关 */
.mode-switch {
position: relative;
display: inline-block;
}
.custom-switch {
transform: scale(1.3);
}
/* 中间文字 */
.switch-text {
position: absolute;
left: 25%;
transform: translateX(-50%);
top: 50%;
transform: translateY(-50%) translateX(-50%);
font-size: 12px;
font-weight: 500;
color: #606266;
white-space: nowrap;
pointer-events: none;
z-index: 1;
top: 53%;
transform: translate(-50%, -50%);
font-size: 12px;
font-weight: bold;
color: white;
pointer-events: none;
z-index: 2;
}
.switch-text2 {
position: absolute;
left: 75%;
transform: translateX(-50%);
top: 50%;
transform: translateY(-50%) translateX(-50%);
font-size: 12px;
font-weight: 500;
color: #606266;
white-space: nowrap;
pointer-events: none;
z-index: 1;
top: 53%;
transform: translate(-50%, -50%);
font-size: 12px;
font-weight: bold;
color: white;
pointer-events: none;
z-index: 2;
}
/* 调整 switch 尺寸以便容纳文字 */
.custom-switch ::v-deep .el-switch__core {
width: 60px;
height: 28px;
}
/* 物料信息卡片 */
.material-info-card {
background: white;
margin: 4px 16px;
padding: 6px 20px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border: 1px solid #f0f0f0;
}
.card-title {
margin-bottom: 16px;
}
.title-label {
font-size: 11px;
color: #999;
display: block;
margin-bottom: 6px;
font-weight: normal;
}
.title-value {
font-size: 18px;
font-weight: bold;
color: #333;
line-height: 1.2;
margin-left: 20px;
}
.card-details {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 4px;
}
.detail-item {
flex: 1;
text-align: center;
min-width: 60px;
max-width: 60px;
}
.detail-label {
font-size: 11px;
color: #999;
margin-bottom: 6px;
font-weight: normal;
line-height: 1.2;
margin-left: -12px;
}
.detail-value {
font-size: 13px;
color: #333;
font-weight: 500;
line-height: 1.2;
margin-left: -12px;
}
.detail-value .qualified {
color: #17B3A3;
font-weight: 500;
}
.detail-value .total {
color: #333;
font-weight: 500;
}
.detail-value .total::before {
content: '/';
color: #333;
}
/* 区域标题 */
.section-title {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 8px;
background: white;
margin: 0 16px;
margin-top: 4px;
border-radius: 8px 8px 0 0;
border-bottom: 2px solid #17B3A3;
}
.title-left {
display: flex;
align-items: center;
}
.title-left i {
color: #17B3A3;
font-size: 16px;
margin-right: 8px;
}
.title-left span {
color: #17B3A3;
font-size: 14px;
font-weight: 500;
}
.title-right {
display: flex;
align-items: center;
}
.material-list-link {
color: #17B3A3;
font-size: 14px;
font-weight: 500;
cursor: pointer;
text-decoration: underline;
transition: color 0.2s ease;
}
.material-list-link:hover {
color: #0d8f7f;
}
/* 标签列表 */
.label-list {
background: white;
margin: 0 16px 12px;
border-radius: 0 0 8px 8px;
overflow: hidden;
}
.list-header {
display: flex;
background: #f8f9fa;
padding: 12px 8px;
border-bottom: 1px solid #e0e0e0;
font-size: 12px;
color: #666;
font-weight: 500;
}
.list-item {
display: flex;
padding: 12px 8px;
border-bottom: 1px solid #f0f0f0;
font-size: 12px;
color: #333;
}
.list-item:last-child {
border-bottom: none;
}
.col-no {
width: 20px;
text-align: center;
}
.col-label {
flex: 2;
text-align: center;
}
.col-part {
flex: 2;
text-align: center;
}
.col-unit {
width: 40px;
text-align: center;
}
.col-qty {
width: 60px;
text-align: center;
}
.empty-labels {
padding: 40px 20px;
text-align: center;
color: #999;
}
.empty-labels p {
margin: 0;
font-size: 14px;
}
/* 底部操作按钮 */
.bottom-actions {
display: flex;
padding: 16px;
gap: 20px;
background: white;
margin-top: auto;
}
.action-btn {
flex: 1;
padding: 12px;
border: 1px solid #17B3A3;
background: white;
color: #17B3A3;
border-radius: 20px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
.action-btn:hover {
background: #17B3A3;
color: white;
}
.action-btn:active {
transform: scale(0.98);
}
/* 物料清单弹窗样式 */
.material-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.material-modal {
background: white;
border-radius: 12px;
width: 100%;
max-width: 800px;
max-height: 80vh;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
overflow: hidden;
display: flex;
flex-direction: column;
}
.material-modal .modal-header {
background: #17B3A3;
color: white;
padding: 5px 16px;
display: flex;
justify-content: space-between;
align-items: center;
min-height: 28px;
}
.close-btn {
font-size: 16px;
cursor: pointer;
color: white;
transition: color 0.2s ease;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
}
.close-btn:hover {
color: #e0e0e0;
}
.material-modal .modal-title {
font-size: 16px;
font-weight: 500;
margin: 0;
line-height: 1.2;
}
.material-modal .modal-body {
flex: 1;
overflow: auto;
padding: 0;
}
.material-table {
width: 100%;
}
.table-header {
display: flex;
background: #f8f9fa;
padding: 10px 6px;
border-bottom: 2px solid #17B3A3;
font-size: 12px;
color: #333;
font-weight: 600;
position: sticky;
top: 0;
z-index: 1;
}
.table-body {
max-height: 400px;
overflow-y: auto;
}
.table-row {
display: flex;
padding: 10px 6px;
border-bottom: 1px solid #f0f0f0;
font-size: 12px;
color: #333;
transition: background-color 0.2s ease;
}
.table-row:hover {
background-color: #f8f9fa;
}
.table-row:last-child {
border-bottom: none;
}
.material-table .col-no {
width: 25px;
text-align: center;
flex-shrink: 0;
font-size: 12px;
}
.material-table .col-material-code {
flex: 1.8;
text-align: center;
min-width: 100px;
font-size: 12px;
word-break: break-all;
}
.material-table .col-required-qty {
flex: 0.8;
text-align: center;
min-width: 65px;
font-size: 12px;
}
.material-table .col-picked-qty {
flex: 0.8;
text-align: center;
min-width: 65px;
font-size: 12px;
}
.material-modal .modal-footer {
padding: 15px 20px;
display: flex;
justify-content: center;
border-top: 1px solid #f0f0f0;
}
.btn-close {
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
border: 1px solid #17B3A3;
background: white;
color: #17B3A3;
}
.btn-close:hover {
background: #17B3A3;
color: white;
}
/* 加载状态样式 */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #666;
}
.loading-container i {
font-size: 24px;
margin-bottom: 12px;
color: #17B3A3;
}
.loading-container span {
font-size: 14px;
}
/* 空数据状态样式 */
.empty-material {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #999;
}
.empty-material i {
font-size: 48px;
margin-bottom: 16px;
color: #ddd;
}
.empty-material p {
margin: 0;
font-size: 14px;
}
/* 响应式设计 */
@media (max-width: 360px) {
.header-bar {
padding: 8px 12px;
}
.search-container {
padding: 8px 12px;
}
.material-info-card {
margin: 4px 12px;
padding: 6px 16px;
}
.section-title {
margin: 0 12px;
margin-top: 4px;
}
.label-list {
margin: 0 12px 8px;
}
.card-details {
flex-wrap: wrap;
gap: 6px;
}
.detail-item {
flex: 0 0 48%;
margin-bottom: 6px;
min-width: 50px;
}
.list-header, .list-item {
font-size: 11px;
}
.col-label, .col-part {
flex: 1.5;
}
}
</style>
Loading…
Cancel
Save