diff --git a/inventory-backend/app/api/v1/inbound/product.py b/inventory-backend/app/api/v1/inbound/product.py index d2c69d7..0536660 100644 --- a/inventory-backend/app/api/v1/inbound/product.py +++ b/inventory-backend/app/api/v1/inbound/product.py @@ -7,19 +7,27 @@ inbound_product_bp = Blueprint('inbound_product', __name__) # ------------------------------------------------------------------ -# 0. 基础物料搜索 +# 0. 基础物料搜索 (关键接口:配合 Service 实现自动回填) # ------------------------------------------------------------------ @inbound_product_bp.route('/search-base', methods=['GET']) def search_base(): + """ + 对应前端 API: /inbound/product/search-base + 功能: 模糊搜索基础物料,返回 spec, unit, category, type 等详细信息 + """ try: - data = ProductInboundService.search_base_material(request.args.get('keyword', '')) + keyword = request.args.get('keyword', '') + # 调用 Service 层已修复的 search_base_material 方法 + data = ProductInboundService.search_base_material(keyword) return jsonify({"code": 200, "msg": "success", "data": data}) except Exception as e: + # 捕获异常并打印堆栈,方便调试 + traceback.print_exc() return jsonify({"code": 500, "msg": str(e)}), 500 # ------------------------------------------------------------------ -# 1. 获取列表 (修改:接收 status 参数) +# 1. 获取列表 (支持 status 多选筛选) # ------------------------------------------------------------------ @inbound_product_bp.route('/list', methods=['GET']) def get_list(): @@ -27,13 +35,15 @@ def get_list(): page = request.args.get('page', 1, type=int) limit = request.args.get('pageSize', 15, type=int) keyword = request.args.get('keyword', '') - # 接收状态参数 + + # 接收状态参数 (逗号分隔字符串 -> 列表) statuses_str = request.args.get('statuses', '') statuses = statuses_str.split(',') if statuses_str else [] result = ProductInboundService.get_list(page, limit, keyword, statuses) return jsonify({"code": 200, "msg": "success", "data": result}) except Exception as e: + traceback.print_exc() return jsonify({"code": 500, "msg": str(e)}), 500 @@ -46,7 +56,7 @@ def submit(): # 调用 Service 处理入库,获取新创建的对象 new_stock = ProductInboundService.handle_inbound(request.get_json()) - # 返回成功信息以及新创建的数据(包含生成的ID和SKU),供前端打印使用 + # 返回成功信息以及新创建的数据(包含生成的ID和SKU),供前端自动打印使用 return jsonify({ "code": 200, "msg": "入库成功", @@ -66,6 +76,7 @@ def update(id): ProductInboundService.update_inbound(id, request.get_json()) return jsonify({"code": 200, "msg": "更新成功"}) except Exception as e: + traceback.print_exc() return jsonify({"code": 500, "msg": str(e)}), 500 @@ -78,11 +89,12 @@ def delete(id): ProductInboundService.delete_inbound(id) return jsonify({"code": 200, "msg": "删除成功"}) except Exception as e: + traceback.print_exc() return jsonify({"code": 500, "msg": str(e)}), 500 # ------------------------------------------------------------------ -# 5. [新增] 获取出库历史 +# 5. 获取出库历史 # ------------------------------------------------------------------ @inbound_product_bp.route('//history', methods=['GET']) def get_history(id): @@ -90,4 +102,5 @@ def get_history(id): data = ProductInboundService.get_outbound_history(id) return jsonify({"code": 200, "msg": "success", "data": data}) except Exception as e: + traceback.print_exc() return jsonify({"code": 500, "msg": str(e)}), 500 \ No newline at end of file diff --git a/inventory-backend/app/services/inbound/product_service.py b/inventory-backend/app/services/inbound/product_service.py index 101b3ff..fba3fc5 100644 --- a/inventory-backend/app/services/inbound/product_service.py +++ b/inventory-backend/app/services/inbound/product_service.py @@ -2,30 +2,46 @@ from app.extensions import db from app.models.base import MaterialBase from app.models.outbound import TransOutbound -from datetime import datetime, timedelta, timezone # [修改] +from datetime import datetime, timedelta, timezone from sqlalchemy import or_, func, text, and_ import traceback import json class ProductInboundService: + + # ============================================================ + # 1. 基础物料搜索 (已修正:完全对齐 Buy/Semi 的逻辑) + # ============================================================ @staticmethod def search_base_material(keyword): try: - if not keyword: - query = MaterialBase.query.filter(MaterialBase.is_enabled == True).order_by( - MaterialBase.id.desc()).limit(20) - else: - query = MaterialBase.query.filter( - MaterialBase.is_enabled == True, - or_(MaterialBase.name.ilike(f'%{keyword}%'), MaterialBase.spec_model.ilike(f'%{keyword}%')) - ).limit(20) + # 1. 基础查询:必须是已启用的物料 + query = MaterialBase.query.filter(MaterialBase.is_enabled == True) + # 2. 动态条件:如果传入了关键词,则增加模糊匹配条件 + if keyword: + query = query.filter( + or_( + MaterialBase.name.ilike(f'%{keyword}%'), + MaterialBase.spec_model.ilike(f'%{keyword}%') + ) + ) + + # 3. 排序与限制:按ID倒序,取最新20条 + query = query.order_by(MaterialBase.id.desc()).limit(20) + + # 4. 结果封装:确保字段名与前端 Vue 的 handleSelect 方法一致 results = [] for item in query.all(): results.append({ - 'id': item.id, 'name': item.name, 'spec': item.spec_model, - 'category': item.category, 'unit': item.unit, 'type': item.material_type + 'id': item.id, + 'name': item.name, + 'spec': item.spec_model, # 对应前端: item.spec + 'category': item.category, # 对应前端: item.category + 'unit': item.unit, # 对应前端: item.unit + 'type': item.material_type, # 对应前端: item.type + 'status': '启用' }) return results except Exception: @@ -129,6 +145,9 @@ class ProductInboundService: db.session.rollback() raise e + # ============================================================ + # 3. 更新逻辑 + # ============================================================ @staticmethod def update_inbound(stock_id, data): from app.models.inbound.product import StockProduct @@ -184,6 +203,9 @@ class ProductInboundService: db.session.rollback() raise e + # ============================================================ + # 4. 删除逻辑 + # ============================================================ @staticmethod def delete_inbound(stock_id): from app.models.inbound.product import StockProduct @@ -197,6 +219,9 @@ class ProductInboundService: db.session.rollback() raise e + # ============================================================ + # 5. 出库历史 + # ============================================================ @staticmethod def get_outbound_history(stock_id): """获取出库历史""" @@ -209,7 +234,7 @@ class ProductInboundService: return [] # ============================================================ - # 获取列表 (修改:按时间倒序排序 + 展示只显示日期) + # 6. 获取列表 # ============================================================ @staticmethod def get_list(page, limit, keyword=None, statuses=None): @@ -240,7 +265,7 @@ class ProductInboundService: ) ) - # [核心修改] 按照 production_date (入库日期) 倒序排序 + # 按照 production_date (入库日期) 倒序排序 pagination = query.order_by(StockProduct.production_date.desc()).paginate(page=page, per_page=limit, error_out=False) @@ -257,7 +282,7 @@ class ProductInboundService: for item in current_items: d = item.to_dict() - # [核心修改] 格式化日期 + # 格式化日期 date_display = '' if item.production_date: try: diff --git a/inventory-backend/app/services/inbound/semi_service.py b/inventory-backend/app/services/inbound/semi_service.py index 40273b8..6d82a54 100644 --- a/inventory-backend/app/services/inbound/semi_service.py +++ b/inventory-backend/app/services/inbound/semi_service.py @@ -2,36 +2,44 @@ from app.extensions import db from app.models.base import MaterialBase from app.models.outbound import TransOutbound -from datetime import datetime, timedelta, timezone # [修改] +from datetime import datetime, timedelta, timezone from sqlalchemy import or_, func, text, and_ import traceback import json class SemiInboundService: + + # ============================================================ + # 1. 基础物料搜索 (已修复:支持空关键词返回最新数据) + # ============================================================ @staticmethod def search_base_material(keyword): try: - if not keyword: - return [] + # 基础查询:必须是已启用的物料 + query = MaterialBase.query.filter(MaterialBase.is_enabled == True) - query = MaterialBase.query.filter( - MaterialBase.is_enabled == True, - or_( - MaterialBase.name.ilike(f'%{keyword}%'), - MaterialBase.spec_model.ilike(f'%{keyword}%') + # 如果有关键词,进行模糊匹配 + if keyword: + query = query.filter( + or_( + MaterialBase.name.ilike(f'%{keyword}%'), + MaterialBase.spec_model.ilike(f'%{keyword}%') + ) ) - ).limit(20) + + # 统一逻辑:按ID倒序,限制20条 + query = query.order_by(MaterialBase.id.desc()).limit(20) results = [] for item in query.all(): results.append({ 'id': item.id, 'name': item.name, - 'spec': item.spec_model, + 'spec': item.spec_model, # 对应前端 item.spec 'category': item.category, 'unit': item.unit, - 'type': item.material_type, + 'type': item.material_type, # 对应前端 item.type 'status': '启用' }) return results @@ -39,6 +47,9 @@ class SemiInboundService: traceback.print_exc() return [] + # ============================================================ + # 2. 新增入库逻辑 + # ============================================================ @staticmethod def handle_inbound(data): from app.models.inbound.semi import StockSemi @@ -171,6 +182,9 @@ class SemiInboundService: traceback.print_exc() raise e + # ============================================================ + # 3. 更新逻辑 + # ============================================================ @staticmethod def update_inbound(stock_id, data): from app.models.inbound.semi import StockSemi @@ -268,6 +282,9 @@ class SemiInboundService: db.session.rollback() raise e + # ============================================================ + # 4. 删除逻辑 + # ============================================================ @staticmethod def delete_inbound(stock_id): from app.models.inbound.semi import StockSemi @@ -282,6 +299,9 @@ class SemiInboundService: db.session.rollback() raise e + # ============================================================ + # 5. 出库历史 + # ============================================================ @staticmethod def get_outbound_history(stock_id): """获取出库历史""" @@ -294,7 +314,7 @@ class SemiInboundService: return [] # ============================================================ - # 6. 获取列表 (修改:按时间倒序排序 + 展示只显示日期) + # 6. 获取列表 # ============================================================ @staticmethod def get_list(page, limit, keyword=None, statuses=None): @@ -329,7 +349,7 @@ class SemiInboundService: ) ) - # [核心修改] 按照 production_date (入库日期) 倒序排序 + # 按照 production_date (入库日期) 倒序排序 pagination = query.order_by(StockSemi.production_date.desc()).paginate(page=page, per_page=limit, error_out=False) @@ -346,7 +366,7 @@ class SemiInboundService: for item in current_items: d = item.to_dict() - # [核心修改] 格式化展示日期,覆盖 to_dict 的默认行为 + # 格式化展示日期 date_display = '' if item.production_date: try: diff --git a/inventory-web/src/api/inbound/product.ts b/inventory-web/src/api/inbound/product.ts index b5dad64..fe045cf 100644 --- a/inventory-web/src/api/inbound/product.ts +++ b/inventory-web/src/api/inbound/product.ts @@ -35,7 +35,7 @@ export function deleteProductInbound(id: number) { export function searchMaterialBase(keyword: string) { return request({ - url: '/inbound/base/search', + url: '/inbound/product/search-base', method: 'get', params: { keyword } }) diff --git a/inventory-web/src/views/stock/inbound/product.vue b/inventory-web/src/views/stock/inbound/product.vue index 3e03ea2..676b64c 100644 --- a/inventory-web/src/views/stock/inbound/product.vue +++ b/inventory-web/src/views/stock/inbound/product.vue @@ -177,7 +177,9 @@ + + @@ -230,21 +232,10 @@
- + -
- 拍照 -
+
拍照
@@ -255,21 +246,10 @@
- + -
- 拍照 -
+
拍照
@@ -279,21 +259,10 @@
- + -
- 拍照 -
+
拍照
@@ -315,15 +284,7 @@ - + @@ -368,10 +329,7 @@ Label Preview
正在生成预览...
-
-

打印机 IP: 192.168.9.205

-

尺寸: 40mm x 30mm

-
+

打印机 IP: 192.168.9.205

尺寸: 40mm x 30mm

@@ -616,7 +438,7 @@ import { deleteSemiInbound, searchMaterialBase } from '@/api/inbound/semi' -import { uploadFile, deleteFile } from '@/api/inbound/buy' // 复用文件上传接口 +import { uploadFile, deleteFile } from '@/api/inbound/buy' import {getLabelPreview, executePrint} from '@/api/common/print' // ------------------------------------ @@ -630,12 +452,7 @@ const dialogStatus = ref<'create' | 'update'>('create') const tableData = ref([]) const total = ref(0) const formRef = ref() -const queryParams = reactive({ - page: 1, - pageSize: 15, - keyword: '', - statuses: ['在库', '借库'] -}) +const queryParams = reactive({ page: 1, pageSize: 15, keyword: '', statuses: ['在库', '借库'] }) const materialOptions = ref([]) // 打印相关变量 @@ -652,7 +469,7 @@ const arrivalFileList = ref([]) const reportFileList = ref([]) const cameraInputRef = ref(null) const currentCameraField = ref<'arrival_photo' | 'quality_report_link'>('arrival_photo') -const quality_report_url = ref('') // 外部链接输入 +const quality_report_url = ref('') const entryMode = ref('batch') const modeLocked = ref(false) @@ -666,17 +483,13 @@ const baseColumns = [ {prop: 'unit', label: '单位'}, ] -// 半成品特有列 const stockColumns = [ {prop: 'id', label: 'ID', minWidth: '60'}, {prop: 'base_id', label: 'BaseID', minWidth: '80'}, {prop: 'sku', label: 'SKU', minWidth: '120'}, {prop: 'inbound_date', label: '入库日期', minWidth: '120'}, {prop: 'barcode', label: '条码', minWidth: '120'}, - - // 修改:合并序列号和批号 {prop: 'sn_bn', label: '序列号/批号', minWidth: '160'}, - {prop: 'status', label: '状态', minWidth: '100'}, {prop: 'quality_status', label: '质量状态', minWidth: '100'}, {prop: 'qty_inbound', label: '入库量', minWidth: '100'}, @@ -696,65 +509,26 @@ const stockColumns = [ {prop: 'quality_report_link', label: '质量报告', minWidth: '100'}, {prop: 'detail_link', label: '详情链接', minWidth: '100'}, ] - const allColumns = [...baseColumns, ...stockColumns] -// 表头持久化 -const STORAGE_KEY = 'stock_semi_visible_columns_v2' // Update key to force refresh -const defaultColumns = [ - 'material_name', 'spec_model', 'unit', - 'inbound_date', 'sn_bn', 'status', 'quality_status', - 'bom_code', 'work_order_code', 'qty_stock', 'qty_available', 'unit_total_cost', 'arrival_photo', 'quality_report_link' -] - -const getSavedColumns = () => { - try { - const saved = localStorage.getItem(STORAGE_KEY) - return saved ? JSON.parse(saved) : defaultColumns - } catch (e) { - return defaultColumns - } -} - +const STORAGE_KEY = 'stock_semi_visible_columns_v2' +const defaultColumns = ['material_name', 'spec_model', 'unit', 'inbound_date', 'sn_bn', 'status', 'quality_status', 'bom_code', 'work_order_code', 'qty_stock', 'qty_available', 'unit_total_cost', 'arrival_photo', 'quality_report_link'] +const getSavedColumns = () => { try { const saved = localStorage.getItem(STORAGE_KEY); return saved ? JSON.parse(saved) : defaultColumns } catch (e) { return defaultColumns } } const visibleColumnProps = ref(getSavedColumns()) - -watch(visibleColumnProps, (newVal) => { - localStorage.setItem(STORAGE_KEY, JSON.stringify(newVal)) -}, {deep: true}) - +watch(visibleColumnProps, (newVal) => { localStorage.setItem(STORAGE_KEY, JSON.stringify(newVal)) }, {deep: true}) const form = reactive({ - id: undefined, - base_id: undefined as number | undefined, - material_name: '', spec_model: '', category: '', unit: '', material_type: '', - sku: '', barcode: '', in_date: '', - serial_number: '', batch_number: '', - status: '在库', - quality_status: '合格', - in_quantity: 1, stock_quantity: 1, available_quantity: 1, - warehouse_location: '', - // 半成品特有 - bom_code: '', - bom_version: '', - work_order_code: '', - raw_material_cost: 0, - manual_cost: 0, - unit_total_cost: 0, - production_manager: '', - production_time_range: [] as string[], - arrival_photo: [] as string[], - quality_report_link: [] as string[], - detail_link: '' + id: undefined, base_id: undefined as number | undefined, material_name: '', spec_model: '', category: '', unit: '', material_type: '', + sku: '', barcode: '', in_date: '', serial_number: '', batch_number: '', status: '在库', quality_status: '合格', + in_quantity: 1, stock_quantity: 1, available_quantity: 1, warehouse_location: '', + bom_code: '', bom_version: '', work_order_code: '', raw_material_cost: 0, manual_cost: 0, unit_total_cost: 0, + production_manager: '', production_time_range: [] as string[], arrival_photo: [] as string[], quality_report_link: [] as string[], detail_link: '' }) // ------------------------------------ // 历史记录管理器 // ------------------------------------ -const HISTORY_KEYS = { - PRODUCTION_MANAGER: 'history_production_managers', - MATERIAL: 'history_semi_materials' -} - +const HISTORY_KEYS = { PRODUCTION_MANAGER: 'history_production_managers', MATERIAL: 'history_semi_materials' } const saveToHistory = (key: string, value: string) => { if (!value) return try { @@ -766,11 +540,7 @@ const saveToHistory = (key: string, value: string) => { localStorage.setItem(key, JSON.stringify(list)) } catch (e) { console.error('save history failed', e) } } - -const getHistoryList = (key: string): any[] => { - try { return (JSON.parse(localStorage.getItem(key) || '[]')).map((v: string) => ({value: v})) } catch (e) { return [] } -} - +const getHistoryList = (key: string): any[] => { try { return (JSON.parse(localStorage.getItem(key) || '[]')).map((v: string) => ({value: v})) } catch (e) { return [] } } const saveMaterialHistory = (item: any) => { if (!item || !item.id) return const key = HISTORY_KEYS.MATERIAL @@ -782,17 +552,13 @@ const saveMaterialHistory = (item: any) => { localStorage.setItem(key, JSON.stringify(list)) } catch (e) {} } - -const getMaterialHistory = () => { - try { return JSON.parse(localStorage.getItem(HISTORY_KEYS.MATERIAL) || '[]') } catch (e) { return [] } -} +const getMaterialHistory = () => { try { return JSON.parse(localStorage.getItem(HISTORY_KEYS.MATERIAL) || '[]') } catch (e) { return [] } } // ------------------------------------ -// Autocomplete Logic +// Autocomplete & Search Logic // ------------------------------------ const createFilter = (queryString: string) => { return (item: any) => (item.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0) } const getTableDataUnique = (field: string) => { return Array.from(new Set(tableData.value.map((i: any) => i[field]).filter(Boolean))).map(i => ({value: i})) } - const mixedSearch = (queryString: string, tableField: string, storageKey: string, cb: any) => { const tableList = getTableDataUnique(tableField) const historyList = getHistoryList(storageKey) @@ -803,10 +569,39 @@ const mixedSearch = (queryString: string, tableField: string, storageKey: string const results = queryString ? allList.filter(createFilter(queryString)) : allList cb(results) } - const querySearchManager = (qs: string, cb: any) => mixedSearch(qs, 'production_manager', HISTORY_KEYS.PRODUCTION_MANAGER, cb) const handleManagerSelect = (item: any) => saveToHistory(HISTORY_KEYS.PRODUCTION_MANAGER, item.value) +// ------------------------------------ +// Material Search (Matches Buy.vue) +// ------------------------------------ +const handleMaterialDropdownVisible = (visible: boolean) => { if (visible && materialOptions.value.length === 0) handleSearchMaterial('') } +const handleSearchMaterial = async (query: string) => { + searchLoading.value = true + try { + const res: any = await searchMaterialBase(query) + const apiResults = (res.data || []).map((i: any) => ({...i, isHistory: false})) + if (!query) { + const history = getMaterialHistory() + const historyIds = new Set(history.map((h: any) => h.id)) + materialOptions.value = [...history, ...apiResults.filter((apiItem: any) => !historyIds.has(apiItem.id))] + } else { materialOptions.value = apiResults } + } finally { searchLoading.value = false } +} +const onMaterialSelected = (val: number) => { + const item = materialOptions.value.find(i => i.id === val) + if (item) { + saveMaterialHistory(item) + // Populate form fields + form.material_name = item.name + form.spec_model = item.spec + form.category = item.category + form.unit = item.unit + form.material_type = item.type + // Trigger batch/serial logic specific to Semi + checkHistoryAndSetMode(item.id) + } +} // ------------------------------------ // Validation Logic @@ -822,13 +617,11 @@ const validateUnique = (rule: any, value: string, callback: any) => { if (isDuplicate) callback(new Error('编号重复')) else callback() } - const validateIdentity = (rule: any, value: any, callback: any) => { if (entryMode.value === 'serial' && !form.serial_number && rule.field === 'serial_number') callback(new Error('SN必填')) else if (entryMode.value === 'batch' && !form.batch_number && rule.field === 'batch_number') callback(new Error('批号必填')) else callback() } - const rules = { base_id: [{required: true, message: '请选择物料', trigger: 'change'}], in_quantity: [{required: true, message: '请输入数量', trigger: 'blur'}], @@ -846,64 +639,22 @@ const checkHistoryAndSetMode = async (baseId: number) => { if (historyItems.length > 0) { modeLocked.value = true const latest = historyItems.sort((a: any, b: any) => b.id - a.id)[0] - if (latest.serial_number) { - entryMode.value = 'serial'; form.serial_number = ''; form.batch_number = '' - } else { - entryMode.value = 'batch'; form.serial_number = ''; form.batch_number = incrementBatchNumber(latest.batch_number || '000000') - } - } else { - modeLocked.value = false; entryMode.value = 'batch'; form.batch_number = '000001' - } + if (latest.serial_number) { entryMode.value = 'serial'; form.serial_number = ''; form.batch_number = '' } + else { entryMode.value = 'batch'; form.serial_number = ''; form.batch_number = incrementBatchNumber(latest.batch_number || '000000') } + } else { modeLocked.value = false; entryMode.value = 'batch'; form.batch_number = '000001' } if(formRef.value) { formRef.value.clearValidate('serial_number'); formRef.value.clearValidate('batch_number') } - } catch (e) { - modeLocked.value = false; entryMode.value = 'batch'; form.batch_number = '000001' - } + } catch (e) { modeLocked.value = false; entryMode.value = 'batch'; form.batch_number = '000001' } } - const incrementBatchNumber = (batchStr: string) => { if (!batchStr || !/^\d+$/.test(batchStr)) return '000001' const num = parseInt(batchStr, 10) return (num + 1).toString().padStart(6, '0') } - const handleEntryModeChange = (val: string) => { - if (val === 'batch') { - form.serial_number = ''; form.batch_number = '000001'; if(formRef.value) formRef.value.clearValidate('serial_number') - } else { - form.batch_number = ''; if(formRef.value) formRef.value.clearValidate('batch_number') - } + if (val === 'batch') { form.serial_number = ''; form.batch_number = '000001'; if(formRef.value) formRef.value.clearValidate('serial_number') } + else { form.batch_number = ''; if(formRef.value) formRef.value.clearValidate('batch_number') } } - -const handleMaterialDropdownVisible = (visible: boolean) => { if (visible && materialOptions.value.length === 0) handleSearchMaterial('') } -const handleSearchMaterial = async (query: string) => { - searchLoading.value = true - try { - const res: any = await searchMaterialBase(query) - const apiResults = (res.data || []).map((i: any) => ({...i, isHistory: false})) - if (!query) { - const history = getMaterialHistory() - const historyIds = new Set(history.map((h: any) => h.id)) - materialOptions.value = [...history, ...apiResults.filter((apiItem: any) => !historyIds.has(apiItem.id))] - } else { materialOptions.value = apiResults } - } finally { searchLoading.value = false } -} - -const onMaterialSelected = (val: number) => { - const item = materialOptions.value.find(i => i.id === val) - if (item) { - saveMaterialHistory(item) - form.material_name = item.name - form.spec_model = item.spec - form.category = item.category - form.unit = item.unit - form.material_type = item.type - checkHistoryAndSetMode(item.id) - } -} - -watch(() => [form.raw_material_cost, form.manual_cost], () => { - form.unit_total_cost = Number((form.raw_material_cost + form.manual_cost).toFixed(2)) -}) +watch(() => [form.raw_material_cost, form.manual_cost], () => { form.unit_total_cost = Number((form.raw_material_cost + form.manual_cost).toFixed(2)) }) const fetchData = async () => { loading.value = true @@ -930,8 +681,6 @@ const handleUpdate = (row: any) => { dialogStatus.value = 'update' resetForm() modeLocked.value = true - - // 基础字段映射 Object.assign(form, { id: row.id, base_id: row.base_id, material_name: row.material_name, spec_model: row.spec_model, category: row.category, unit: row.unit, material_type: row.material_type, sku: row.sku, barcode: row.barcode, in_date: row.inbound_date, @@ -942,159 +691,80 @@ const handleUpdate = (row: any) => { production_manager: row.production_manager, production_time_range: (row.production_start_time && row.production_end_time) ? [row.production_start_time, row.production_end_time] : [], detail_link: row.detail_link, - // 图片列表处理 - arrival_photo: row.arrival_photo || [], - quality_report_link: row.quality_report_link || [] + arrival_photo: row.arrival_photo || [], quality_report_link: row.quality_report_link || [] }) - - // 1. 同步到货图片 arrivalFileList.value = form.arrival_photo.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) })) - - // 2. 分离质量报告中的图片和链接 const reports = form.quality_report_link || [] const reportImgs = reports.filter(r => !isExternalLink(r)) const reportLinks = reports.filter(r => isExternalLink(r)) - reportFileList.value = reportImgs.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) })) quality_report_url.value = reportLinks.length > 0 ? reportLinks[0] : '' - - // 模式设定 - if (row.serial_number) { - entryMode.value = 'serial'; form.serial_number = row.serial_number; form.batch_number = '' - } else { - entryMode.value = 'batch'; form.batch_number = row.batch_number; form.serial_number = '' - } - - materialOptions.value = [{ id: row.base_id, name: row.material_name, spec: row.spec_model, category: row.category }] + if (row.serial_number) { entryMode.value = 'serial'; form.serial_number = row.serial_number; form.batch_number = '' } + else { entryMode.value = 'batch'; form.batch_number = row.batch_number; form.serial_number = '' } + materialOptions.value = [{ id: row.base_id, name: row.material_name, spec: row.spec_model, category: row.category, isHistory: false }] visible.value = true } -// ------------------------------------ -// 图片上传、拍照、删除逻辑 -// ------------------------------------ -const getImageUrl = (url: string) => { - if (!url) return '' - if (url.startsWith('http')) return url - return url -} - -const isExternalLink = (str: string) => { - return str && (str.startsWith('http://') || str.startsWith('https://')) && !str.includes('/api/v1/common/files') -} - -const getImagesOnly = (list: string[]) => { - if (!list) return [] - return list.filter(item => !isExternalLink(item)) -} - -const hasExternalLink = (list: string[]) => { - if (!list) return false - return list.some(item => isExternalLink(item)) -} - +const getImageUrl = (url: string) => { if (!url) return ''; return url.startsWith('http') ? url : url } +const isExternalLink = (str: string) => { return str && (str.startsWith('http://') || str.startsWith('https://')) && !str.includes('/api/v1/common/files') } +const getImagesOnly = (list: string[]) => { return !list ? [] : list.filter(item => !isExternalLink(item)) } +const hasExternalLink = (list: string[]) => { return !list ? false : list.some(item => isExternalLink(item)) } const beforeAvatarUpload = (rawFile: any) => { if (rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/png') { ElMessage.error('仅支持 JPG/PNG'); return false } if (rawFile.size / 1024 / 1024 > 5) { ElMessage.error('图片不能超过 5MB'); return false } return true } - const customUpload = async (options: any, targetField: 'arrival_photo' | 'quality_report_link') => { const { file, onSuccess, onError } = options - const formData = new FormData() - formData.append('file', file) + const formData = new FormData(); formData.append('file', file) try { const res: any = await uploadFile(formData) if (res.code === 200) { - const newUrl = res.data.url - form[targetField].push(newUrl) - ElMessage.success('上传成功') - onSuccess(res) - } else { - ElMessage.error(res.msg || '上传失败') - onError(new Error(res.msg)) - } + const newUrl = res.data.url; form[targetField].push(newUrl); ElMessage.success('上传成功'); onSuccess(res) + } else { ElMessage.error(res.msg || '上传失败'); onError(new Error(res.msg)) } } catch (e) { ElMessage.error('网络错误'); onError(e) } } - const handleRemoveImage = async (uploadFile: any, targetField: 'arrival_photo' | 'quality_report_link') => { try { const urlToRemove = form[targetField].find(u => getImageUrl(u) === uploadFile.url) || uploadFile.url form[targetField] = form[targetField].filter(u => u !== urlToRemove) - if (!isExternalLink(urlToRemove)) { - const filename = urlToRemove.split('/').pop() - if (filename) await deleteFile(filename) - } + if (!isExternalLink(urlToRemove)) { const filename = urlToRemove.split('/').pop(); if (filename) await deleteFile(filename) } ElMessage.success('已删除') } catch (e) { console.error(e) } } - -const handlePreviewPicture = (uploadFile: any) => { - dialogImageUrl.value = uploadFile.url! - dialogVisibleImage.value = true -} - -const triggerCamera = (field: 'arrival_photo' | 'quality_report_link') => { - currentCameraField.value = field - if (cameraInputRef.value) cameraInputRef.value.click() -} - +const handlePreviewPicture = (uploadFile: any) => { dialogImageUrl.value = uploadFile.url!; dialogVisibleImage.value = true } +const triggerCamera = (field: 'arrival_photo' | 'quality_report_link') => { currentCameraField.value = field; if (cameraInputRef.value) cameraInputRef.value.click() } const handleCameraFile = async (event: Event) => { const input = event.target as HTMLInputElement if (input.files && input.files[0]) { const file = input.files[0] if (!beforeAvatarUpload(file)) { input.value = ''; return } - const formData = new FormData() - formData.append('file', file) + const formData = new FormData(); formData.append('file', file) const loadingMsg = ElMessage.loading({ message: '照片上传中...', duration: 0 }) try { const res: any = await uploadFile(formData) if (res.code === 200) { - const newUrl = res.data.url - const field = currentCameraField.value + const newUrl = res.data.url; const field = currentCameraField.value form[field].push(newUrl) - if (field === 'arrival_photo') { - arrivalFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) }) - } else { - reportFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) }) - } + if (field === 'arrival_photo') arrivalFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) }) + else reportFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) }) ElMessage.success('拍照上传成功') } else { ElMessage.error(res.msg || '上传失败') } - } catch (e) { ElMessage.error('网络错误,上传失败') } - finally { loadingMsg.close(); input.value = '' } + } catch (e) { ElMessage.error('网络错误,上传失败') } finally { loadingMsg.close(); input.value = '' } } } -// ------------------------------------ -// Submit Logic -// ------------------------------------ const submitForm = async () => { if (!formRef.value) return await formRef.value.validate(async (valid: boolean) => { if (valid) { submitting.value = true - - // 合并外部链接到 quality_report_link 数组 const finalReportList = [...form.quality_report_link] - if (quality_report_url.value && !finalReportList.includes(quality_report_url.value)) { - finalReportList.push(quality_report_url.value) - } - // 确保如果是修改模式下清空了输入框,只保留图片(重新构建只包含图片+当前输入链接的列表) + if (quality_report_url.value && !finalReportList.includes(quality_report_url.value)) finalReportList.push(quality_report_url.value) const onlyImages = finalReportList.filter(item => !isExternalLink(item)) - if (quality_report_url.value) { - onlyImages.push(quality_report_url.value) - } - - const payload: any = { - ...form, - quality_report_link: onlyImages, - in_quantity: Number(form.in_quantity), - raw_material_cost: Number(form.raw_material_cost), - manual_cost: Number(form.manual_cost), - production_start_time: form.production_time_range?.[0] || null, - production_end_time: form.production_time_range?.[1] || null - } + if (quality_report_url.value) onlyImages.push(quality_report_url.value) + const payload: any = { ...form, quality_report_link: onlyImages, in_quantity: Number(form.in_quantity), raw_material_cost: Number(form.raw_material_cost), manual_cost: Number(form.manual_cost), production_start_time: form.production_time_range?.[0] || null, production_end_time: form.production_time_range?.[1] || null } delete payload.production_time_range - try { if (dialogStatus.value === 'create') { const res: any = await createSemiInbound(payload) @@ -1105,68 +775,25 @@ const submitForm = async () => { try { await executePrint(newItem); ElMessage.success('打印指令已发送') } catch (printErr: any) { ElMessage.warning('入库成功,但自动打印失败:' + (printErr.msg || '未知错误')) } } - } else { - await updateSemiInbound(form.id!, payload) - ElMessage.success('更新成功') - } + } else { await updateSemiInbound(form.id!, payload); ElMessage.success('更新成功') } saveToHistory(HISTORY_KEYS.PRODUCTION_MANAGER, form.production_manager) - await fetchData() - visible.value = false - } catch (e: any) { ElMessage.error(e.msg || '操作失败') } - finally { submitting.value = false } + await fetchData(); visible.value = false + } catch (e: any) { ElMessage.error(e.msg || '操作失败') } finally { submitting.value = false } } }) } -const handleDelete = async (row: any) => { - try { await deleteSemiInbound(row.id); ElMessage.success('删除成功'); fetchData() } - catch (e) { ElMessage.error('删除失败') } -} - +const handleDelete = async (row: any) => { try { await deleteSemiInbound(row.id); ElMessage.success('删除成功'); fetchData() } catch (e) { ElMessage.error('删除失败') } } const handlePrint = async (row: any) => { printVisible.value = true; printLoading.value = true; previewUrl.value = '' - const printData = { - global_print_id: row.global_print_id, - material_name: row.material_name, - spec_model: row.spec_model, - category: row.category, - material_type: row.material_type, - warehouse_loc: row.warehouse_loc, - serial_number: row.serial_number, - batch_number: row.batch_number, - sku: row.sku - } - currentPrintData.value = printData - try { const res: any = await getLabelPreview(printData); previewUrl.value = res.data } - catch (e) { ElMessage.error('预览生成失败') } finally { printLoading.value = false } + currentPrintData.value = { global_print_id: row.global_print_id, material_name: row.material_name, spec_model: row.spec_model, category: row.category, material_type: row.material_type, warehouse_loc: row.warehouse_loc, serial_number: row.serial_number, batch_number: row.batch_number, sku: row.sku } + try { const res: any = await getLabelPreview(currentPrintData.value); previewUrl.value = res.data } catch (e) { ElMessage.error('预览生成失败') } finally { printLoading.value = false } } - -const confirmPrint = async () => { - printing.value = true - try { await executePrint(currentPrintData.value); ElMessage.success('指令已发送'); printVisible.value = false } - catch (e: any) { ElMessage.error(e.msg || '打印失败') } finally { printing.value = false } -} - +const confirmPrint = async () => { printing.value = true; try { await executePrint(currentPrintData.value); ElMessage.success('指令已发送'); printVisible.value = false } catch (e: any) { ElMessage.error(e.msg || '打印失败') } finally { printing.value = false } } const resetForm = () => { - materialOptions.value = [] - arrivalFileList.value = [] - reportFileList.value = [] - quality_report_url.value = '' - Object.assign(form, { - id: undefined, base_id: undefined, - material_name: '', spec_model: '', category: '', unit: '', material_type: '', - sku: '', barcode: '', in_date: '', - serial_number: '', batch_number: '', - status: '在库', quality_status: '合格', - in_quantity: 1, stock_quantity: 1, available_quantity: 1, - warehouse_location: '', - bom_code: '', bom_version: '', work_order_code: '', - raw_material_cost: 0, manual_cost: 0, unit_total_cost: 0, - production_manager: '', production_time_range: [], - arrival_photo: [], quality_report_link: [], detail_link: '' - }) + materialOptions.value = []; arrivalFileList.value = []; reportFileList.value = []; quality_report_url.value = '' + Object.assign(form, { id: undefined, base_id: undefined, material_name: '', spec_model: '', category: '', unit: '', material_type: '', sku: '', barcode: '', in_date: '', serial_number: '', batch_number: '', status: '在库', quality_status: '合格', in_quantity: 1, stock_quantity: 1, available_quantity: 1, warehouse_location: '', bom_code: '', bom_version: '', work_order_code: '', raw_material_cost: 0, manual_cost: 0, unit_total_cost: 0, production_manager: '', production_time_range: [], arrival_photo: [], quality_report_link: [], detail_link: '' }) } - const getStatusType = (status: string) => { const map: any = { '在库': 'success', '出库': 'info', '借库': 'warning', '损耗': 'danger' }; return map[status] || 'warning' } const getQualityType = (status: string) => { const map: any = { '合格': 'success', '不合格': 'danger', '待检': 'info', '返修中': 'warning' }; return map[status] || 'info' } const formatMoney = (val: any) => { const num = Number(val); return isNaN(num) ? '-' : `¥ ${num.toFixed(2)}` } @@ -1175,16 +802,11 @@ onMounted(() => fetchData())