From ba3085c1f250b3b4f355c51b1a49b70c3575ff96 Mon Sep 17 00:00:00 2001 From: dxc Date: Tue, 3 Feb 2026 11:55:33 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=87=E8=B4=AD=E4=BB=B6=E5=9B=BE=E5=83=8F?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E5=88=9D=E5=AE=9E=E7=8E=B0=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=A4=9A=E5=9B=BE=EF=BC=8C=E6=A3=80=E6=B5=8B=E6=8A=A5?= =?UTF-8?q?=E5=91=8A=E7=9A=84=E5=9B=BE=E7=89=87=E4=BB=A5=E5=8F=8A=E9=93=BE?= =?UTF-8?q?=E6=8E=A5=E4=B8=8A=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- inventory-backend/app/models/inbound/buy.py | 33 +- .../app/services/inbound/buy_service.py | 76 +- inventory-web/src/views/stock/inbound/buy.vue | 1012 +++++------------ 3 files changed, 399 insertions(+), 722 deletions(-) diff --git a/inventory-backend/app/models/inbound/buy.py b/inventory-backend/app/models/inbound/buy.py index b9bf065..7492879 100644 --- a/inventory-backend/app/models/inbound/buy.py +++ b/inventory-backend/app/models/inbound/buy.py @@ -1,5 +1,6 @@ # app/models/inbound/buy.py from app.extensions import db +import json class StockBuy(db.Model): @@ -36,22 +37,35 @@ class StockBuy(db.Model): exchange_rate = db.Column(db.Numeric(15, 6), default=1.0) supplier_name = db.Column(db.String(255)) - buyer_name = db.Column(db.String(100)) - buyer_email = db.Column(db.String(100)) - original_link = db.Column(db.Text) + buyer_name = db.Column(db.String(100)) # 对应 SQL: buyer_name + buyer_email = db.Column(db.String(100)) # 对应 SQL: buyer_email + original_link = db.Column(db.Text) # 对应 SQL: original_link detail_link = db.Column(db.Text) - arrival_photo = db.Column(db.Text) - # [新增] 检测报告图片路径 + # 图片字段 (存储 JSON 字符串) + arrival_photo = db.Column(db.Text) + # [新增] 检测报告图片路径 (存储 JSON 字符串) inspection_report = db.Column(db.Text) - # 全局打印流水号 + # [新增] 全局打印流水号 (用于跨表连续编号,对应 Sequence: global_print_seq) global_print_id = db.Column(db.Integer) # 关系定义 material = db.relationship('MaterialBase', back_populates='stock_buys') def to_dict(self): + # 辅助解析函数:将数据库存储的 JSON 字符串转为 List + def parse_img_list(json_str): + if not json_str: + return [] + try: + # 兼容旧数据:如果不是 JSON 格式(比如是单个 URL),则包装成 list + if not json_str.startswith('['): + return [json_str] + return json.loads(json_str) + except: + return [] + return { 'id': self.id, 'base_id': self.base_id, @@ -87,11 +101,12 @@ class StockBuy(db.Model): 'purchaser_email': self.buyer_email, 'source_link': self.original_link, 'detail_link': self.detail_link, - 'arrival_photo': self.arrival_photo, - # [新增] 返回检测报告字段 - 'inspection_report': self.inspection_report, + # [修改] 解析为数组返回给前端 + 'arrival_photo': parse_img_list(self.arrival_photo), + 'inspection_report': parse_img_list(self.inspection_report), + # [新增] 返回全局打印ID及其格式化字符串 'global_print_id': self.global_print_id, 'global_print_id_str': f"{self.global_print_id:010d}" if self.global_print_id else "" } \ No newline at end of file diff --git a/inventory-backend/app/services/inbound/buy_service.py b/inventory-backend/app/services/inbound/buy_service.py index 2895687..34e4cec 100644 --- a/inventory-backend/app/services/inbound/buy_service.py +++ b/inventory-backend/app/services/inbound/buy_service.py @@ -5,6 +5,7 @@ from app.models.base import MaterialBase from datetime import datetime from sqlalchemy import or_, func, text import traceback +import json class BuyInboundService: @@ -46,6 +47,9 @@ class BuyInboundService: @staticmethod def handle_inbound(data): + """ + 处理入库逻辑 + """ try: base_id = data.get('base_id') if not base_id: @@ -83,17 +87,26 @@ class BuyInboundService: # ------------------------------------------------------------------ # 3. 条码逻辑处理 - # 如果前端没传条码(barcode),则默认使用系统生成的 SKU 作为条码 # ------------------------------------------------------------------ final_barcode = data.get('barcode') if not final_barcode: final_barcode = generated_sku + # ------------------------------------------------------------------ + # 4. 图片列表转 JSON 字符串处理 + # ------------------------------------------------------------------ + arrival_list = data.get('arrival_photo', []) + report_list = data.get('inspection_report', []) + + # 确保是列表类型,防止前端传错导致报错 + if not isinstance(arrival_list, list): arrival_list = [] + if not isinstance(report_list, list): report_list = [] + new_stock = StockBuy( base_id=material.id, global_print_id=next_global_id, - sku=generated_sku, - barcode=final_barcode, + sku=generated_sku, # 自动生成的SKU + barcode=final_barcode, # 如果未输入,则存入SKU值 in_date=in_date_val, serial_number=data.get('serial_number'), @@ -113,15 +126,16 @@ class BuyInboundService: buyer_email=data.get('purchaser_email'), original_link=data.get('source_link'), detail_link=data.get('detail_link'), - arrival_photo=data.get('arrival_photo'), - # [新增] 保存检测报告字段 - inspection_report=data.get('inspection_report') + # [核心修改] 将列表转为 JSON 字符串存储 + arrival_photo=json.dumps(arrival_list), + inspection_report=json.dumps(report_list) ) db.session.add(new_stock) db.session.commit() + # 返回创建的对象实例 return new_stock except Exception as e: @@ -130,6 +144,9 @@ class BuyInboundService: @staticmethod def update_inbound(stock_id, data): + """ + 更新入库记录 + """ try: print(f"----- UPDATE DEBUG: ID={stock_id} -----") @@ -137,6 +154,7 @@ class BuyInboundService: if not stock: raise ValueError("记录不存在") + # 基础字段映射 field_mapping = { 'sku': 'sku', 'barcode': 'barcode', @@ -147,21 +165,29 @@ class BuyInboundService: 'inspection_status': 'inspection_status', 'supplier_name': 'supplier_name', 'detail_link': 'detail_link', - 'arrival_photo': 'arrival_photo', 'currency': 'currency', 'exchange_rate': 'exchange_rate', 'purchaser': 'buyer_name', 'purchaser_email': 'buyer_email', - 'source_link': 'original_link', - - # [新增] 允许更新检测报告 - 'inspection_report': 'inspection_report' + 'source_link': 'original_link' } for frontend_key, db_attr in field_mapping.items(): if frontend_key in data: setattr(stock, db_attr, data[frontend_key]) + # [核心修改] 图片字段更新 (List -> JSON String) + if 'arrival_photo' in data: + imgs = data['arrival_photo'] + if isinstance(imgs, list): + stock.arrival_photo = json.dumps(imgs) + + if 'inspection_report' in data: + imgs = data['inspection_report'] + if isinstance(imgs, list): + stock.inspection_report = json.dumps(imgs) + + # 数量与金额联动更新逻辑 qty_changed = False price_changed = False @@ -197,6 +223,9 @@ class BuyInboundService: @staticmethod def delete_inbound(stock_id): + """ + 删除入库记录 + """ try: stock = StockBuy.query.get(stock_id) if not stock: @@ -210,7 +239,11 @@ class BuyInboundService: @staticmethod def get_list(page, limit, keyword=None): + """ + 获取分页列表 + """ try: + # 1. 查询分页数据 query = db.session.query(StockBuy).outerjoin(MaterialBase, StockBuy.base_id == MaterialBase.id) if keyword: @@ -226,6 +259,9 @@ class BuyInboundService: pagination = query.order_by(StockBuy.id.desc()).paginate(page=page, per_page=limit, error_out=False) + # --------------------------------------------------------------------- + # 计算总库存 (聚合) + # --------------------------------------------------------------------- current_items = pagination.items base_ids = list(set([item.base_id for item in current_items if item.base_id])) @@ -243,6 +279,18 @@ class BuyInboundService: 'total_avail': float(agg.total_avail or 0) } + # 辅助函数:解析JSON图片列表 + def parse_img_list(json_str): + if not json_str: + return [] + try: + # 兼容旧数据:如果不是 JSON 格式(比如是单个 URL),则包装成 list + if not json_str.startswith('['): + return [json_str] + return json.loads(json_str) + except: + return [] + items = [] for item in current_items: mat_name = item.material.name if item.material else '未知物料' @@ -287,10 +335,10 @@ class BuyInboundService: 'purchaser_email': item.buyer_email, 'source_link': item.original_link, 'detail_link': item.detail_link, - 'arrival_photo': item.arrival_photo, - # [新增] 返回检测报告 - 'inspection_report': item.inspection_report, + # [核心修改] 解析 JSON 字符串为数组返回给前端 + 'arrival_photo': parse_img_list(item.arrival_photo), + 'inspection_report': parse_img_list(item.inspection_report), 'global_print_id': item.global_print_id, 'global_print_id_str': f"{item.global_print_id:08d}" if item.global_print_id else "" diff --git a/inventory-web/src/views/stock/inbound/buy.vue b/inventory-web/src/views/stock/inbound/buy.vue index be0d46f..a733e3f 100644 --- a/inventory-web/src/views/stock/inbound/buy.vue +++ b/inventory-web/src/views/stock/inbound/buy.vue @@ -60,7 +60,13 @@ :min-width="col.minWidth || '140'" show-overflow-tooltip > - - + + + + Preview Image + + +
Label Preview
正在生成预览...
-

打印机 IP: 192.168.9.205

尺寸: 40mm x 30mm

@@ -587,7 +608,6 @@ const formRef = ref() const queryParams = reactive({page: 1, pageSize: 15, keyword: ''}) const materialOptions = ref([]) -// 打印相关变量 const printVisible = ref(false) const printLoading = ref(false) const printing = ref(false) @@ -597,8 +617,15 @@ const currentPrintData = ref({}) const entryMode = ref('batch') const modeLocked = ref(false) -// 拍照/上传相关 +// 图片预览/拍照相关 +const dialogImageUrl = ref('') +const dialogVisibleImage = ref(false) +const arrivalFileList = ref([]) +const reportFileList = ref([]) const cameraInputRef = ref(null) +const currentCameraField = ref<'arrival_photo' | 'inspection_report'>('arrival_photo') +// [新增] 检测报告外部链接输入框 +const inspection_report_url = ref('') // 列定义 const baseColumns = [ @@ -633,13 +660,12 @@ const stockColumns = [ {prop: 'source_link', label: '采购链接', minWidth: '100'}, {prop: 'detail_link', label: '详情链接', minWidth: '100'}, {prop: 'arrival_photo', label: '到货图', minWidth: '100'}, - {prop: 'inspection_report', label: '检测报告', minWidth: '100'} // 新增列 + {prop: 'inspection_report', label: '检测报告', minWidth: '100'} ] const allColumns = [...baseColumns, ...stockColumns] const STORAGE_KEY_COLS = 'stock_buy_visible_columns' -// 确保 arrival_photo 和 inspection_report 在默认列中 const defaultColumns = [ 'material_name', 'category', 'material_type', 'spec_model', 'unit', 'inbound_date', 'serial_number', 'batch_number', 'status', 'inspection_status', @@ -674,20 +700,14 @@ const form = reactive({ unit_price: 0, total_price: 0, currency: 'CNY', exchange_rate: 1.00, supplier_name: '', purchaser: '', purchaser_email: '', source_link: '', detail_link: '', - arrival_photo: '', - inspection_report: '' // 新增字段 + arrival_photo: [] as string[], + inspection_report: [] as string[] }) // ------------------------------------ -// 历史记录管理器 (Local Storage) +// 历史记录管理器 // ------------------------------------ -const HISTORY_KEYS = { - SUPPLIER: 'history_suppliers', - PURCHASER: 'history_purchasers', - EMAIL: 'history_emails', - MATERIAL: 'history_materials' -} - +const HISTORY_KEYS = { SUPPLIER: 'history_suppliers', PURCHASER: 'history_purchasers', EMAIL: 'history_emails', MATERIAL: 'history_materials' } const saveToHistory = (key: string, value: string) => { if (!value) return try { @@ -697,59 +717,31 @@ const saveToHistory = (key: string, value: string) => { list.unshift(value) if (list.length > 20) list = list.slice(0, 20) localStorage.setItem(key, JSON.stringify(list)) - } catch (e) { - console.error('save history failed', e) - } + } catch (e) { console.error('save history failed', e) } } - const getHistoryList = (key: string): any[] => { - try { - const existing = localStorage.getItem(key) - const list = existing ? JSON.parse(existing) : [] - return list.map((v: string) => ({value: v})) - } catch (e) { - return [] - } + 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 try { - const existing = localStorage.getItem(key) - let list = existing ? JSON.parse(existing) : [] + let list = JSON.parse(localStorage.getItem(key) || '[]') list = list.filter((i: any) => i.id !== item.id) list.unshift({...item, isHistory: true}) if (list.length > 10) list = list.slice(0, 10) localStorage.setItem(key, JSON.stringify(list)) - } catch (e) { - } + } catch (e) {} } - const getMaterialHistory = () => { - try { - const existing = localStorage.getItem(HISTORY_KEYS.MATERIAL) - return existing ? JSON.parse(existing) : [] - } catch (e) { - return [] - } + try { return JSON.parse(localStorage.getItem(HISTORY_KEYS.MATERIAL) || '[]') } catch (e) { return [] } } - // ------------------------------------ // Autocomplete & Search Logic // ------------------------------------ -const createFilter = (queryString: string) => { - return (item: any) => { - return (item.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0) - } -} - -const getTableDataUnique = (field: string) => { - const uniqueItems = Array.from(new Set(tableData.value.map((i: any) => i[field]).filter(Boolean))) - return uniqueItems.map(i => ({value: i})) -} - +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) @@ -763,19 +755,15 @@ const mixedSearch = (queryString: string, tableField: string, storageKey: string const querySearchSupplier = (qs: string, cb: any) => mixedSearch(qs, 'supplier_name', HISTORY_KEYS.SUPPLIER, cb) const handleSupplierSelect = (item: any) => saveToHistory(HISTORY_KEYS.SUPPLIER, item.value) - const querySearchPurchaser = (qs: string, cb: any) => mixedSearch(qs, 'purchaser', HISTORY_KEYS.PURCHASER, cb) const handlePurchaserSelect = (item: any) => saveToHistory(HISTORY_KEYS.PURCHASER, item.value) - const querySearchEmail = (qs: string, cb: any) => mixedSearch(qs, 'purchaser_email', HISTORY_KEYS.EMAIL, cb) const handleEmailSelect = (item: any) => saveToHistory(HISTORY_KEYS.EMAIL, item.value) -const currencyOptions = [ - {value: 'CNY', desc: '人民币'}, - {value: 'USD', desc: '美元'}, - {value: 'EUR', desc: '欧元'} -] +// 币种逻辑修复 +const currencyOptions = [{value: 'CNY', desc: '人民币'}, {value: 'USD', desc: '美元'}, {value: 'EUR', desc: '欧元'}] const querySearchCurrency = (queryString: string, cb: any) => { + // 如果输入为空,返回所有选项 const results = queryString ? currencyOptions.filter(createFilter(queryString)) : currencyOptions cb(results) } @@ -783,33 +771,19 @@ const querySearchCurrency = (queryString: string, cb: any) => { // ------------------------------------ // Material Search Logic // ------------------------------------ -const handleMaterialDropdownVisible = (visible: boolean) => { - if (visible) { - if (materialOptions.value.length === 0) { - handleSearchMaterial('') - } - } -} - +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)) - const filteredApi = apiResults.filter((apiItem: any) => !historyIds.has(apiItem.id)) - materialOptions.value = [...history, ...filteredApi] - } else { - materialOptions.value = apiResults - } - } finally { - searchLoading.value = false - } + 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) { @@ -834,23 +808,14 @@ const validateUnique = (rule: any, value: string, callback: any) => { if (rule.field === 'batch_number' && row.batch_number === value) return true return false }) - if (isDuplicate) { - callback(new Error('编号重复')) - } else { - callback() - } + 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() - } + 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'}], @@ -858,66 +823,35 @@ const rules = { batch_number: [{validator: validateIdentity, trigger: 'blur'}, {validator: validateUnique, trigger: 'blur'}] } -// ------------------------------------ -// Business Logic: Batch/SN Mode -// ------------------------------------ const checkHistoryAndSetMode = async (baseId: number) => { try { const res: any = await getBuyList({page: 1, pageSize: 1000}) - const allItems = res.data.items || [] - const historyItems = allItems.filter((item: any) => item.base_id === baseId) - + const historyItems = (res.data.items || []).filter((item: any) => item.base_id === baseId) 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 = '' + entryMode.value = 'serial'; form.serial_number = ''; form.batch_number = '' } else { - entryMode.value = 'batch' - form.serial_number = '' - const lastBatch = latest.batch_number || '000000' - form.batch_number = incrementBatchNumber(lastBatch) + 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') + 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) { - console.error(e) - modeLocked.value = false - entryMode.value = 'batch' - form.batch_number = '000001' + 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') + return (parseInt(batchStr, 10) + 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') } } - -watch(() => [form.in_quantity, form.unit_price], () => { - form.total_price = Number((form.in_quantity * form.unit_price).toFixed(4)) -}) +watch(() => [form.in_quantity, form.unit_price], () => { form.total_price = Number((form.in_quantity * form.unit_price).toFixed(4)) }) const fetchData = async () => { loading.value = true @@ -925,9 +859,7 @@ const fetchData = async () => { const res: any = await getBuyList(queryParams) tableData.value = res.data.items || [] total.value = res.data.total || 0 - } finally { - loading.value = false - } + } finally { loading.value = false } } const handleCreate = () => { @@ -941,121 +873,133 @@ const handleCreate = () => { materialOptions.value = [] } +// [核心逻辑] 更新表单时的多图回显处理 const handleUpdate = (row: any) => { dialogStatus.value = 'update' resetForm() modeLocked.value = true - // 映射所有字段,包括新增的 inspection_report 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, - warehouse_location: row.warehouse_loc, - status: row.status, - inspection_status: row.inspection_status, - in_quantity: Number(row.qty_inbound), - stock_quantity: Number(row.qty_stock), - available_quantity: Number(row.qty_available), - unit_price: Number(row.unit_price), - total_price: Number(row.total_price), - currency: row.currency, - exchange_rate: Number(row.exchange_rate), - supplier_name: row.supplier_name, - purchaser: row.purchaser, - purchaser_email: row.purchaser_email, - source_link: row.source_link, - detail_link: row.detail_link, - arrival_photo: row.arrival_photo, - inspection_report: row.inspection_report // 映射新字段 + 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, + warehouse_location: row.warehouse_loc, status: row.status, inspection_status: row.inspection_status, + in_quantity: Number(row.qty_inbound), stock_quantity: Number(row.qty_stock), available_quantity: Number(row.qty_available), + unit_price: Number(row.unit_price), total_price: Number(row.total_price), currency: row.currency, exchange_rate: Number(row.exchange_rate), + supplier_name: row.supplier_name, purchaser: row.purchaser, purchaser_email: row.purchaser_email, + source_link: row.source_link, detail_link: row.detail_link, + // 后端返回的是数组 + arrival_photo: row.arrival_photo || [], + inspection_report: row.inspection_report || [] }) - 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 = '' - } + // 1. 同步图片列表 + arrivalFileList.value = form.arrival_photo.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) })) - materialOptions.value = [{ - id: row.base_id, - name: row.material_name, - spec: row.spec_model, - category: row.category - }] + // 2. 分离检测报告中的图片和链接 + const reports = form.inspection_report || [] + 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) })) + inspection_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 }] visible.value = true } // ------------------------------------ -// 图片上传、拍照、删除逻辑 (通用化) +// 图片上传、拍照、删除逻辑 // ------------------------------------ -// 1. 获取图片URL辅助函数 const getImageUrl = (url: string) => { if (!url) return '' if (url.startsWith('http')) return url - // 如果是相对路径,直接返回 (假设后端代理已配置好) - 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)) } -// 2. 校验 const beforeAvatarUpload = (rawFile: any) => { - if (rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/png') { - ElMessage.error('图片必须是 JPG 或 PNG 格式!') - return false - } else if (rawFile.size / 1024 / 1024 > 5) { - ElMessage.error('图片大小不能超过 5MB!') - return false - } + 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 } -// 3. 自定义上传 (支持不同字段) -const customUpload = async (options: any, targetField: keyof typeof form) => { +// 通用上传 +const customUpload = async (options: any, targetField: 'arrival_photo' | 'inspection_report') => { const { file, onSuccess, onError } = options const formData = new FormData() formData.append('file', file) - try { const res: any = await uploadFile(formData) if (res.code === 200) { - // @ts-ignore - form[targetField] = res.data.url + 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) - } + } catch (e) { ElMessage.error('网络错误'); onError(e) } } -// 4. 拍照触发 (逻辑保留,UI已移除) -const triggerCamera = () => { +// 删除图片 (从数组移除 + 物理删除) +const handleRemoveImage = async (uploadFile: any, targetField: 'arrival_photo' | 'inspection_report') => { + try { + const urlToRemove = form[targetField].find(u => getImageUrl(u) === uploadFile.url) || uploadFile.url + + // 1. 从前端数组移除 + form[targetField] = form[targetField].filter(u => u !== urlToRemove) + + // 2. 调用后端物理删除 (仅对本地文件) + 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 +} + +// [关键修复] 拍照功能 +// 1. 触发相机:记录当前操作的是哪个字段 +const triggerCamera = (field: 'arrival_photo' | 'inspection_report') => { + currentCameraField.value = field if (cameraInputRef.value) { cameraInputRef.value.click() } } -// 5. 处理拍照文件 (逻辑保留,UI已移除) +// 2. 处理拍照回调 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 @@ -1063,12 +1007,25 @@ const handleCameraFile = async (event: Event) => { const formData = new FormData() formData.append('file', file) - const loadingMsg = ElMessage.loading({ message: '正在上传...', duration: 0 }) + const loadingMsg = ElMessage.loading({ message: '照片上传中...', duration: 0 }) try { + // 1. 上传文件 const res: any = await uploadFile(formData) if (res.code === 200) { - form.arrival_photo = res.data.url // 默认拍给到货图,如果需要给检测报告需扩展逻辑 + const newUrl = res.data.url + const field = currentCameraField.value + + // 2. 更新表单数据数组 + form[field].push(newUrl) + + // 3. 同步更新 el-upload 的 fileList,确保界面立即显示缩略图 + 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 || '上传失败') @@ -1077,41 +1034,11 @@ const handleCameraFile = async (event: Event) => { ElMessage.error('网络错误,上传失败') } finally { loadingMsg.close() - input.value = '' // 清空以便下次触发 + input.value = '' // 清空 input 防止无法连续拍同一场景 } } } -// 6. 删除图片 (带物理删除,支持指定字段) -const handleRemoveImage = (targetField: keyof typeof form) => { - ElMessageBox.confirm( - '确定要删除当前图片吗?', - '提示', - { confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' } - ).then(async () => { - try { - // @ts-ignore - const url = form[targetField] - if (url) { - // 解析文件名: /api/v1/common/files/xxxx.jpg -> xxxx.jpg - const filename = url.split('/').pop() - if (filename) { - // 调用后端删除文件 - await deleteFile(filename) - } - } - // 清空前端引用 - // @ts-ignore - form[targetField] = '' - ElMessage.success('图片已删除') - } catch (e) { - console.error(e) - ElMessage.error('删除失败') - } - }).catch(() => {}) -} - - // ------------------------------------ // 提交逻辑 // ------------------------------------ @@ -1120,469 +1047,156 @@ const submitForm = async () => { await formRef.value.validate(async (valid: boolean) => { if (valid) { submitting.value = true + + // [核心新增] 提交前,将 inspection_report_url 加入到 form.inspection_report 数组中 + // 注意:这里我们做个临时的合并,避免重复添加 + const finalReportList = [...form.inspection_report] + + // 如果输入框有值,且数组里还没这个值,就加进去 + if (inspection_report_url.value && !finalReportList.includes(inspection_report_url.value)) { + finalReportList.push(inspection_report_url.value) + } + // 如果输入框清空了,但数组里还有旧值(链接类型的),要移除旧的链接值(只保留图片) + // 这里的逻辑稍微复杂:为了简单起见,我们假设每次编辑时,URL输入框的值覆盖之前的链接值 + // 所以策略是:保留所有图片,移除所有旧链接,然后加入当前输入框的链接 + const onlyImages = finalReportList.filter(item => !isExternalLink(item)) + if (inspection_report_url.value) { + onlyImages.push(inspection_report_url.value) + } + + // 更新 form 用于提交 + // 拷贝一份 payload 防止污染响应式对象 + const payload = { + ...form, + inspection_report: onlyImages, + in_quantity: Number(form.in_quantity), + unit_price: Number(form.unit_price) + } + try { if (dialogStatus.value === 'create') { - // 1. 创建入库 - const res: any = await createBuyInbound(form) + const res: any = await createBuyInbound(payload) ElMessage.success('入库成功') - - // 2. 自动打印 - const newItem = res.data - if (newItem) { - ElMessage.info('正在发送打印指令...') - try { - await executePrint(newItem) - ElMessage.success('打印指令已发送') - } catch (printErr: any) { - console.error(printErr) - ElMessage.warning('入库成功,但自动打印失败:' + (printErr.msg || '未知错误')) - } + if (res.data) { + ElMessage.info('发送打印指令...') + try { await executePrint(res.data); ElMessage.success('打印指令已发送') } + catch (printErr: any) { ElMessage.warning('打印失败:' + (printErr.msg || '未知错误')) } } - } else { - const payload = { - ...form, - in_quantity: Number(form.in_quantity), - unit_price: Number(form.unit_price) - } await updateBuyInbound(form.id!, payload) ElMessage.success('更新成功') } - saveToHistory(HISTORY_KEYS.SUPPLIER, form.supplier_name) saveToHistory(HISTORY_KEYS.PURCHASER, form.purchaser) saveToHistory(HISTORY_KEYS.EMAIL, form.purchaser_email) - await fetchData() visible.value = false - } catch (e: any) { - ElMessage.error(e.msg || '操作失败') - } finally { - submitting.value = false - } + } catch (e: any) { ElMessage.error(e.msg || '操作失败') } + finally { submitting.value = false } } }) } const handleDelete = async (row: any) => { - try { - await deleteBuyInbound(row.id) - ElMessage.success('删除成功') - fetchData() - } catch (e) { - ElMessage.error('删除失败') - } + try { await deleteBuyInbound(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 - } + printVisible.value = true; printLoading.value = true; previewUrl.value = '' + 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 - } + 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 = [] - 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: '在库', inspection_status: '未检', - in_quantity: 1, stock_quantity: 1, available_quantity: 1, - warehouse_location: '', - unit_price: 0, total_price: 0, currency: 'CNY', exchange_rate: 1.00, - supplier_name: '', purchaser: '', purchaser_email: '', - source_link: '', detail_link: '', - arrival_photo: '', - inspection_report: '' // 重置新增字段 - }) -} - -const getStatusType = (status: string) => { - const map: any = {'在库': 'success', '出库': 'info', '损耗': 'danger'} - return map[status] || 'warning' -} - -const formatMoney = (val: any, currency = '¥') => { - const num = Number(val) - return isNaN(num) ? '-' : `${currency} ${num.toFixed(2)}` + arrivalFileList.value = [] + reportFileList.value = [] + inspection_report_url.value = '' // 清空URL + 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: '在库', inspection_status: '未检', in_quantity: 1, stock_quantity: 1, available_quantity: 1, warehouse_location: '', unit_price: 0, total_price: 0, currency: 'CNY', exchange_rate: 1.00, supplier_name: '', purchaser: '', purchaser_email: '', source_link: '', detail_link: '', arrival_photo: [], inspection_report: [] }) } +const getStatusType = (status: string) => { const map: any = {'在库': 'success', '出库': 'info', '损耗': 'danger'}; return map[status] || 'warning' } +const formatMoney = (val: any, currency = '¥') => { const num = Number(val); return isNaN(num) ? '-' : `${currency} ${num.toFixed(2)}` } onMounted(() => fetchData()) \ No newline at end of file