feat: MOM 扫码入库对接 Track 产品身份证自动填充
- 半成品/成品入库弹窗顶部新增'扫码入库'按钮,打开扫码面板(扫码枪/摄像头) - 扫码 Track 身份证后自动填充物料信息与序列号,不再二次搜索 - 后端新增 track_query_service(httpx 调 Track lookup)、track-lookup 接口 - 入库成功后 notify_track 通知 Track 闭环
This commit is contained in:
@ -84,4 +84,13 @@ export function calculateBomCost(params: {bom_code: string, bom_version: string}
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// [新增] Track 扫码入库查询:根据身份证查询产品信息并匹配物料
|
||||
export function lookupTrackProduct(code: string) {
|
||||
return request({
|
||||
url: '/inbound/product/track-lookup',
|
||||
method: 'get',
|
||||
params: { code }
|
||||
})
|
||||
}
|
||||
@ -86,4 +86,13 @@ export function calculateBomCost(params: {bom_code: string, bom_version: string}
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// [新增] Track 扫码入库查询:根据身份证查询产品信息并匹配物料
|
||||
export function lookupTrackProduct(code: string) {
|
||||
return request({
|
||||
url: '/inbound/semi/track-lookup',
|
||||
method: 'get',
|
||||
params: { code }
|
||||
})
|
||||
}
|
||||
205
inventory-web/src/components/TrackScanDialog.vue
Normal file
205
inventory-web/src/components/TrackScanDialog.vue
Normal file
@ -0,0 +1,205 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="扫码入库"
|
||||
width="min(520px, 92vw)"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
append-to-body
|
||||
@opened="onOpened"
|
||||
@closed="onClosed"
|
||||
>
|
||||
<div class="track-scan-panel">
|
||||
<!-- 扫码输入框(支持扫码枪回车 / 手动输入) -->
|
||||
<el-input
|
||||
ref="inputRef"
|
||||
v-model="inputCode"
|
||||
placeholder="扫描或输入身份证后回车"
|
||||
size="large"
|
||||
clearable
|
||||
autofocus
|
||||
:disabled="loading"
|
||||
@keyup.enter="doQuery"
|
||||
>
|
||||
<template #prefix><el-icon><Scissor /></el-icon></template>
|
||||
<template #append><el-button :loading="loading" @click="doQuery">查询</el-button></template>
|
||||
</el-input>
|
||||
|
||||
<!-- 摄像头触发区 -->
|
||||
<div class="camera-trigger" @click="showCamera = true">
|
||||
<el-icon :size="40" color="#409EFF"><CameraFilled /></el-icon>
|
||||
<span>点击开启摄像头扫码</span>
|
||||
</div>
|
||||
|
||||
<el-alert type="info" :closable="false" show-icon>
|
||||
扫码 Track 产品身份证,自动带出物料、序列号并填充入库表单
|
||||
</el-alert>
|
||||
</div>
|
||||
|
||||
<!-- 全屏摄像头扫码层 -->
|
||||
<div v-if="showCamera" class="fullscreen-scanner-overlay">
|
||||
<div class="scanner-header">
|
||||
<el-button circle icon="Close" @click="showCamera = false" class="close-btn" />
|
||||
<span class="scanner-title">扫码模式</span>
|
||||
<div class="scanner-placeholder"></div>
|
||||
</div>
|
||||
<div class="scanner-body">
|
||||
<QrScanner @decode="onScanSuccess" />
|
||||
</div>
|
||||
<div class="scanner-footer">
|
||||
<p>请将条码/二维码放入镜头范围</p>
|
||||
<p v-if="loading" class="current-count">查询中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, nextTick } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Scissor, CameraFilled } from '@element-plus/icons-vue'
|
||||
import QrScanner from '@/components/QrScanner/index.vue'
|
||||
import { lookupTrackProduct as lookupSemi } from '@/api/inbound/semi'
|
||||
import { lookupTrackProduct as lookupProduct } from '@/api/inbound/product'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
/** 入库类型: semi=半成品, product=成品 */
|
||||
inboundType: 'semi' | 'product'
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), { inboundType: 'semi' })
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', val: boolean): void
|
||||
(e: 'fill', data: { track: any; material: any }): void
|
||||
}>()
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emit('update:modelValue', v)
|
||||
})
|
||||
|
||||
const inputRef = ref<InstanceType<typeof import('element-plus')['ElInput']> | null>(null)
|
||||
const inputCode = ref('')
|
||||
const loading = ref(false)
|
||||
const showCamera = ref(false)
|
||||
|
||||
const onOpened = () => {
|
||||
nextTick(() => inputRef.value?.focus())
|
||||
}
|
||||
|
||||
const onClosed = () => {
|
||||
inputCode.value = ''
|
||||
showCamera.value = false
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
/** 校验扫码内容格式,避免误扫 */
|
||||
const sanitizeCode = (raw: string): string => {
|
||||
return (raw || '').trim().replace(/[^A-Za-z0-9\-\.]/g, '')
|
||||
}
|
||||
|
||||
const doQuery = async () => {
|
||||
const code = inputCode.value.trim()
|
||||
if (!code) return
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const lookupFn = props.inboundType === 'product' ? lookupProduct : lookupSemi
|
||||
const res: any = await lookupFn(code)
|
||||
if (res.code !== 200) {
|
||||
ElMessage.error(res.msg || '扫码查询失败')
|
||||
return
|
||||
}
|
||||
const { track, material } = res.data
|
||||
const typeLabel = props.inboundType === 'product' ? '成品' : '半成品'
|
||||
const needCategory = props.inboundType === 'product' ? '/成品' : '/半成品'
|
||||
if (!material.category || !material.category.includes(needCategory)) {
|
||||
ElMessage.error(`扫码产品【${material.name || ''}】不是${typeLabel},无法进行${typeLabel}入库,请检查产品类别`)
|
||||
return
|
||||
}
|
||||
emit('fill', { track, material })
|
||||
visible.value = false
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.msg || e.message || '扫码查询失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** QrScanner 全屏扫码回调 */
|
||||
const onScanSuccess = (code: string) => {
|
||||
inputCode.value = sanitizeCode(code)
|
||||
showCamera.value = false
|
||||
doQuery()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.track-scan-panel { padding: 4px 2px; }
|
||||
|
||||
.camera-trigger {
|
||||
height: 120px;
|
||||
background: #f5f7fa;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #909399;
|
||||
margin: 16px 0 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.camera-trigger:active { background: #e6e8eb; }
|
||||
.camera-trigger span { margin-top: 5px; font-size: 13px; }
|
||||
|
||||
/* 全屏扫码层 */
|
||||
.fullscreen-scanner-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: #000;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.scanner-header {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 15px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: #fff;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
}
|
||||
.scanner-title { font-size: 16px; font-weight: bold; }
|
||||
.close-btn { background: rgba(255, 255, 255, 0.2); border: none; color: #fff; }
|
||||
.scanner-body {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
:deep(.qr-scanner-container) { width: 100% !important; height: 100% !important; border-radius: 0 !important; }
|
||||
.scanner-footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
z-index: 10;
|
||||
}
|
||||
.current-count { color: #67c23a; font-weight: bold; margin-top: 5px; font-size: 16px; }
|
||||
</style>
|
||||
@ -265,6 +265,18 @@
|
||||
<div class="dialog-scroll-container">
|
||||
<el-form :model="form" label-width="110px" ref="formRef" :rules="rules" size="default" class="stylish-form">
|
||||
|
||||
<!-- 扫码入库(Track 身份证自动带出物料、序列号与订单号) -->
|
||||
<div v-if="dialogStatus === 'create'" class="scan-inbound-banner">
|
||||
<el-button type="primary" size="large" @click="trackScanVisible = true">
|
||||
<el-icon><Camera /></el-icon>
|
||||
<span style="margin-left: 6px;">扫码入库</span>
|
||||
</el-button>
|
||||
<span class="scan-banner-tip">
|
||||
<el-icon><InfoFilled /></el-icon>
|
||||
扫描 Track 产品身份证,自动带出物料与序列号
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-card basic-card">
|
||||
<div class="card-title">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
@ -579,6 +591,8 @@
|
||||
|
||||
<!-- 智能扫码弹窗 -->
|
||||
<SmartScannerDialog v-model="scannerDialogVisible" @confirm="handleScannerConfirm" />
|
||||
<!-- 顶部扫码入库面板 (Track 身份证自动填充物料+序列号+订单号) -->
|
||||
<TrackScanDialog v-model="trackScanVisible" inbound-type="product" @fill="handleTrackScanFill" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -604,6 +618,7 @@ import { uploadFile, deleteFile } from '@/api/inbound/buy'
|
||||
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
|
||||
import WarehouseSelector from '@/components/WarehouseSelector.vue'
|
||||
import SmartScannerDialog from '@/components/SmartScannerDialog.vue'
|
||||
import TrackScanDialog from '@/components/TrackScanDialog.vue'
|
||||
import { getLabelPreview, executePrint } from '@/api/common/print'
|
||||
import { getWarehouseTree } from '@/api/common/warehouse'
|
||||
import { usePasteUpload } from '@/hooks/usePasteUpload'
|
||||
@ -741,6 +756,8 @@ const inspection_url = ref('')
|
||||
|
||||
// 智能扫码弹窗
|
||||
const scannerDialogVisible = ref(false)
|
||||
// 顶部扫码入库面板 (Track 身份证自动填充物料+序列号+订单号)
|
||||
const trackScanVisible = ref(false)
|
||||
|
||||
// 库位级联选择器数据
|
||||
const warehouseOptions = ref<any[]>([])
|
||||
@ -1377,7 +1394,7 @@ const handleCameraConfirm = async (file: File) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 智能扫码
|
||||
// 智能扫码 (序列号旁,仅填序列号)
|
||||
const openScanner = () => {
|
||||
scannerDialogVisible.value = true
|
||||
}
|
||||
@ -1388,6 +1405,15 @@ const handleScannerConfirm = (result: string) => {
|
||||
ElMessage.success('序列号已提取')
|
||||
}
|
||||
|
||||
// Track 扫码入库:扫码面板扫到身份证后,自动带出物料基础信息 + 序列号 + 订单号
|
||||
const handleTrackScanFill = async (data: { track: any; material: any }) => {
|
||||
const { track, material } = data
|
||||
await onMaterialSelected(material)
|
||||
form.serial_number = track.serial_number
|
||||
if (track.order_no) form.order_id = track.order_no
|
||||
ElMessage.success('扫码入库信息已自动填充,请核对后补充库位/数量等')
|
||||
}
|
||||
|
||||
// 快速基于此物料创建 BOM
|
||||
const createBomForMaterial = () => {
|
||||
if (!form.base_id) return ElMessage.warning('请先锁定物料基础信息')
|
||||
@ -1525,6 +1551,10 @@ watch([() => form.unit_total_cost, () => form.in_quantity], ([unit, qty]) => {
|
||||
:deep(.table-header-gray th) { background-color: #f8f9fb !important; color: #606266; }
|
||||
.tag-sn { color: #409EFF; font-weight: bold; font-family: monospace; }
|
||||
.stock-num { font-weight: bold; font-size: 15px; }
|
||||
/* 扫码入库横幅 */
|
||||
.scan-inbound-banner { background: linear-gradient(90deg, #ecf5ff, #f0f9eb); border: 1px dashed #409EFF; border-radius: 8px; padding: 14px 20px; margin-bottom: 20px; display: flex; align-items: center; gap: 14px; }
|
||||
.scan-banner-tip { font-size: 13px; color: #606266; display: flex; align-items: center; gap: 4px; }
|
||||
|
||||
.form-card { background: #fff; border-radius: 8px; margin-bottom: 20px; border: 1px solid #e4e7ed; overflow: hidden; }
|
||||
.card-title { background: #fcfcfc; padding: 10px 20px; border-bottom: 1px solid #ebeef5; font-weight: 600; display: flex; align-items: center; gap: 8px; }
|
||||
.card-title .icon { font-size: 18px; }
|
||||
|
||||
@ -298,6 +298,18 @@
|
||||
<div class="dialog-scroll-container">
|
||||
<el-form :model="form" label-width="100px" ref="formRef" :rules="rules" size="default" class="stylish-form">
|
||||
|
||||
<!-- 扫码入库(Track 身份证自动带出物料与序列号) -->
|
||||
<div v-if="dialogStatus === 'create'" class="scan-inbound-banner">
|
||||
<el-button type="primary" size="large" @click="trackScanVisible = true">
|
||||
<el-icon><Camera /></el-icon>
|
||||
<span style="margin-left: 6px;">扫码入库</span>
|
||||
</el-button>
|
||||
<span class="scan-banner-tip">
|
||||
<el-icon><InfoFilled /></el-icon>
|
||||
扫描 Track 产品身份证,自动带出物料与序列号
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-card basic-card">
|
||||
<div class="card-title">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
@ -623,6 +635,8 @@
|
||||
|
||||
<!-- 智能扫码弹窗 -->
|
||||
<SmartScannerDialog v-model="scannerDialogVisible" @confirm="handleScannerConfirm" />
|
||||
<!-- 顶部扫码入库面板 (Track 身份证自动填充物料+序列号) -->
|
||||
<TrackScanDialog v-model="trackScanVisible" inbound-type="semi" @fill="handleTrackScanFill" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -648,6 +662,7 @@ import { uploadFile, deleteFile } from '@/api/inbound/buy'
|
||||
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
|
||||
import WarehouseSelector from '@/components/WarehouseSelector.vue'
|
||||
import SmartScannerDialog from '@/components/SmartScannerDialog.vue'
|
||||
import TrackScanDialog from '@/components/TrackScanDialog.vue'
|
||||
import {getLabelPreview, executePrint} from '@/api/common/print'
|
||||
import { getWarehouseTree } from '@/api/common/warehouse'
|
||||
import { usePasteUpload } from '@/hooks/usePasteUpload'
|
||||
@ -808,6 +823,8 @@ const quality_report_url = ref('')
|
||||
|
||||
// 智能扫码弹窗
|
||||
const scannerDialogVisible = ref(false)
|
||||
// 顶部扫码入库面板 (Track 身份证自动填充物料+序列号)
|
||||
const trackScanVisible = ref(false)
|
||||
|
||||
// 库位级联选择器数据
|
||||
const warehouseOptions = ref<any[]>([])
|
||||
@ -1470,7 +1487,7 @@ const handleCameraConfirm = async (file: File) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 智能扫码
|
||||
// 智能扫码 (序列号旁,仅填序列号)
|
||||
const openScanner = () => {
|
||||
scannerDialogVisible.value = true
|
||||
}
|
||||
@ -1481,6 +1498,14 @@ const handleScannerConfirm = (result: string) => {
|
||||
ElMessage.success('序列号已提取')
|
||||
}
|
||||
|
||||
// Track 扫码入库:扫码面板扫到身份证后,自动带出物料基础信息 + 序列号
|
||||
const handleTrackScanFill = async (data: { track: any; material: any }) => {
|
||||
const { track, material } = data
|
||||
await onMaterialSelected(material)
|
||||
form.serial_number = track.serial_number
|
||||
ElMessage.success('扫码入库信息已自动填充,请核对后补充库位/数量等')
|
||||
}
|
||||
|
||||
// 快速基于此物料创建 BOM
|
||||
const createBomForMaterial = () => {
|
||||
if (!form.base_id) return ElMessage.warning('请先锁定物料基础信息')
|
||||
@ -1625,6 +1650,10 @@ onMounted(() => {
|
||||
/* [修改] 增加 min-height */
|
||||
.dialog-scroll-container { padding: 20px; max-height: 70vh; overflow-y: auto; overflow-x: hidden; min-height: 450px; }
|
||||
|
||||
/* 扫码入库横幅 */
|
||||
.scan-inbound-banner { background: linear-gradient(90deg, #ecf5ff, #f0f9eb); border: 1px dashed #409EFF; border-radius: 8px; padding: 14px 20px; margin-bottom: 20px; display: flex; align-items: center; gap: 14px; }
|
||||
.scan-banner-tip { font-size: 13px; color: #606266; display: flex; align-items: center; gap: 4px; }
|
||||
|
||||
.stylish-form .form-card { background: #fff; border-radius: 8px; border: 1px solid #e4e7ed; margin-bottom: 20px; }
|
||||
.card-title { background: #fcfcfc; padding: 12px 20px; border-bottom: 1px solid #ebeef5; font-weight: 600; font-size: 15px; color: #303133; display: flex; align-items: center; }
|
||||
.card-title .icon { margin-right: 8px; font-size: 18px; color: #409EFF; }
|
||||
|
||||
Reference in New Issue
Block a user