This commit is contained in:
zc
2026-04-11 22:57:34 +08:00
parent d34a501df9
commit a062c996c7
6 changed files with 301 additions and 74 deletions

View File

@@ -87,3 +87,26 @@ export function uploadMaterialPhotos(data: FormData) {
export function catchPhoto(data: FormData) { export function catchPhoto(data: FormData) {
return http.post(`${BASE_URL}/import/catch`, data) return http.post(`${BASE_URL}/import/catch`, data)
} }
/* 批次导入结果类型 */
export interface BatchImportResp {
importKey: string
totalRows: number
validRows: number
duplicateRows: number
}
/** @desc 下载批次导入模板 */
export function downloadBatchImportTemplate() {
return http.download(`${BASE_URL}/batch/import/template`)
}
/** @desc 解析批次导入数据 */
export function parseBatchImport(data: FormData) {
return http.post(`${BASE_URL}/batch/import/parse`, data)
}
/** @desc 批次导入 */
export function batchImport(data: any) {
return http.post(`${BASE_URL}/batch/import`, data)
}

View File

@@ -9,6 +9,7 @@ export interface WeighManageResp {
materialSpec: string materialSpec: string
unitWeight: number unitWeight: number
photoUrl: string photoUrl: string
batch: string
materialProcess: string materialProcess: string
matchResult: string matchResult: string
downFloatRatio: string downFloatRatio: string

View File

@@ -0,0 +1,163 @@
<template>
<a-drawer
v-model:visible="visible"
title="批次导入"
:mask-closable="false"
:esc-to-close="false"
:width="width >= 600 ? 600 : '100%'"
ok-text="确认导入"
cancel-text="取消导入"
@before-ok="save"
@close="reset"
>
<a-form ref="formRef" :model="form" size="large" auto-label-width>
<a-alert v-if="!form.disabled" style="margin-bottom: 15px">
请按照模板要求填写数据填写完毕后请先上传并进行解析
<template #action>
<a-link @click="downloadTemplate">
<template #icon><GiSvgIcon name="file-excel" :size="16" /></template>
<template #default>下载模板</template>
</a-link>
</template>
</a-alert>
<fieldset>
<legend>1.解析数据</legend>
<div class="file-box">
<a-upload
draggable
:custom-request="handleUpload"
:limit="1"
:show-retry-butto="false"
:show-cancel-button="false" tip="仅支持xls、xlsx格式"
:file-list="uploadFile"
accept=".xls, .xlsx, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
/>
</div>
<div v-if="dataResult.importKey">
<div class="file-box">
<a-space size="large">
<a-statistic title="总计行数" :value="dataResult.totalRows" />
<a-statistic title="正常行数" :value="dataResult.validRows" />
</a-space>
</div>
<div class="file-box">
<a-space size="large">
<a-statistic title="已存在批次" :value="dataResult.duplicateRows" />
</a-space>
</div>
</div>
</fieldset>
</a-form>
</a-drawer>
</template>
<script setup lang="ts">
import { type FormInstance, Message, type RequestOption } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import {
type BatchImportResp,
downloadBatchImportTemplate,
batchImport,
parseBatchImport,
} from '@/apis/material/materialInfo'
import { useDownload, useResetReactive } from '@/hooks'
const emit = defineEmits<{
(e: 'save-success'): void
}>()
const { width } = useWindowSize()
const visible = ref(false)
const formRef = ref<FormInstance>()
const uploadFile = ref([])
const [form, resetForm] = useResetReactive({
importKey: '',
})
const dataResult = ref<BatchImportResp>({
importKey: '',
totalRows: 0,
validRows: 0,
duplicateRows: 0,
})
const reset = () => {
formRef.value?.resetFields()
dataResult.value.importKey = ''
uploadFile.value = []
resetForm()
}
const downloadTemplate = () => {
useDownload(() => downloadBatchImportTemplate())
}
const handleUpload = (options: RequestOption) => {
const controller = new AbortController();
(async function requestWrap() {
const { onProgress, onError, onSuccess, fileItem, name = 'file' } = options
onProgress(20)
const formData = new FormData()
formData.append(name as string, fileItem.file as Blob)
try {
const res = await parseBatchImport(formData)
dataResult.value = res.data
Message.success('上传解析成功')
onSuccess(res)
} catch (error) {
onError(error)
}
})()
return {
abort() {
controller.abort()
},
}
}
const save = async () => {
try {
if (!dataResult.value.importKey) {
Message.warning('请先上传文件,解析导入数据')
return false
}
form.importKey = dataResult.value.importKey
const res = await batchImport(form)
Message.success(`导入成功! 表格总数${res.data.totalRows} 修改${res.data.updateRows}`)
emit('save-success')
return true
} catch (error) {
return false
}
}
const onOpen = () => {
reset()
visible.value = true
}
defineExpose({ onOpen })
</script>
<style scoped lang="scss">
fieldset {
padding: 15px 15px 0 15px;
margin-bottom: 15px;
border: 1px solid var(--color-neutral-3);
border-radius: 3px;
}
fieldset legend {
color: rgb(var(--gray-10));
padding: 2px 5px 2px 5px;
border: 1px solid var(--color-neutral-3);
border-radius: 3px;
}
.file-box {
margin-bottom: 20px;
margin-left: 10px;
}
</style>

View File

@@ -78,6 +78,19 @@ const columns: ColumnItem[] = reactive([
allowSearch: true, allowSearch: true,
}, },
}, },
{
label: '物料流程编码',
field: 'materialProcess',
type: 'input',
span: 24,
required: true,
},
{
label: '批次',
field: 'batch',
type: 'input',
span: 24,
},
{ {
label: '灯光等级', label: '灯光等级',
field: 'lightLevel', field: 'lightLevel',
@@ -90,13 +103,6 @@ const columns: ColumnItem[] = reactive([
}, },
span: 24, span: 24,
}, },
{
label: '物料流程编码',
field: 'materialProcess',
type: 'input',
span: 24,
required: true,
},
{ {
label: '物料直径', label: '物料直径',
field: 'materialSpec', field: 'materialSpec',
@@ -120,7 +126,7 @@ const columns: ColumnItem[] = reactive([
type: 'image', type: 'image',
savePath: 'material/', savePath: 'material/',
span: 24, span: 24,
// required: true, required: true,
}, },
]) ])

View File

@@ -44,7 +44,11 @@
</a-button> </a-button>
<a-button v-permission="['admin:materialInfo:import']" @click="onPhotosImport"> <a-button v-permission="['admin:materialInfo:import']" @click="onPhotosImport">
<template #icon><icon-upload /></template> <template #icon><icon-upload /></template>
<template #default>照片批量导入</template> <template #default>照片导入</template>
</a-button>
<a-button v-permission="['admin:materialInfo:import']" @click="onBatchImport">
<template #icon><icon-upload /></template>
<template #default>批次导入</template>
</a-button> </a-button>
</template> </template>
@@ -77,6 +81,7 @@
<MaterialInfoAddModal ref="MaterialInfoAddModalRef" @save-success="search" /> <MaterialInfoAddModal ref="MaterialInfoAddModalRef" @save-success="search" />
<MaterialInfoImportDrawer ref="MaterialInfoImportDrawerRef" @save-success="search" /> <MaterialInfoImportDrawer ref="MaterialInfoImportDrawerRef" @save-success="search" />
<PhotosImport ref="PhotosImportRef" @save-success="search"/> <PhotosImport ref="PhotosImportRef" @save-success="search"/>
<BatchImport ref="BatchImportRef" @save-success="search"/>
</div> </div>
</template> </template>
@@ -84,6 +89,7 @@
import MaterialInfoAddModal from './MaterialInfoAddModal.vue' import MaterialInfoAddModal from './MaterialInfoAddModal.vue'
import MaterialInfoImportDrawer from './MaterialInfoImportDrawer.vue' import MaterialInfoImportDrawer from './MaterialInfoImportDrawer.vue'
import PhotosImport from '@/views/material/PhotosImport.vue'; import PhotosImport from '@/views/material/PhotosImport.vue';
import BatchImport from '@/views/material/BatchImport.vue';
import { type MaterialInfoQuery, type MaterialInfoResp, deleteMaterialInfo, exportMaterialInfo, listMaterialInfo } from '@/apis/material/materialInfo' import { type MaterialInfoQuery, type MaterialInfoResp, deleteMaterialInfo, exportMaterialInfo, listMaterialInfo } from '@/apis/material/materialInfo'
import type { TableInstanceColumns } from '@/components/GiTable/type' import type { TableInstanceColumns } from '@/components/GiTable/type'
import { useDownload, useTable } from '@/hooks' import { useDownload, useTable } from '@/hooks'
@@ -121,8 +127,9 @@ const columns = ref<TableInstanceColumns[]>([
{ title: '物料编码', dataIndex: 'encoding', slotName: 'encoding' }, { title: '物料编码', dataIndex: 'encoding', slotName: 'encoding' },
{ title: '物料单位重量(g)', dataIndex: 'unitWeight', slotName: 'unitWeight' }, { title: '物料单位重量(g)', dataIndex: 'unitWeight', slotName: 'unitWeight' },
{ title: '物料品类', dataIndex: 'typeName' }, { title: '物料品类', dataIndex: 'typeName' },
{ title: '物料直径', dataIndex: 'materialSpec', slotName: 'materialSpec' }, { title: '批次', dataIndex: 'batch' },
{ title: '物料颜色', dataIndex: 'color', slotName: 'color' }, { title: '物料直径', dataIndex: 'materialSpec', slotName: 'materialSpec', show: false },
{ title: '物料颜色', dataIndex: 'color', slotName: 'color', show: false },
{ title: '物料流程', dataIndex: 'materialProcess', slotName: 'materialProcess' }, { title: '物料流程', dataIndex: 'materialProcess', slotName: 'materialProcess' },
{ title: '灯光等级', dataIndex: 'lightLevel', slotName: 'lightLevel' }, { title: '灯光等级', dataIndex: 'lightLevel', slotName: 'lightLevel' },
{ title: '物料照片', dataIndex: 'photoUrl', slotName: 'photoUrl', width: 120, align: 'center' }, { title: '物料照片', dataIndex: 'photoUrl', slotName: 'photoUrl', width: 120, align: 'center' },
@@ -196,6 +203,12 @@ const PhotosImportRef = ref<InstanceType<typeof PhotosImport>>()
const onPhotosImport = () => { const onPhotosImport = () => {
PhotosImportRef.value?.onOpen() PhotosImportRef.value?.onOpen()
} }
const BatchImportRef = ref<InstanceType<typeof BatchImport>>()
const onBatchImport = () => {
BatchImportRef.value?.onOpen()
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -32,13 +32,13 @@
<a-form :model="formData" layout="vertical"> <a-form :model="formData" layout="vertical">
<div class="form-row"> <div class="form-row">
<div class="form-item"> <div class="form-item">
<a-form-item label="物料名称"> <a-form-item label="批次">
<a-input v-model="formData.materialName" placeholder="物料名称" disabled /> <a-input v-model="formData.batch" placeholder="批次" disabled />
</a-form-item> </a-form-item>
</div> </div>
<div class="form-item"> <div class="form-item">
<a-form-item label="物料直径"> <a-form-item label="物料名称">
<a-input v-model="formData.materialSpec" placeholder="物料直径" disabled /> <a-input v-model="formData.materialName" placeholder="物料名称" disabled />
</a-form-item> </a-form-item>
</div> </div>
</div> </div>
@@ -134,6 +134,10 @@
<label>输入数量:</label> <label>输入数量:</label>
<a-input-number :min="1" mode="button" v-model="inputQuantity" placeholder="请输入数量" @change="calculateWeight" /> <a-input-number :min="1" mode="button" v-model="inputQuantity" placeholder="请输入数量" @change="calculateWeight" />
</div> </div>
<div class="form-item">
<label>当前数量:</label>
<a-input v-model="calculateNumber" placeholder="-" disabled/>
</div>
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-item"> <div class="form-item">
@@ -185,8 +189,8 @@
<h4>称重列表</h4> <h4>称重列表</h4>
</div> </div>
<a-table :columns="columns" :data="weighingList" bordered> <a-table :columns="columns" :data="weighingList" bordered>
<template #image="{ record }"> <template #imgUrl="{ record }">
<a-image width="60" :src="record.image"/> <a-image width="60" :src="record.imgUrl"/>
</template> </template>
<template #action="{ record }"> <template #action="{ record }">
@@ -255,20 +259,9 @@
v-if="activeStep === 1" v-if="activeStep === 1"
type="primary" type="primary"
class="next-button" class="next-button"
@click="compareHandle"
>
开始比对
</a-button>
<!-- 下一步按钮 -->
<a-button
v-if="activeStep === 1"
type="primary"
class="next-button"
:disabled="compareMatchResult !== 'success'"
style="margin-left: 12px"
@click="handleNext" @click="handleNext"
> >
下一步 开始比对
</a-button> </a-button>
<!-- 完成按钮 --> <!-- 完成按钮 -->
<a-button <a-button
@@ -313,6 +306,7 @@ const formData = reactive({
encoding: '', // 物料编码 encoding: '', // 物料编码
materialName: '', // 物料名称 materialName: '', // 物料名称
materialSpec: '', // 物料直径 materialSpec: '', // 物料直径
batch: '', // 批次
materialProcess: '', // 物料流程 materialProcess: '', // 物料流程
unitWeight: 0, // 重量 unitWeight: 0, // 重量
photoUrl: '', // 样图URL photoUrl: '', // 样图URL
@@ -572,8 +566,9 @@ const workOrderResp = ref<WorkOrderResp>({
// 称重登记页面数据 // 称重登记页面数据
const inputQuantity = ref() const inputQuantity = ref()
const calculateNumber = ref()
// todo // todo
const ahDeviceWeight = ref('') const ahDeviceWeight = ref('6')
const calculatedWeight = ref('') const calculatedWeight = ref('')
const weighingCount = ref(1) const weighingCount = ref(1)
@@ -592,7 +587,7 @@ const columns = ref<TableInstanceColumns[]>([
{ title: '数量', dataIndex: 'quantity', key: 'quantity', className: 'green-bg',}, { title: '数量', dataIndex: 'quantity', key: 'quantity', className: 'green-bg',},
{ title: '称重重量(g)', dataIndex: 'weight', key: 'weight', className: 'green-bg',}, { title: '称重重量(g)', dataIndex: 'weight', key: 'weight', className: 'green-bg',},
{ title: '标准重量(g)', dataIndex: 'calculatedWeight', key: 'calculatedWeight',}, { title: '标准重量(g)', dataIndex: 'calculatedWeight', key: 'calculatedWeight',},
{ title: '抓图', dataIndex: 'image', key: 'image',slotName: 'image',}, { title: '抓图', dataIndex: 'imgUrl', key: 'imgUrl',slotName: 'imgUrl',},
{ title: '操作', dataIndex: 'action', key: 'action', slotName: 'action',}, { title: '操作', dataIndex: 'action', key: 'action', slotName: 'action',},
]) ])
@@ -679,47 +674,14 @@ const fetchMaterialData = async (code: string) => {
formData.materialProcess = res.data?.materialProcess || '' formData.materialProcess = res.data?.materialProcess || ''
formData.unitWeight = res.data?.unitWeight || 0 formData.unitWeight = res.data?.unitWeight || 0
formData.photoUrl = res.data?.photoUrl || '' formData.photoUrl = res.data?.photoUrl || ''
formData.weightRange = (res.data?.downFloatRatio || '-') + '% ~ ' + (res.data?.upFloatRatio || '-') + '%' formData.batch = res.data?.batch || ''
formData.weightRange = (res.data?.downFloatRatio ?? '-') + '% ~ ' + (res.data?.upFloatRatio ?? '-') + '%'
} }
if(res.data && res.data.id) { if(res.data && res.data.id) {
await brightness(res.data.id); await brightness(res.data.id);
} }
} }
const compareHandle = async () => {
try {
const materialCode = formData.inputMaterialCode?.trim()
if (!materialCode) {
Message.error('请先扫描物料编码')
return;
}
if (!formData.materialProcess || formData.materialProcess === '') {
Message.error('未找到物料流程,无法对比')
return;
}
// 调用后端接口获取比对结果 // todo
const res = await vmSend(materialCode);
if (res.data) {
compareMatchResult.value = res.data
if (res.data === 'success') {
Message.success('比对成功')
} else {
Message.error('比对失败')
}
} else {
// 比对失败,提示错误
Message.error('比对数据异常')
}
// compareMatchResult.value = 'success'
} catch (error) {
console.error('比对失败:', error)
Message.error('相机异常,请重试')
}
}
// 处理下一步 // 处理下一步
const handleNext = async () => { const handleNext = async () => {
// 步骤2显示确认弹框 // 步骤2显示确认弹框
@@ -763,9 +725,48 @@ const handleNext = async () => {
return; return;
} }
// 其他情况步骤1且已成功比对或步骤0直接进入下一步 if (activeStep.value === 1) {
try {
const materialCode = formData.inputMaterialCode?.trim()
if (!materialCode) {
Message.error('请先扫描物料编码')
return;
}
if (!formData.batch || formData.batch === '') {
Message.error('该物料没有批次,无法对比')
return;
}
if (!formData.materialProcess || formData.materialProcess === '') {
Message.error('未找到物料流程,无法对比')
return;
}
// 调用后端接口获取比对结果 // todo
// const res = await vmSend(materialCode);
const res = {
data: 'success',
};
if (res.data) {
compareMatchResult.value = res.data
if (res.data === 'success') {
Message.success('比对成功')
activeStep.value++ activeStep.value++
// 进入称重页面的操作已在watch监听器中处理 } else {
Message.error('比对失败')
}
} else {
// 比对失败,提示错误
Message.error('比对数据异常')
}
// compareMatchResult.value = 'success'
} catch (error) {
console.error('比对失败:', error)
Message.error('相机异常,请重试')
}
}
} }
// 处理上一步 // 处理上一步
@@ -807,7 +808,7 @@ const handleBackToFirst = () => {
// 重置称重登记页面数据 // 重置称重登记页面数据
inputQuantity.value = '' inputQuantity.value = ''
// todo // todo
ahDeviceWeight.value = '' ahDeviceWeight.value = '6'
calculatedWeight.value = '' calculatedWeight.value = ''
weighingCount.value = 1 weighingCount.value = 1
// 清空称重列表 // 清空称重列表
@@ -885,7 +886,7 @@ const handleConfirm = async () => {
quantity: inputQuantity.value, quantity: inputQuantity.value,
weight: ahDeviceWeight.value, weight: ahDeviceWeight.value,
calculatedWeight: calculatedWeight.value, calculatedWeight: calculatedWeight.value,
image: '加载中...', // 先显示加载状态 imgUrl: '加载中...', // 先显示加载状态
} }
// 先添加到列表,让用户能立即看到记录 // 先添加到列表,让用户能立即看到记录
@@ -894,7 +895,7 @@ const handleConfirm = async () => {
// 重置输入(让用户能继续输入) // 重置输入(让用户能继续输入)
inputQuantity.value = '' inputQuantity.value = ''
// todo // todo
ahDeviceWeight.value = '' ahDeviceWeight.value = '6'
calculatedWeight.value = '' calculatedWeight.value = ''
weighingCount.value = weighingList.value.length + 1 weighingCount.value = weighingList.value.length + 1
@@ -902,7 +903,7 @@ const handleConfirm = async () => {
const imageUrl = await captureImage(newItem.key + '_' + newItem.materialId); const imageUrl = await captureImage(newItem.key + '_' + newItem.materialId);
const addedItem = weighingList.value.find((item) => item.key === newItem.key) const addedItem = weighingList.value.find((item) => item.key === newItem.key)
if (addedItem) { if (addedItem) {
addedItem.image = imageUrl || '抓图失败' addedItem.imgUrl = imageUrl || '抓图失败'
// 触发视图更新 // 触发视图更新
weighingList.value = [...weighingList.value] weighingList.value = [...weighingList.value]
} }
@@ -945,6 +946,13 @@ const establishWebSocket = () => {
try { try {
if (event.data) { if (event.data) {
ahDeviceWeight.value = event.data ahDeviceWeight.value = event.data
const weight = Number.parseFloat(ahDeviceWeight.value.toString())
const unitWeight = formData.unitWeight
if (!isNaN(weight) && !isNaN(unitWeight) && unitWeight > 0) {
calculateNumber.value = Math.floor(weight / unitWeight)
} else {
calculateNumber.value = ''
}
} }
} catch (error) { } catch (error) {
console.error('WebSocket消息解析失败:', error) console.error('WebSocket消息解析失败:', error)
@@ -1377,4 +1385,17 @@ onUnmounted(() => {
font-size: 14px; font-size: 14px;
margin-right: 12px; margin-right: 12px;
} }
/* 禁用输入框样式 - 黑色加粗字体 */
:deep(.arco-input-wrapper.arco-input-disabled) {
background-color: #ffffff !important;
border-color: #d9d9d9 !important;
}
:deep(.arco-input-wrapper.arco-input-disabled .arco-input) {
color: #000000 !important;
background-color: transparent !important;
opacity: 1 !important;
-webkit-text-fill-color: #000000 !important;
}
</style> </style>