将半成品和成品跟bom表进行相关联

This commit is contained in:
dxc
2026-02-12 17:16:24 +08:00
parent b682d4b02f
commit 853374de5d
8 changed files with 292 additions and 192 deletions

View File

@ -276,8 +276,36 @@
<div class="card-title"><el-icon class="icon"><Setting /></el-icon><span>3. 生产与销售信息</span></div>
<div class="card-content">
<el-row :gutter="24">
<el-col :span="8"><el-form-item label="BOM编号"><el-input v-model="form.bom_code" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="BOM版本"><el-input v-model="form.bom_version" /></el-form-item></el-col>
<el-col :span="8">
<el-form-item label="BOM编号">
<el-select
v-model="form.bom_code"
filterable
remote
clearable
placeholder="搜规格/编号"
:remote-method="handleSearchBom"
:loading="bomSearchLoading"
@change="handleBomSelect"
style="width: 100%"
>
<el-option
v-for="item in bomOptions"
:key="`${item.bom_no}_${item.version}`"
:label="item.bom_no"
:value="`${item.bom_no}###${item.version}`"
>
<span style="float: left; font-weight: bold;">{{ item.bom_no }}</span>
<span style="float: right; color: #8492a6; font-size: 13px; margin-left: 10px;">
{{ item.version }} <span v-if="item.parent_spec">({{ item.parent_spec }})</span>
</span>
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8"><el-form-item label="BOM版本"><el-input v-model="form.bom_version" placeholder="自动填充" readonly/></el-form-item></el-col>
<el-col :span="8"><el-form-item label="工单号"><el-input v-model="form.work_order_code" /></el-form-item></el-col>
</el-row>
<el-row :gutter="24">
@ -353,10 +381,16 @@
<script setup lang="ts">
import { ref, reactive, onMounted, watch } from 'vue'
import { Plus, Setting, Refresh, Search, Box, House, Link, InfoFilled, Printer, Camera, Picture } from '@element-plus/icons-vue'
// 修复点:引入 ElLoading
import { ElMessage, ElLoading } from 'element-plus'
import dayjs from 'dayjs'
import { getProductList, createProductInbound, updateProductInbound, deleteProductInbound, searchMaterialBase } from '@/api/inbound/product'
import {
getProductList,
createProductInbound,
updateProductInbound,
deleteProductInbound,
searchMaterialBase,
searchBom // [新增]
} from '@/api/inbound/product'
import { uploadFile, deleteFile } from '@/api/inbound/buy'
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
import { getLabelPreview, executePrint } from '@/api/common/print'
@ -372,6 +406,10 @@ const formRef = ref()
const queryParams = reactive({ page: 1, pageSize: 100, keyword: '', statuses: ['在库', '借库'] })
const materialOptions = ref<any[]>([])
// BOM 搜索相关
const bomSearchLoading = ref(false)
const bomOptions = ref<any[]>([])
// 打印相关变量
const printVisible = ref(false)
const printLoading = ref(false)
@ -382,14 +420,12 @@ const currentPrintData = ref<any>({})
// 图片/拍照相关
const dialogImageUrl = ref('')
const dialogVisibleImage = ref(false)
// 3个独立的列表
const productPhotoList = ref<any[]>([]) // 成品实拍
const qualityFileList = ref<any[]>([]) // 质量报告
const inspectionFileList = ref<any[]>([]) // 检测报告
const cameraDialogVisible = ref(false)
const cameraRef = ref<InstanceType<typeof WebRtcCamera> | null>(null)
// 定义当前触发拍照的字段
const currentCameraField = ref<'product_photo' | 'quality_report_link' | 'inspection_report_link'>('product_photo')
const quality_url = ref('')
const inspection_url = ref('')
@ -433,11 +469,31 @@ const form = reactive({
})
// ------------------------------------
// 校验规则 (前端 pre-check)
// BOM Search Logic
// ------------------------------------
const handleSearchBom = async (query: string) => {
bomSearchLoading.value = true
try {
const res: any = await searchBom(query)
bomOptions.value = res.data || []
} finally { bomSearchLoading.value = false }
}
const handleBomSelect = (val: string) => {
if (!val) {
form.bom_code = ''
form.bom_version = ''
return
}
const [code, version] = val.split('###')
form.bom_code = code
form.bom_version = version
}
// ------------------------------------
// Validation Logic
// ------------------------------------
const validateUnique = (rule: any, value: string, callback: any) => {
if (!value) return callback()
// 简单的列表前端查重
const isDuplicate = tableData.value.some((row: any) => {
if (dialogStatus.value === 'update' && row.id === form.id) return false
if (rule.field === 'serial_number' && row.serial_number === value) return true
@ -469,7 +525,6 @@ const handleSearchMaterial = async (query: string) => {
const onMaterialSelected = (val: number) => {
const item = materialOptions.value.find(i => i.id === val)
if (item) {
// Auto-populate readonly fields
form.material_name = item.name
form.spec_model = item.spec
form.material_type = item.type
@ -482,11 +537,9 @@ const onMaterialSelected = (val: number) => {
// Autocomplete (Manager) - 后端驱动
// ------------------------------------
const querySearchManager = async (query: string, cb: any) => {
// 后续从后端获取用户建议
cb([])
}
const handleManagerSelect = (item: any) => {
// 无需保存历史
}
const fetchData = async () => {
@ -530,6 +583,10 @@ const handleUpdate = (row: any) => {
const iLinks = iReports.filter(r => isExternalLink(r))
inspection_url.value = iLinks.length > 0 ? iLinks[0] : ''
materialOptions.value = [{ id: row.base_id, name: row.material_name, spec: row.spec_model, category: row.category, isHistory: false }]
// 回显BOM
if (form.bom_code) {
bomOptions.value = [{ bom_no: form.bom_code, version: form.bom_version }]
}
visible.value = true
}
@ -563,9 +620,6 @@ const triggerCamera = (field: any) => {
cameraDialogVisible.value = true;
}
// ------------------------------------------------------------------------
// 修复核心:拍照上传回调逻辑
// ------------------------------------------------------------------------
const handleCameraConfirm = async (file: File) => {
if (!beforeAvatarUpload(file)) {
cameraDialogVisible.value = false;
@ -573,25 +627,18 @@ const handleCameraConfirm = async (file: File) => {
}
const formData = new FormData();
formData.append('file', file);
// 使用 ElLoading.service 替代报错的 ElMessage.loading
const loadingMsg = ElLoading.service({
lock: true,
text: '照片处理中...',
background: 'rgba(0, 0, 0, 0.7)'
});
let success = false;
try {
const res: any = await uploadFile(formData);
if (res.code === 200) {
const newUrl = res.data.url;
const field = currentCameraField.value; // 根据触发时记录的字段
// 添加到表单数据
const field = currentCameraField.value;
form[field].push(newUrl);
// 更新对应的显示列表
const previewItem = { name: newUrl.split('/').pop(), url: getImageUrl(newUrl) };
if (field === 'product_photo') {
productPhotoList.value.push(previewItem);
@ -600,7 +647,6 @@ const handleCameraConfirm = async (file: File) => {
} else if (field === 'inspection_report_link') {
inspectionFileList.value.push(previewItem);
}
ElMessage.success('拍照上传成功');
success = true;
} else {
@ -639,7 +685,6 @@ const submitForm = async () => {
} else { await updateProductInbound(form.id!, payload); ElMessage.success('更新成功') }
visible.value = false; fetchData()
} catch(e:any) {
// 捕获后端报错
ElMessage.error(e.msg || '操作失败')
} finally { submitting.value = false }
}
@ -654,7 +699,7 @@ const handlePrint = async (row: any) => {
}
const confirmPrint = async () => { printing.value = true; try { await executePrint(currentPrintData.value); ElMessage.success('已发送'); printVisible.value = false } catch (e: any) { ElMessage.error('打印失败') } finally { printing.value = false } }
const resetForm = () => {
materialOptions.value = []; productPhotoList.value = []; qualityFileList.value = []; inspectionFileList.value = []; quality_url.value = ''; inspection_url.value = ''
materialOptions.value = []; bomOptions.value = []; productPhotoList.value = []; qualityFileList.value = []; inspectionFileList.value = []; quality_url.value = ''; inspection_url.value = ''
Object.assign(form, { id: undefined, base_id: undefined, material_name: '', spec_model: '', material_type: '', category: '', unit: '', sku: '', barcode: '', serial_number: '', in_date: '', in_quantity: 1, stock_quantity: 1, available_quantity: 1, warehouse_location: '', status: '在库', quality_status: '合格', bom_code: '', bom_version: '', work_order_code: '', order_id: '', production_manager: '', production_time_range: [], raw_material_cost: 0, manual_cost: 0, sale_price: 0, quality_report_link: [], inspection_report_link: [], product_photo: [], detail_link: '' })
}
const getStatusType = (s:string) => ({'在库':'success','出库':'info','借库':'warning','损耗':'danger'}[s]||'warning')
@ -702,4 +747,4 @@ onMounted(() => fetchData())
.camera-card:hover { border-color: #409EFF; color: #409EFF; }
.camera-card .text { font-size: 12px; margin-top: 5px; }
.camera-card .el-icon { font-size: 24px; }
</style>
</style>