Files
KCGL/inventory-web/src/views/outbound/create.vue

286 lines
8.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="app-container">
<el-card class="box-card">
<template #header>
<div class="card-header">
<span>扫码出库作业台</span>
<el-button type="primary" @click="toggleCamera">
{{ showCamera ? '关闭摄像头' : '打开摄像头扫码' }}
</el-button>
</div>
</template>
<div v-if="!currentItem" class="scan-area">
<div v-if="showCamera" id="reader" class="camera-box"></div>
<div class="input-box">
<el-input
v-model="barcodeInput"
placeholder="请扫描条码或手动输入后回车"
@keyup.enter="handleScan"
clearable
ref="barcodeRef"
>
<template #prefix>
<el-icon><Scissor /></el-icon>
</template>
<template #append>
<el-button @click="handleScan">确定</el-button>
</template>
</el-input>
</div>
</div>
<div v-else class="confirm-area">
<el-descriptions title="货物信息" border :column="1">
<el-descriptions-item label="名称">{{ currentItem.name }}</el-descriptions-item>
<el-descriptions-item label="规格/型号">{{ currentItem.spec_model }}</el-descriptions-item>
<el-descriptions-item label="当前库存">{{ currentItem.stock_quantity }}</el-descriptions-item>
<el-descriptions-item label="库位">{{ currentItem.warehouse_location || '暂无' }}</el-descriptions-item>
</el-descriptions>
<el-form :model="form" ref="formRef" :rules="rules" label-position="top" class="mt-20">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="出库类型" prop="outbound_type">
<el-select v-model="form.outbound_type" placeholder="请选择">
<el-option label="销售出库" value="SALES" />
<el-option label="内部领用" value="USE" />
<el-option label="调拨" value="TRANSFER" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="出库数量" prop="quantity">
<el-input-number v-model="form.quantity" :min="1" :max="currentItem.available_quantity" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="领用人/客户姓名" prop="consumer_name">
<el-input v-model="form.consumer_name" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" />
</el-form-item>
<el-form-item label="电子签名 (请在下方区域签字)" required>
<Signature ref="signatureRef" />
</el-form-item>
<div class="form-actions">
<el-button @click="resetScan">取消 / 重新扫码</el-button>
<el-button type="primary" :loading="loading" @click="submitForm">确认出库</el-button>
</div>
</el-form>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, nextTick, onUnmounted } from 'vue'
import { ElMessage } from 'element-plus'
// ★ [修改] 引入 Html5QrcodeSupportedFormats 以支持 Code 128
import { Html5Qrcode, Html5QrcodeSupportedFormats } from 'html5-qrcode'
import { getStockByBarcode, submitOutbound, type ScanResult } from '@/api/outbound'
import { uploadFile } from '@/api/common/upload'
import Signature from '@/components/Signature/index.vue'
// --- 状态定义 ---
const barcodeInput = ref('')
const currentItem = ref<ScanResult | null>(null)
const loading = ref(false)
const showCamera = ref(false)
const html5QrCode = ref<Html5Qrcode | null>(null)
const barcodeRef = ref()
const signatureRef = ref()
const formRef = ref() // ★ [修复] 补充 formRef 定义
const form = reactive({
outbound_type: 'SALES',
quantity: 1,
consumer_name: '',
remark: ''
})
const rules = {
consumer_name: [{ required: true, message: '请输入领用人姓名', trigger: 'blur' }],
quantity: [{ required: true, message: '请输入数量', trigger: 'blur' }]
}
// --- 扫码逻辑 ---
const handleScan = async () => {
if (!barcodeInput.value) return
try {
loading.value = true
const res = await getStockByBarcode(barcodeInput.value)
if (res.data) {
currentItem.value = res.data
// 停止摄像头
if (html5QrCode.value && html5QrCode.value.isScanning) {
await stopCamera()
}
} else {
ElMessage.warning('未找到对应库存条码')
}
} catch (error) {
console.error(error)
} finally {
loading.value = false
}
}
// --- 摄像头控制 ---
const toggleCamera = async () => {
if (showCamera.value) {
await stopCamera()
} else {
showCamera.value = true
nextTick(() => {
startCamera()
})
}
}
const startCamera = () => {
// ★ [关键修改] 初始化时指定只支持 CODE_128这是提高条码识别率的关键
html5QrCode.value = new Html5Qrcode("reader", {
formatsToSupport: [ Html5QrcodeSupportedFormats.CODE_128 ],
verbose: false
})
html5QrCode.value.start(
{ facingMode: "environment" }, // 后置摄像头
{
fps: 10,
// ★ [修改] 扫一维码时,扫描区域设置为长方形效果更好
qrbox: { width: 250, height: 150 },
aspectRatio: 1.0
},
(decodedText) => {
// 扫码成功回调
console.log("扫码成功:", decodedText)
barcodeInput.value = decodedText
handleScan() // 自动触发查询
},
(errorMessage) => {
// 扫描中错误忽略
}
).catch(err => {
// 如果还报错,大概率是因为没有使用 HTTPS 访问
ElMessage.error('无法启动摄像头: ' + err)
})
}
const stopCamera = async () => {
if (html5QrCode.value) {
try {
// 先判断是否在运行,避免报错
if (html5QrCode.value.isScanning) {
await html5QrCode.value.stop()
}
html5QrCode.value.clear()
showCamera.value = false
} catch (err) {
console.log('停止摄像头失败', err)
// 强制关闭 UI 状态
showCamera.value = false
}
}
}
// --- 提交逻辑 ---
const submitForm = async () => {
if (!formRef.value) return // 确保 formRef 存在
await formRef.value.validate(async (valid: boolean) => {
if (!valid) return
if (!currentItem.value) return
// 1. 获取签名图片
const file = await signatureRef.value.generateFile()
if (!file) {
ElMessage.error('请先进行电子签名')
return
}
try {
loading.value = true
// 2. 上传签名
const uploadRes = await uploadFile(file)
const signatureUrl = uploadRes.data.url
// 3. 提交业务单据
await submitOutbound({
sku: currentItem.value.sku,
source_table: currentItem.value.source_table,
stock_id: currentItem.value.id,
barcode: barcodeInput.value,
outbound_type: form.outbound_type,
quantity: form.quantity,
consumer_name: form.consumer_name,
remark: form.remark,
signature_path: signatureUrl
})
ElMessage.success('出库成功')
resetScan()
} catch (error) {
console.error(error)
} finally {
loading.value = false
}
})
}
const resetScan = () => {
currentItem.value = null
barcodeInput.value = ''
form.consumer_name = ''
form.quantity = 1
form.remark = ''
signatureRef.value?.clear() // 清空签名
// 重新聚焦输入框
nextTick(() => {
barcodeRef.value?.focus()
})
}
onUnmounted(() => {
stopCamera()
})
</script>
<style scoped>
.scan-area {
text-align: center;
padding: 20px;
}
.camera-box {
width: 100%; /* 响应式宽度 */
max-width: 400px; /* 限制最大宽度 */
height: 300px;
margin: 0 auto 20px;
background: #000;
overflow: hidden;
}
.confirm-area {
max-width: 600px;
margin: 0 auto;
}
.mt-20 {
margin-top: 20px;
}
.form-actions {
margin-top: 30px;
display: flex;
justify-content: space-between;
}
</style>