修改采购件入库逻辑
This commit is contained in:
@ -29,21 +29,28 @@ def search_base():
|
|||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# 1. 获取列表
|
# 1. 获取列表 (修改:支持状态筛选)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_buy_bp.route('/list', methods=['GET'])
|
@inbound_buy_bp.route('/list', methods=['GET'])
|
||||||
def get_list():
|
def get_list():
|
||||||
try:
|
try:
|
||||||
page = request.args.get('page', 1, type=int)
|
page = request.args.get('page', 1, type=int)
|
||||||
limit = request.args.get('pageSize', 15, type=int)
|
limit = request.args.get('pageSize', 15, type=int)
|
||||||
result = BuyInboundService.get_list(page, limit)
|
keyword = request.args.get('keyword', '')
|
||||||
|
|
||||||
|
# 获取状态列表参数,前端传参格式: statuses=在库,借库
|
||||||
|
statuses_str = request.args.get('statuses', '')
|
||||||
|
statuses = statuses_str.split(',') if statuses_str else []
|
||||||
|
|
||||||
|
result = BuyInboundService.get_list(page, limit, keyword, statuses)
|
||||||
return jsonify({"code": 200, "msg": "success", "data": result})
|
return jsonify({"code": 200, "msg": "success", "data": result})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# 2. 新增入库 (修改:返回创建的对象数据)
|
# 2. 新增入库
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_buy_bp.route('/submit', methods=['POST'])
|
@inbound_buy_bp.route('/submit', methods=['POST'])
|
||||||
def submit():
|
def submit():
|
||||||
@ -52,10 +59,8 @@ def submit():
|
|||||||
if not data:
|
if not data:
|
||||||
return jsonify({"code": 400, "msg": "No data"}), 400
|
return jsonify({"code": 400, "msg": "No data"}), 400
|
||||||
|
|
||||||
# 调用 Service 处理入库,获取新创建的对象
|
|
||||||
new_stock = BuyInboundService.handle_inbound(data)
|
new_stock = BuyInboundService.handle_inbound(data)
|
||||||
|
|
||||||
# 返回成功信息以及新创建的数据(包含生成的ID和SKU),供前端打印使用
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"code": 200,
|
"code": 200,
|
||||||
"msg": "入库成功",
|
"msg": "入库成功",
|
||||||
@ -89,3 +94,20 @@ def delete_buy(id):
|
|||||||
return jsonify({"code": 200, "msg": "删除成功"})
|
return jsonify({"code": 200, "msg": "删除成功"})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 5. 获取关联的出库历史
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@inbound_buy_bp.route('/<int:id>/history', methods=['GET'])
|
||||||
|
def get_history(id):
|
||||||
|
try:
|
||||||
|
history = BuyInboundService.get_outbound_history(id)
|
||||||
|
return jsonify({
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": history
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||||
@ -1,12 +1,10 @@
|
|||||||
# 文件路径: inventory-backend/app/services/inbound/buy_service.py
|
# app/services/inbound/buy_service.py
|
||||||
|
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models.inbound.buy import StockBuy
|
from app.models.inbound.buy import StockBuy
|
||||||
from app.models.base import MaterialBase
|
from app.models.base import MaterialBase
|
||||||
# 引入出库记录模型,用于查询流转历史
|
|
||||||
from app.models.outbound import TransOutbound
|
from app.models.outbound import TransOutbound
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from sqlalchemy import or_, func, text
|
from sqlalchemy import or_, func, text, and_
|
||||||
import traceback
|
import traceback
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@ -14,17 +12,13 @@ import json
|
|||||||
class BuyInboundService:
|
class BuyInboundService:
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 1. 基础物料搜索 (供下拉框使用)
|
# 1. 基础物料搜索
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def search_base_material(keyword):
|
def search_base_material(keyword):
|
||||||
"""
|
"""搜索基础物料"""
|
||||||
搜索基础物料
|
|
||||||
如果 keyword 为空,返回最新的 20 条记录
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
query = MaterialBase.query.filter(MaterialBase.is_enabled == True)
|
query = MaterialBase.query.filter(MaterialBase.is_enabled == True)
|
||||||
|
|
||||||
if keyword:
|
if keyword:
|
||||||
query = query.filter(
|
query = query.filter(
|
||||||
or_(
|
or_(
|
||||||
@ -32,20 +26,13 @@ class BuyInboundService:
|
|||||||
MaterialBase.spec_model.ilike(f'%{keyword}%')
|
MaterialBase.spec_model.ilike(f'%{keyword}%')
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 无论是否有关键词,都按 ID 倒序排列,取前 20 条
|
|
||||||
query = query.order_by(MaterialBase.id.desc()).limit(20)
|
query = query.order_by(MaterialBase.id.desc()).limit(20)
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
for item in query.all():
|
for item in query.all():
|
||||||
results.append({
|
results.append({
|
||||||
'id': item.id,
|
'id': item.id, 'name': item.name, 'spec': item.spec_model,
|
||||||
'name': item.name,
|
'category': item.category, 'unit': item.unit,
|
||||||
'spec': item.spec_model,
|
'type': item.material_type, 'status': '启用'
|
||||||
'category': item.category,
|
|
||||||
'unit': item.unit,
|
|
||||||
'type': item.material_type,
|
|
||||||
'status': '启用'
|
|
||||||
})
|
})
|
||||||
return results
|
return results
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -57,62 +44,46 @@ class BuyInboundService:
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def handle_inbound(data):
|
def handle_inbound(data):
|
||||||
"""
|
"""新增入库"""
|
||||||
处理入库逻辑
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
base_id = data.get('base_id')
|
base_id = data.get('base_id')
|
||||||
if not base_id:
|
if not base_id: raise ValueError("必须选择基础物料")
|
||||||
raise ValueError("必须选择基础物料进行入库 (缺少 base_id)")
|
|
||||||
|
|
||||||
material = MaterialBase.query.get(base_id)
|
material = MaterialBase.query.get(base_id)
|
||||||
if not material:
|
if not material: raise ValueError("物料不存在")
|
||||||
raise ValueError(f"ID为 {base_id} 的基础物料不存在")
|
|
||||||
|
|
||||||
in_date_val = datetime.utcnow().date()
|
in_date_val = datetime.utcnow().date()
|
||||||
if data.get('in_date'):
|
if data.get('in_date'):
|
||||||
try:
|
try:
|
||||||
# 兼容 YYYY-MM-DD HH:MM:SS 或 YYYY-MM-DD
|
|
||||||
date_str = str(data['in_date'])
|
date_str = str(data['in_date'])
|
||||||
if len(date_str) > 10:
|
if len(date_str) > 10: date_str = date_str[:10]
|
||||||
date_str = date_str[:10]
|
|
||||||
in_date_val = datetime.strptime(date_str, '%Y-%m-%d').date()
|
in_date_val = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||||
except ValueError:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
in_qty = float(data.get('in_quantity') or 0)
|
in_qty = float(data.get('in_quantity') or 0)
|
||||||
u_price = float(data.get('unit_price') or 0)
|
u_price = float(data.get('unit_price') or 0)
|
||||||
|
|
||||||
# 1. 获取全局打印流水号 (跨表唯一)
|
|
||||||
seq_sql = text("SELECT nextval('global_print_seq')")
|
seq_sql = text("SELECT nextval('global_print_seq')")
|
||||||
result = db.session.execute(seq_sql)
|
result = db.session.execute(seq_sql)
|
||||||
next_global_id = result.scalar()
|
next_global_id = result.scalar()
|
||||||
|
|
||||||
# 2. 自动生成 SKU (格式: 00000001)
|
|
||||||
generated_sku = str(next_global_id).zfill(10)
|
generated_sku = str(next_global_id).zfill(10)
|
||||||
|
final_barcode = data.get('barcode') or generated_sku
|
||||||
|
|
||||||
# 3. 条码逻辑处理
|
|
||||||
final_barcode = data.get('barcode')
|
|
||||||
if not final_barcode:
|
|
||||||
final_barcode = generated_sku
|
|
||||||
|
|
||||||
# 4. 图片列表转 JSON 字符串处理
|
|
||||||
arrival_list = data.get('arrival_photo', [])
|
arrival_list = data.get('arrival_photo', [])
|
||||||
report_list = data.get('inspection_report', [])
|
report_list = data.get('inspection_report', [])
|
||||||
|
|
||||||
if not isinstance(arrival_list, list): arrival_list = []
|
if not isinstance(arrival_list, list): arrival_list = []
|
||||||
if not isinstance(report_list, list): report_list = []
|
if not isinstance(report_list, list): report_list = []
|
||||||
|
|
||||||
new_stock = StockBuy(
|
new_stock = StockBuy(
|
||||||
base_id=material.id,
|
base_id=material.id,
|
||||||
global_print_id=next_global_id,
|
global_print_id=next_global_id,
|
||||||
sku=generated_sku, # 自动生成的SKU
|
sku=generated_sku,
|
||||||
barcode=final_barcode, # 如果未输入,则存入SKU值
|
barcode=final_barcode,
|
||||||
|
|
||||||
in_date=in_date_val,
|
in_date=in_date_val,
|
||||||
serial_number=data.get('serial_number'),
|
serial_number=data.get('serial_number'),
|
||||||
batch_number=data.get('batch_number'),
|
batch_number=data.get('batch_number'),
|
||||||
status='在库',
|
status=data.get('status', '在库'), # 默认在库
|
||||||
in_quantity=in_qty,
|
in_quantity=in_qty,
|
||||||
stock_quantity=in_qty,
|
stock_quantity=in_qty,
|
||||||
available_quantity=in_qty,
|
available_quantity=in_qty,
|
||||||
@ -127,18 +98,12 @@ class BuyInboundService:
|
|||||||
buyer_email=data.get('purchaser_email'),
|
buyer_email=data.get('purchaser_email'),
|
||||||
original_link=data.get('source_link'),
|
original_link=data.get('source_link'),
|
||||||
detail_link=data.get('detail_link'),
|
detail_link=data.get('detail_link'),
|
||||||
|
|
||||||
# 将列表转为 JSON 字符串存储
|
|
||||||
arrival_photo=json.dumps(arrival_list),
|
arrival_photo=json.dumps(arrival_list),
|
||||||
inspection_report=json.dumps(report_list)
|
inspection_report=json.dumps(report_list)
|
||||||
)
|
)
|
||||||
|
|
||||||
db.session.add(new_stock)
|
db.session.add(new_stock)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# 返回创建的对象实例
|
|
||||||
return new_stock
|
return new_stock
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
raise e
|
raise e
|
||||||
@ -148,80 +113,45 @@ class BuyInboundService:
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_inbound(stock_id, data):
|
def update_inbound(stock_id, data):
|
||||||
"""
|
"""更新入库"""
|
||||||
更新入库记录
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
print(f"----- UPDATE DEBUG: ID={stock_id} -----")
|
|
||||||
stock = StockBuy.query.get(stock_id)
|
stock = StockBuy.query.get(stock_id)
|
||||||
if not stock:
|
if not stock: raise ValueError("记录不存在")
|
||||||
raise ValueError("记录不存在")
|
|
||||||
|
|
||||||
# 基础字段映射
|
|
||||||
field_mapping = {
|
field_mapping = {
|
||||||
'sku': 'sku',
|
'sku': 'sku', 'barcode': 'barcode',
|
||||||
'barcode': 'barcode',
|
|
||||||
'warehouse_location': 'warehouse_location',
|
'warehouse_location': 'warehouse_location',
|
||||||
'serial_number': 'serial_number',
|
'serial_number': 'serial_number', 'batch_number': 'batch_number',
|
||||||
'batch_number': 'batch_number',
|
'status': 'status', 'inspection_status': 'inspection_status',
|
||||||
'status': 'status',
|
'supplier_name': 'supplier_name', 'detail_link': 'detail_link',
|
||||||
'inspection_status': 'inspection_status',
|
'currency': 'currency', 'exchange_rate': 'exchange_rate',
|
||||||
'supplier_name': 'supplier_name',
|
'purchaser': 'buyer_name', 'purchaser_email': 'buyer_email',
|
||||||
'detail_link': 'detail_link',
|
|
||||||
'currency': 'currency',
|
|
||||||
'exchange_rate': 'exchange_rate',
|
|
||||||
'purchaser': 'buyer_name',
|
|
||||||
'purchaser_email': 'buyer_email',
|
|
||||||
'source_link': 'original_link'
|
'source_link': 'original_link'
|
||||||
}
|
}
|
||||||
|
for k, v in field_mapping.items():
|
||||||
|
if k in data: setattr(stock, v, data[k])
|
||||||
|
|
||||||
for frontend_key, db_attr in field_mapping.items():
|
if 'arrival_photo' in data and isinstance(data['arrival_photo'], list):
|
||||||
if frontend_key in data:
|
stock.arrival_photo = json.dumps(data['arrival_photo'])
|
||||||
setattr(stock, db_attr, data[frontend_key])
|
if 'inspection_report' in data and isinstance(data['inspection_report'], list):
|
||||||
|
stock.inspection_report = json.dumps(data['inspection_report'])
|
||||||
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
|
|
||||||
|
|
||||||
if 'in_quantity' in data:
|
if 'in_quantity' in data:
|
||||||
new_qty = float(data['in_quantity'])
|
new_qty = float(data['in_quantity'])
|
||||||
old_qty = float(stock.in_quantity)
|
diff = new_qty - float(stock.in_quantity)
|
||||||
if new_qty != old_qty:
|
if diff != 0:
|
||||||
diff = new_qty - old_qty
|
|
||||||
stock.in_quantity = new_qty
|
stock.in_quantity = new_qty
|
||||||
# 注意:手动修改入库量时,同步调整总库存和可用库存
|
|
||||||
stock.stock_quantity = float(stock.stock_quantity) + diff
|
stock.stock_quantity = float(stock.stock_quantity) + diff
|
||||||
stock.available_quantity = float(stock.available_quantity) + diff
|
stock.available_quantity = float(stock.available_quantity) + diff
|
||||||
qty_changed = True
|
|
||||||
|
|
||||||
if 'unit_price' in data:
|
if 'unit_price' in data:
|
||||||
new_price = float(data['unit_price'])
|
stock.unit_price = float(data['unit_price'])
|
||||||
old_price = float(stock.unit_price)
|
|
||||||
if new_price != old_price:
|
|
||||||
stock.unit_price = new_price
|
|
||||||
price_changed = True
|
|
||||||
|
|
||||||
if qty_changed or price_changed:
|
|
||||||
stock.total_price = float(stock.in_quantity) * float(stock.unit_price)
|
stock.total_price = float(stock.in_quantity) * float(stock.unit_price)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
print("----- UPDATE SUCCESS -----")
|
|
||||||
return stock
|
return stock
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
print(f"----- UPDATE FAILED: {str(e)} -----")
|
|
||||||
traceback.print_exc()
|
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@ -229,13 +159,10 @@ class BuyInboundService:
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def delete_inbound(stock_id):
|
def delete_inbound(stock_id):
|
||||||
"""
|
"""删除入库"""
|
||||||
删除入库记录
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
stock = StockBuy.query.get(stock_id)
|
stock = StockBuy.query.get(stock_id)
|
||||||
if not stock:
|
if not stock: raise ValueError("记录不存在")
|
||||||
raise ValueError("记录不存在")
|
|
||||||
db.session.delete(stock)
|
db.session.delete(stock)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return True
|
return True
|
||||||
@ -244,108 +171,94 @@ class BuyInboundService:
|
|||||||
raise e
|
raise e
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 5. [新增] 获取出库流转历史 (挂钩出库记录)
|
# 5. 获取出库流转历史
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_outbound_history(stock_id):
|
def get_outbound_history(stock_id):
|
||||||
"""
|
"""获取出库历史"""
|
||||||
查询该入库单对应的所有出库记录
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
records = TransOutbound.query.filter_by(
|
records = TransOutbound.query.filter_by(
|
||||||
source_table='stock_buy',
|
source_table='stock_buy', stock_id=stock_id
|
||||||
stock_id=stock_id
|
|
||||||
).order_by(TransOutbound.outbound_time.desc()).all()
|
).order_by(TransOutbound.outbound_time.desc()).all()
|
||||||
|
|
||||||
return [r.to_dict() for r in records]
|
return [r.to_dict() for r in records]
|
||||||
except Exception as e:
|
except:
|
||||||
traceback.print_exc()
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 6. 获取列表 (含动态状态计算)
|
# 6. 获取列表 (核心逻辑修改)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_list(page, limit, keyword=None):
|
def get_list(page, limit, keyword=None, statuses=None):
|
||||||
|
"""
|
||||||
|
获取列表
|
||||||
|
:param statuses: 状态列表 (e.g. ['在库', '借库', '已出库'])
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
# 1. 联表查询:StockBuy join MaterialBase
|
|
||||||
query = db.session.query(StockBuy).outerjoin(MaterialBase, StockBuy.base_id == MaterialBase.id)
|
query = db.session.query(StockBuy).outerjoin(MaterialBase, StockBuy.base_id == MaterialBase.id)
|
||||||
|
|
||||||
|
# 1. 关键词搜索 (覆盖所有关键字段)
|
||||||
if keyword:
|
if keyword:
|
||||||
|
kw = f'%{keyword}%'
|
||||||
query = query.filter(
|
query = query.filter(
|
||||||
or_(
|
or_(
|
||||||
MaterialBase.name.ilike(f'%{keyword}%'),
|
MaterialBase.name.ilike(kw),
|
||||||
MaterialBase.spec_model.ilike(f'%{keyword}%'),
|
MaterialBase.spec_model.ilike(kw),
|
||||||
StockBuy.batch_number.ilike(f'%{keyword}%'),
|
StockBuy.batch_number.ilike(kw),
|
||||||
StockBuy.serial_number.ilike(f'%{keyword}%'),
|
StockBuy.serial_number.ilike(kw),
|
||||||
StockBuy.sku.ilike(f'%{keyword}%'),
|
StockBuy.sku.ilike(kw),
|
||||||
StockBuy.supplier_name.ilike(f'%{keyword}%')
|
StockBuy.supplier_name.ilike(kw)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. 状态筛选与零库存隐藏逻辑
|
||||||
|
# 用户要求:
|
||||||
|
# - 默认显示:'在库', '借库'。
|
||||||
|
# - 零库存规则:库存为0时,不在页面显示(除非筛选了'已出库')。
|
||||||
|
|
||||||
|
if not statuses:
|
||||||
|
# 默认情况:只查 '在库' 和 '借库'
|
||||||
|
statuses = ['在库', '借库']
|
||||||
|
|
||||||
|
# 构建筛选条件
|
||||||
|
# 如果筛选条件中 包含 '已出库',则允许显示 stock_quantity >= 0 (即显示所有)
|
||||||
|
# 如果筛选条件中 不包含 '已出库',则强制要求 stock_quantity > 0 (隐藏零库存)
|
||||||
|
|
||||||
|
if '已出库' in statuses:
|
||||||
|
# 用户想看已出库的,直接按状态查,不做数量限制
|
||||||
|
query = query.filter(StockBuy.status.in_(statuses))
|
||||||
|
else:
|
||||||
|
# 用户不想看已出库的,按状态查 AND 数量必须 > 0
|
||||||
|
query = query.filter(
|
||||||
|
and_(
|
||||||
|
StockBuy.status.in_(statuses),
|
||||||
|
StockBuy.stock_quantity > 0
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
pagination = query.order_by(StockBuy.id.desc()).paginate(page=page, per_page=limit, error_out=False)
|
pagination = query.order_by(StockBuy.id.desc()).paginate(page=page, per_page=limit, error_out=False)
|
||||||
|
|
||||||
current_items = pagination.items
|
current_items = pagination.items
|
||||||
base_ids = list(set([item.base_id for item in current_items if item.base_id]))
|
|
||||||
|
|
||||||
# 2. 聚合统计 (计算该种物料的总库存)
|
def parse_img(json_str):
|
||||||
stock_map = {}
|
if not json_str: return []
|
||||||
if base_ids:
|
|
||||||
aggregates = db.session.query(
|
|
||||||
StockBuy.base_id,
|
|
||||||
func.sum(StockBuy.stock_quantity).label('total_stock'),
|
|
||||||
func.sum(StockBuy.available_quantity).label('total_avail')
|
|
||||||
).filter(StockBuy.base_id.in_(base_ids)).group_by(StockBuy.base_id).all()
|
|
||||||
|
|
||||||
for agg in aggregates:
|
|
||||||
stock_map[agg.base_id] = {
|
|
||||||
'total_stock': float(agg.total_stock or 0),
|
|
||||||
'total_avail': float(agg.total_avail or 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
# 辅助函数:解析JSON图片列表
|
|
||||||
def parse_img_list(json_str):
|
|
||||||
if not json_str:
|
|
||||||
return []
|
|
||||||
try:
|
try:
|
||||||
if not json_str.startswith('['):
|
return json.loads(json_str) if json_str.startswith('[') else [json_str]
|
||||||
return [json_str]
|
|
||||||
return json.loads(json_str)
|
|
||||||
except:
|
except:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
items = []
|
items = []
|
||||||
for item in current_items:
|
for item in current_items:
|
||||||
# -------------------------------------------------------------
|
# 获取单行数据,不再进行聚合计算
|
||||||
# [核心逻辑] 动态计算状态
|
qty_stock = float(item.stock_quantity or 0)
|
||||||
# -------------------------------------------------------------
|
|
||||||
qty_in = float(item.in_quantity or 0)
|
|
||||||
qty_avail = float(item.available_quantity or 0)
|
qty_avail = float(item.available_quantity or 0)
|
||||||
|
|
||||||
# 默认使用数据库字段
|
|
||||||
current_status = item.status
|
|
||||||
|
|
||||||
# 如果有入库量,但可用量为0,说明已经全部出库
|
|
||||||
if qty_in > 0 and qty_avail <= 0:
|
|
||||||
current_status = '出库'
|
|
||||||
|
|
||||||
# 获取聚合数据
|
|
||||||
mat_name = item.material.name if item.material else '未知物料'
|
|
||||||
mat_spec = item.material.spec_model if item.material else ''
|
|
||||||
mat_cat = item.material.category if item.material else ''
|
|
||||||
mat_unit = item.material.unit if item.material else ''
|
|
||||||
mat_type = item.material.material_type if item.material else ''
|
|
||||||
|
|
||||||
stats = stock_map.get(item.base_id, {'total_stock': 0, 'total_avail': 0})
|
|
||||||
|
|
||||||
d = {
|
d = {
|
||||||
'id': item.id,
|
'id': item.id,
|
||||||
'base_id': item.base_id,
|
'base_id': item.base_id,
|
||||||
'material_name': mat_name,
|
'material_name': item.material.name if item.material else '',
|
||||||
'spec_model': mat_spec,
|
'spec_model': item.material.spec_model if item.material else '',
|
||||||
'category': mat_cat,
|
'category': item.material.category if item.material else '',
|
||||||
'unit': mat_unit,
|
'unit': item.material.unit if item.material else '',
|
||||||
'material_type': mat_type,
|
'material_type': item.material.material_type if item.material else '',
|
||||||
|
|
||||||
'sku': item.sku,
|
'sku': item.sku,
|
||||||
'inbound_date': str(item.in_date) if item.in_date else '',
|
'inbound_date': str(item.in_date) if item.in_date else '',
|
||||||
@ -353,17 +266,17 @@ class BuyInboundService:
|
|||||||
'serial_number': item.serial_number,
|
'serial_number': item.serial_number,
|
||||||
'batch_number': item.batch_number,
|
'batch_number': item.batch_number,
|
||||||
|
|
||||||
# 使用动态计算的状态
|
'status': item.status,
|
||||||
'status': current_status,
|
|
||||||
|
|
||||||
'inspection_status': item.inspection_status,
|
'inspection_status': item.inspection_status,
|
||||||
|
|
||||||
'qty_inbound': qty_in,
|
'qty_inbound': float(item.in_quantity or 0),
|
||||||
'qty_stock': float(item.stock_quantity or 0),
|
'qty_stock': qty_stock,
|
||||||
'qty_available': qty_avail,
|
'qty_available': qty_avail,
|
||||||
|
|
||||||
'sum_stock': stats['total_stock'],
|
# 解除挂钩:不再返回所有批次的总和,直接返回当前批次的数量
|
||||||
'sum_available': stats['total_avail'],
|
# 为了兼容前端字段名,这里直接用当前行数量填充
|
||||||
|
'sum_stock': qty_stock,
|
||||||
|
'sum_available': qty_avail,
|
||||||
|
|
||||||
'warehouse_loc': item.warehouse_location,
|
'warehouse_loc': item.warehouse_location,
|
||||||
'unit_price': float(item.unit_price or 0),
|
'unit_price': float(item.unit_price or 0),
|
||||||
@ -375,10 +288,8 @@ class BuyInboundService:
|
|||||||
'purchaser_email': item.buyer_email,
|
'purchaser_email': item.buyer_email,
|
||||||
'source_link': item.original_link,
|
'source_link': item.original_link,
|
||||||
'detail_link': item.detail_link,
|
'detail_link': item.detail_link,
|
||||||
|
'arrival_photo': parse_img(item.arrival_photo),
|
||||||
'arrival_photo': parse_img_list(item.arrival_photo),
|
'inspection_report': parse_img(item.inspection_report),
|
||||||
'inspection_report': parse_img_list(item.inspection_report),
|
|
||||||
|
|
||||||
'global_print_id': item.global_print_id,
|
'global_print_id': item.global_print_id,
|
||||||
'global_print_id_str': f"{item.global_print_id:08d}" if item.global_print_id else ""
|
'global_print_id_str': f"{item.global_print_id:08d}" if item.global_print_id else ""
|
||||||
}
|
}
|
||||||
@ -386,6 +297,5 @@ class BuyInboundService:
|
|||||||
|
|
||||||
return {"total": pagination.total, "items": items}
|
return {"total": pagination.total, "items": items}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"List Error: {e}")
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return {"total": 0, "items": []}
|
return {"total": 0, "items": []}
|
||||||
@ -9,11 +9,25 @@
|
|||||||
clearable
|
clearable
|
||||||
@clear="fetchData"
|
@clear="fetchData"
|
||||||
@keyup.enter="fetchData"
|
@keyup.enter="fetchData"
|
||||||
|
style="width: 300px; margin-right: 10px;"
|
||||||
>
|
>
|
||||||
<template #append>
|
<template #append>
|
||||||
<el-button :icon="Search" @click="fetchData"/>
|
<el-button :icon="Search" @click="fetchData"/>
|
||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
|
|
||||||
|
<el-select
|
||||||
|
v-model="queryParams.statuses"
|
||||||
|
multiple
|
||||||
|
collapse-tags
|
||||||
|
placeholder="状态筛选"
|
||||||
|
style="width: 220px;"
|
||||||
|
@change="fetchData"
|
||||||
|
>
|
||||||
|
<el-option label="在库" value="在库" />
|
||||||
|
<el-option label="借库" value="借库" />
|
||||||
|
<el-option label="已出库" value="已出库" />
|
||||||
|
</el-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="right-tools">
|
<div class="right-tools">
|
||||||
@ -66,21 +80,24 @@
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #default="scope" v-else-if="['serial_number', 'batch_number'].includes(col.prop)">
|
<template #default="scope" v-else-if="col.prop === 'sn_bn'">
|
||||||
<span v-if="scope.row[col.prop]" :class="col.prop === 'serial_number' ? 'tag-sn' : 'tag-bn'">
|
<div v-if="scope.row.serial_number" class="id-cell">
|
||||||
{{ scope.row[col.prop] }}
|
<span class="prefix-tag sn">SN</span>
|
||||||
</span>
|
<span class="id-text">{{ scope.row.serial_number }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="scope.row.batch_number" class="id-cell">
|
||||||
|
<span class="prefix-tag bn">BN</span>
|
||||||
|
<span class="id-text">{{ scope.row.batch_number }}</span>
|
||||||
|
</div>
|
||||||
<span v-else class="text-placeholder">-</span>
|
<span v-else class="text-placeholder">-</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #default="scope" v-else-if="col.prop === 'qty_stock'">
|
<template #default="scope" v-else-if="col.prop === 'qty_stock'">
|
||||||
<span class="stock-num">{{ scope.row.sum_stock }}</span>
|
<span class="stock-num">{{ scope.row.qty_stock }}</span>
|
||||||
<el-tag size="small" type="info" effect="plain" class="sum-tag">总</el-tag>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #default="scope" v-else-if="col.prop === 'qty_available'">
|
<template #default="scope" v-else-if="col.prop === 'qty_available'">
|
||||||
<span class="avail-num">{{ scope.row.sum_available }}</span>
|
<span class="avail-num">{{ scope.row.qty_available }}</span>
|
||||||
<el-tag size="small" type="info" effect="plain" class="sum-tag">总</el-tag>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #default="scope" v-else-if="col.prop === 'status'">
|
<template #default="scope" v-else-if="col.prop === 'status'">
|
||||||
@ -170,13 +187,10 @@
|
|||||||
|
|
||||||
<div class="form-card basic-card">
|
<div class="form-card basic-card">
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<el-icon class="icon">
|
<el-icon class="icon"><Box/></el-icon>
|
||||||
<Box/>
|
|
||||||
</el-icon>
|
|
||||||
<span>1. 基础信息</span>
|
<span>1. 基础信息</span>
|
||||||
<span class="sub-title" v-if="dialogStatus === 'create'"> (请先搜索锁定物料)</span>
|
<span class="sub-title" v-if="dialogStatus === 'create'"> (请先搜索锁定物料)</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<el-row :gutter="24" v-if="dialogStatus === 'create'" style="margin-bottom: 15px;">
|
<el-row :gutter="24" v-if="dialogStatus === 'create'" style="margin-bottom: 15px;">
|
||||||
<el-col :span="10">
|
<el-col :span="10">
|
||||||
@ -219,31 +233,13 @@
|
|||||||
|
|
||||||
<div class="read-only-grid">
|
<div class="read-only-grid">
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="8">
|
<el-col :span="8"><el-form-item label="名称"><el-input v-model="form.material_name" disabled class="is-text-view"/></el-form-item></el-col>
|
||||||
<el-form-item label="名称">
|
|
||||||
<el-input v-model="form.material_name" disabled class="is-text-view"/>
|
<el-col :span="8"><el-form-item label="类型"><el-input v-model="form.material_type" disabled class="is-text-view"/></el-form-item></el-col>
|
||||||
</el-form-item>
|
<el-col :span="8"><el-form-item label="类别"><el-input v-model="form.category" disabled class="is-text-view"/></el-form-item></el-col>
|
||||||
</el-col>
|
|
||||||
<el-col :span="8">
|
<el-col :span="8"><el-form-item label="规格型号"><el-input v-model="form.spec_model" disabled class="is-text-view"/></el-form-item></el-col>
|
||||||
<el-form-item label="规格型号">
|
<el-col :span="8"><el-form-item label="单位"><el-input v-model="form.unit" disabled class="is-text-view"/></el-form-item></el-col>
|
||||||
<el-input v-model="form.spec_model" disabled class="is-text-view"/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="8">
|
|
||||||
<el-form-item label="单位">
|
|
||||||
<el-input v-model="form.unit" disabled class="is-text-view"/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="8">
|
|
||||||
<el-form-item label="类别">
|
|
||||||
<el-input v-model="form.category" disabled class="is-text-view"/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="8">
|
|
||||||
<el-form-item label="类型">
|
|
||||||
<el-input v-model="form.material_type" disabled class="is-text-view"/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -251,74 +247,46 @@
|
|||||||
|
|
||||||
<div class="form-card inbound-card">
|
<div class="form-card inbound-card">
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<el-icon class="icon">
|
<el-icon class="icon"><House/></el-icon>
|
||||||
<House/>
|
|
||||||
</el-icon>
|
|
||||||
<span>2. 入库详情</span>
|
<span>2. 入库详情</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<el-form-item label="编码/SKU" prop="sku">
|
<el-form-item label="编码/SKU" prop="sku"><el-input v-model="form.sku" placeholder="系统自动生成" disabled/></el-form-item>
|
||||||
<el-input
|
|
||||||
v-model="form.sku"
|
|
||||||
placeholder="系统自动生成"
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<el-form-item label="入库日期" prop="in_date">
|
<el-form-item label="入库日期" prop="in_date"><el-date-picker v-model="form.in_date" type="date" value-format="YYYY-MM-DD" style="width:100%" disabled/></el-form-item>
|
||||||
<el-date-picker v-model="form.in_date" type="date" value-format="YYYY-MM-DD" style="width:100%"
|
|
||||||
disabled/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<el-form-item label="条码" prop="barcode">
|
<el-form-item label="条码" prop="barcode"><el-input v-model="form.barcode" placeholder="扫描条码"/></el-form-item>
|
||||||
<el-input v-model="form.barcode" placeholder="扫描条码"/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<el-form-item label="库位" prop="warehouse_location">
|
<el-form-item label="库位" prop="warehouse_location"><el-input v-model="form.warehouse_location" placeholder="例如: A-01-02"/></el-form-item>
|
||||||
<el-input v-model="form.warehouse_location" placeholder="例如: A-01-02"/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<div class="identity-panel">
|
<div class="identity-panel">
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-col :span="24" style="margin-bottom: 8px;">
|
<el-col :span="24" style="margin-bottom: 8px;">
|
||||||
<el-radio-group v-model="entryMode" @change="handleEntryModeChange" :disabled="modeLocked"
|
<el-radio-group v-model="entryMode" @change="handleEntryModeChange" :disabled="modeLocked" size="small" class="custom-radio-group">
|
||||||
size="small" class="custom-radio-group">
|
|
||||||
<el-radio-button label="batch">按批号入库 (Batch)</el-radio-button>
|
<el-radio-button label="batch">按批号入库 (Batch)</el-radio-button>
|
||||||
<el-radio-button label="serial">按序列号入库 (SN)</el-radio-button>
|
<el-radio-button label="serial">按序列号入库 (SN)</el-radio-button>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
<span v-if="modeLocked" class="locked-msg"><el-icon><Lock/></el-icon> 历史锁定</span>
|
<span v-if="modeLocked" class="locked-msg"><el-icon><Lock/></el-icon> 历史锁定</span>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="批号" prop="batch_number">
|
<el-form-item label="批号" prop="batch_number">
|
||||||
<el-input
|
<el-input v-model="form.batch_number" :placeholder="entryMode === 'batch' ? '系统生成...' : '不可用'" :disabled="entryMode === 'serial'" clearable>
|
||||||
v-model="form.batch_number"
|
|
||||||
:placeholder="entryMode === 'batch' ? '系统生成...' : '不可用'"
|
|
||||||
:disabled="entryMode === 'serial'"
|
|
||||||
clearable
|
|
||||||
>
|
|
||||||
<template #prefix><span class="prefix-tag bn">BN</span></template>
|
<template #prefix><span class="prefix-tag bn">BN</span></template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="序列号" prop="serial_number">
|
<el-form-item label="序列号" prop="serial_number">
|
||||||
<el-input
|
<el-input v-model="form.serial_number" :placeholder="entryMode === 'serial' ? '请扫描 SN...' : '不可用'" :disabled="entryMode === 'batch'" clearable>
|
||||||
v-model="form.serial_number"
|
|
||||||
:placeholder="entryMode === 'serial' ? '请扫描 SN...' : '不可用'"
|
|
||||||
:disabled="entryMode === 'batch'"
|
|
||||||
clearable
|
|
||||||
>
|
|
||||||
<template #prefix><span class="prefix-tag sn">SN</span></template>
|
<template #prefix><span class="prefix-tag sn">SN</span></template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -329,33 +297,22 @@
|
|||||||
<el-row :gutter="20" style="margin-top: 10px;">
|
<el-row :gutter="20" style="margin-top: 10px;">
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<el-form-item label="入库数量" prop="in_quantity">
|
<el-form-item label="入库数量" prop="in_quantity">
|
||||||
<el-input-number v-model="form.in_quantity" :min="1" controls-position="right" style="width:100%"
|
<el-input-number v-model="form.in_quantity" :min="1" controls-position="right" style="width:100%" class="strong-input"/>
|
||||||
class="strong-input"/>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
|
||||||
<template v-if="dialogStatus === 'update'">
|
<template v-if="dialogStatus === 'update'">
|
||||||
<el-col :span="6">
|
<el-col :span="6"><el-form-item label="当前库存" prop="stock_quantity"><el-input-number v-model="form.stock_quantity" disabled style="width:100%" :controls="false"/></el-form-item></el-col>
|
||||||
<el-form-item label="当前库存" prop="stock_quantity">
|
<el-col :span="6"><el-form-item label="当前可用" prop="available_quantity"><el-input-number v-model="form.available_quantity" disabled style="width:100%" :controls="false"/></el-form-item></el-col>
|
||||||
<el-input-number v-model="form.stock_quantity" disabled style="width:100%" :controls="false"/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="6">
|
|
||||||
<el-form-item label="当前可用" prop="available_quantity">
|
|
||||||
<el-input-number v-model="form.available_quantity" disabled style="width:100%" :controls="false"/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<el-form-item label="库存状态" prop="status">
|
<el-form-item label="库存状态" prop="status">
|
||||||
<el-select v-model="form.status" style="width:100%">
|
<el-select v-model="form.status" style="width:100%">
|
||||||
<el-option label="在库" value="在库"/>
|
<el-option label="在库" value="在库"/>
|
||||||
<el-option label="出库" value="出库"/>
|
<el-option label="已出库" value="已出库"/>
|
||||||
<el-option label="损耗" value="损耗"/>
|
<el-option label="借库" value="借库"/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<el-form-item label="到检状态" prop="inspection_status">
|
<el-form-item label="到检状态" prop="inspection_status">
|
||||||
<el-select v-model="form.inspection_status" style="width:100%">
|
<el-select v-model="form.inspection_status" style="width:100%">
|
||||||
@ -371,210 +328,76 @@
|
|||||||
<el-col :span="24">
|
<el-col :span="24">
|
||||||
<el-form-item label="到货图片" prop="arrival_photo">
|
<el-form-item label="到货图片" prop="arrival_photo">
|
||||||
<div class="upload-container">
|
<div class="upload-container">
|
||||||
<el-upload
|
<el-upload v-model:file-list="arrivalFileList" action="#" list-type="picture-card" multiple :http-request="(opts) => customUpload(opts, 'arrival_photo')" :on-preview="handlePreviewPicture" :on-remove="(file) => handleRemoveImage(file, 'arrival_photo')" :before-upload="beforeAvatarUpload">
|
||||||
v-model:file-list="arrivalFileList"
|
|
||||||
action="#"
|
|
||||||
list-type="picture-card"
|
|
||||||
multiple
|
|
||||||
:http-request="(opts) => customUpload(opts, 'arrival_photo')"
|
|
||||||
:on-preview="handlePreviewPicture"
|
|
||||||
:on-remove="(file) => handleRemoveImage(file, 'arrival_photo')"
|
|
||||||
:before-upload="beforeAvatarUpload"
|
|
||||||
>
|
|
||||||
<el-icon><Plus /></el-icon>
|
<el-icon><Plus /></el-icon>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
|
<div class="camera-card" @click="triggerCamera('arrival_photo')"><el-icon><Camera /></el-icon><span class="text">拍照</span></div>
|
||||||
<div class="camera-card" @click="triggerCamera('arrival_photo')">
|
|
||||||
<el-icon><Camera /></el-icon>
|
|
||||||
<span class="text">拍照</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<el-input v-model="form.arrival_photo" placeholder="图片列表" style="display:none;" />
|
<el-input v-model="form.arrival_photo" placeholder="图片列表" style="display:none;" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
|
||||||
<el-col :span="24">
|
<el-col :span="24">
|
||||||
<el-form-item label="检测报告" prop="inspection_report">
|
<el-form-item label="检测报告" prop="inspection_report">
|
||||||
<div class="upload-container">
|
<div class="upload-container">
|
||||||
<el-upload
|
<el-upload v-model:file-list="reportFileList" action="#" list-type="picture-card" multiple :http-request="(opts) => customUpload(opts, 'inspection_report')" :on-preview="handlePreviewPicture" :on-remove="(file) => handleRemoveImage(file, 'inspection_report')" :before-upload="beforeAvatarUpload">
|
||||||
v-model:file-list="reportFileList"
|
|
||||||
action="#"
|
|
||||||
list-type="picture-card"
|
|
||||||
multiple
|
|
||||||
:http-request="(opts) => customUpload(opts, 'inspection_report')"
|
|
||||||
:on-preview="handlePreviewPicture"
|
|
||||||
:on-remove="(file) => handleRemoveImage(file, 'inspection_report')"
|
|
||||||
:before-upload="beforeAvatarUpload"
|
|
||||||
>
|
|
||||||
<el-icon><Plus /></el-icon>
|
<el-icon><Plus /></el-icon>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
|
<div class="camera-card" @click="triggerCamera('inspection_report')"><el-icon><Camera /></el-icon><span class="text">拍照</span></div>
|
||||||
<div class="camera-card" @click="triggerCamera('inspection_report')">
|
|
||||||
<el-icon><Camera /></el-icon>
|
|
||||||
<span class="text">拍照</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<el-input v-model="inspection_report_url" placeholder="如有外部报告链接,请在此输入 (选填)" style="margin-top: 8px;" clearable><template #prefix><el-icon><Link /></el-icon></template></el-input>
|
||||||
|
|
||||||
<el-input
|
|
||||||
v-model="inspection_report_url"
|
|
||||||
placeholder="如有外部报告链接,请在此输入 (选填)"
|
|
||||||
style="margin-top: 8px;"
|
|
||||||
clearable
|
|
||||||
>
|
|
||||||
<template #prefix><el-icon><Link /></el-icon></template>
|
|
||||||
</el-input>
|
|
||||||
|
|
||||||
<el-input v-model="form.inspection_report" placeholder="图片列表" style="display:none;" />
|
<el-input v-model="form.inspection_report" placeholder="图片列表" style="display:none;" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<div class="divider-text">商务与采购信息</div>
|
<div class="divider-text">商务与采购信息</div>
|
||||||
|
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<el-form-item label="币种">
|
<el-form-item label="币种">
|
||||||
<el-autocomplete
|
<el-autocomplete v-model="form.currency" :fetch-suggestions="querySearchCurrency" placeholder="币种" style="width: 100%" :trigger-on-focus="true">
|
||||||
v-model="form.currency"
|
<template #default="{ item }"><span>{{ item.value }}</span><span style="float:right; color:#999; font-size:12px">{{ item.desc }}</span></template>
|
||||||
:fetch-suggestions="querySearchCurrency"
|
|
||||||
placeholder="币种"
|
|
||||||
style="width: 100%"
|
|
||||||
:trigger-on-focus="true"
|
|
||||||
>
|
|
||||||
<template #default="{ item }">
|
|
||||||
<span>{{ item.value }}</span>
|
|
||||||
<span style="float:right; color:#999; font-size:12px">{{ item.desc }}</span>
|
|
||||||
</template>
|
|
||||||
</el-autocomplete>
|
</el-autocomplete>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="6">
|
<el-col :span="6"><el-form-item label="汇率"><el-input-number v-model="form.exchange_rate" :precision="2" controls-position="right" style="width:100%"/></el-form-item></el-col>
|
||||||
<el-form-item label="汇率">
|
<el-col :span="6"><el-form-item label="含税单价" prop="unit_price"><el-input-number v-model="form.unit_price" :precision="4" controls-position="right" style="width:100%"/></el-form-item></el-col>
|
||||||
<el-input-number v-model="form.exchange_rate" :precision="2" controls-position="right"
|
<el-col :span="6"><el-form-item label="总价"><el-input-number v-model="form.total_price" :precision="2" disabled :controls="false" style="width:100%" class="total-price-input"/></el-form-item></el-col>
|
||||||
style="width:100%"/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="6">
|
|
||||||
<el-form-item label="含税单价" prop="unit_price">
|
|
||||||
<el-input-number v-model="form.unit_price" :precision="4" controls-position="right"
|
|
||||||
style="width:100%"/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="6">
|
|
||||||
<el-form-item label="总价">
|
|
||||||
<el-input-number v-model="form.total_price" :precision="2" disabled :controls="false"
|
|
||||||
style="width:100%" class="total-price-input"/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="8">
|
<el-col :span="8"><el-form-item label="供应商"><el-autocomplete v-model="form.supplier_name" :fetch-suggestions="querySearchSupplier" placeholder="输入或选择供应商" style="width: 100%" clearable :trigger-on-focus="true" @select="handleSupplierSelect"/></el-form-item></el-col>
|
||||||
<el-form-item label="供应商">
|
<el-col :span="8"><el-form-item label="采购人"><el-autocomplete v-model="form.purchaser" :fetch-suggestions="querySearchPurchaser" placeholder="输入采购人" style="width: 100%" clearable :trigger-on-focus="true" @select="handlePurchaserSelect"/></el-form-item></el-col>
|
||||||
<el-autocomplete
|
<el-col :span="8"><el-form-item label="采购邮箱"><el-autocomplete v-model="form.purchaser_email" :fetch-suggestions="querySearchEmail" placeholder="输入邮箱" style="width: 100%" clearable :trigger-on-focus="true" @select="handleEmailSelect"/></el-form-item></el-col>
|
||||||
v-model="form.supplier_name"
|
|
||||||
:fetch-suggestions="querySearchSupplier"
|
|
||||||
placeholder="输入或选择供应商"
|
|
||||||
style="width: 100%"
|
|
||||||
clearable
|
|
||||||
:trigger-on-focus="true"
|
|
||||||
@select="handleSupplierSelect"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="8">
|
|
||||||
<el-form-item label="采购人">
|
|
||||||
<el-autocomplete
|
|
||||||
v-model="form.purchaser"
|
|
||||||
:fetch-suggestions="querySearchPurchaser"
|
|
||||||
placeholder="输入采购人"
|
|
||||||
style="width: 100%"
|
|
||||||
clearable
|
|
||||||
:trigger-on-focus="true"
|
|
||||||
@select="handlePurchaserSelect"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="8">
|
|
||||||
<el-form-item label="采购邮箱">
|
|
||||||
<el-autocomplete
|
|
||||||
v-model="form.purchaser_email"
|
|
||||||
:fetch-suggestions="querySearchEmail"
|
|
||||||
placeholder="输入邮箱"
|
|
||||||
style="width: 100%"
|
|
||||||
clearable
|
|
||||||
:trigger-on-focus="true"
|
|
||||||
@select="handleEmailSelect"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="12">
|
<el-col :span="12"><el-form-item label="原始链接"><el-input v-model="form.source_link" placeholder="http://"/></el-form-item></el-col>
|
||||||
<el-form-item label="原始链接">
|
<el-col :span="12"><el-form-item label="详情链接"><el-input v-model="form.detail_link" placeholder="http://"/></el-form-item></el-col>
|
||||||
<el-input v-model="form.source_link" placeholder="http://"/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-form-item label="详情链接">
|
|
||||||
<el-input v-model="form.detail_link" placeholder="http://"/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<el-button @click="visible = false" size="large">取消</el-button>
|
<el-button @click="visible = false" size="large">取消</el-button>
|
||||||
<el-button type="primary" :loading="submitting" @click="submitForm" size="large" class="confirm-btn">
|
<el-button type="primary" :loading="submitting" @click="submitForm" size="large" class="confirm-btn">{{ dialogStatus === 'create' ? '确认入库并打印' : '保存修改' }}</el-button>
|
||||||
{{ dialogStatus === 'create' ? '确认入库并打印' : '保存修改' }}
|
|
||||||
</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<input
|
<input type="file" ref="cameraInputRef" accept="image/*" capture="environment" style="display: none" @change="handleCameraFile"/>
|
||||||
type="file"
|
<el-dialog v-model="dialogVisibleImage" append-to-body width="50%"><img style="width: 100%" :src="dialogImageUrl" alt="Preview Image" /></el-dialog>
|
||||||
ref="cameraInputRef"
|
|
||||||
accept="image/*"
|
|
||||||
capture="environment"
|
|
||||||
style="display: none"
|
|
||||||
@change="handleCameraFile"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<el-dialog v-model="dialogVisibleImage" append-to-body width="50%">
|
|
||||||
<img style="width: 100%" :src="dialogImageUrl" alt="Preview Image" />
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<el-dialog v-model="printVisible" title="标签打印预览" width="400px" destroy-on-close append-to-body>
|
<el-dialog v-model="printVisible" title="标签打印预览" width="400px" destroy-on-close append-to-body>
|
||||||
<div style="text-align: center;">
|
<div style="text-align: center;">
|
||||||
<div v-loading="printLoading" class="preview-box">
|
<div v-loading="printLoading" class="preview-box">
|
||||||
<img v-if="previewUrl" :src="previewUrl" alt="Label Preview" style="width: 100%; border: 1px solid #ccc;"/>
|
<img v-if="previewUrl" :src="previewUrl" alt="Label Preview" style="width: 100%; border: 1px solid #ccc;"/>
|
||||||
<div v-else class="empty-preview">正在生成预览...</div>
|
<div v-else class="empty-preview">正在生成预览...</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top: 20px; font-size: 14px; color: #666;">
|
<div style="margin-top: 20px; font-size: 14px; color: #666;"><p>打印机 IP: 192.168.9.205</p><p>尺寸: 40mm x 30mm</p></div>
|
||||||
<p>打印机 IP: 192.168.9.205</p>
|
|
||||||
<p>尺寸: 40mm x 30mm</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer"><el-button @click="printVisible = false">取消</el-button><el-button type="primary" :loading="printing" @click="confirmPrint"><el-icon><Printer/></el-icon>确认打印</el-button></div>
|
||||||
<el-button @click="printVisible = false">取消</el-button>
|
|
||||||
<el-button type="primary" :loading="printing" @click="confirmPrint">
|
|
||||||
<el-icon>
|
|
||||||
<Printer/>
|
|
||||||
</el-icon>
|
|
||||||
确认打印
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -605,45 +428,50 @@ const dialogStatus = ref<'create' | 'update'>('create')
|
|||||||
const tableData = ref([])
|
const tableData = ref([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const formRef = ref()
|
const formRef = ref()
|
||||||
const queryParams = reactive({page: 1, pageSize: 15, keyword: ''})
|
|
||||||
const materialOptions = ref<any[]>([])
|
|
||||||
|
|
||||||
|
const queryParams = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 15,
|
||||||
|
keyword: '',
|
||||||
|
statuses: ['在库', '借库']
|
||||||
|
})
|
||||||
|
|
||||||
|
const materialOptions = ref<any[]>([])
|
||||||
const printVisible = ref(false)
|
const printVisible = ref(false)
|
||||||
const printLoading = ref(false)
|
const printLoading = ref(false)
|
||||||
const printing = ref(false)
|
const printing = ref(false)
|
||||||
const previewUrl = ref('')
|
const previewUrl = ref('')
|
||||||
const currentPrintData = ref<any>({})
|
const currentPrintData = ref<any>({})
|
||||||
|
|
||||||
const entryMode = ref('batch')
|
const entryMode = ref('batch')
|
||||||
const modeLocked = ref(false)
|
const modeLocked = ref(false)
|
||||||
|
|
||||||
// 图片预览/拍照相关
|
|
||||||
const dialogImageUrl = ref('')
|
const dialogImageUrl = ref('')
|
||||||
const dialogVisibleImage = ref(false)
|
const dialogVisibleImage = ref(false)
|
||||||
const arrivalFileList = ref<any[]>([])
|
const arrivalFileList = ref<any[]>([])
|
||||||
const reportFileList = ref<any[]>([])
|
const reportFileList = ref<any[]>([])
|
||||||
const cameraInputRef = ref<HTMLInputElement | null>(null)
|
const cameraInputRef = ref<HTMLInputElement | null>(null)
|
||||||
const currentCameraField = ref<'arrival_photo' | 'inspection_report'>('arrival_photo')
|
const currentCameraField = ref<'arrival_photo' | 'inspection_report'>('arrival_photo')
|
||||||
// [新增] 检测报告外部链接输入框
|
|
||||||
const inspection_report_url = ref('')
|
const inspection_report_url = ref('')
|
||||||
|
|
||||||
// 列定义
|
// 基础列
|
||||||
const baseColumns = [
|
const baseColumns = [
|
||||||
{prop: 'material_name', label: '名称'},
|
{prop: 'material_name', label: '名称'},
|
||||||
|
{prop: 'material_type', label: '类型'}, // 移到类别前面
|
||||||
{prop: 'category', label: '类别'},
|
{prop: 'category', label: '类别'},
|
||||||
{prop: 'material_type', label: '类型'},
|
|
||||||
{prop: 'spec_model', label: '规格型号'},
|
{prop: 'spec_model', label: '规格型号'},
|
||||||
{prop: 'unit', label: '单位'},
|
{prop: 'unit', label: '单位'},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// [修改] 库存与商务列配置:将序列号/批号改为 "序列号/批号"
|
||||||
const stockColumns = [
|
const stockColumns = [
|
||||||
{prop: 'id', label: 'ID', minWidth: '60'},
|
{prop: 'id', label: 'ID', minWidth: '60'},
|
||||||
{prop: 'base_id', label: 'BaseID', minWidth: '80'},
|
{prop: 'base_id', label: 'BaseID', minWidth: '80'},
|
||||||
{prop: 'sku', label: 'SKU', minWidth: '120'},
|
{prop: 'sku', label: 'SKU', minWidth: '120'},
|
||||||
{prop: 'inbound_date', label: '入库日期', minWidth: '120'},
|
{prop: 'inbound_date', label: '入库日期', minWidth: '120'},
|
||||||
{prop: 'barcode', label: '条码', minWidth: '120'},
|
{prop: 'barcode', label: '条码', minWidth: '120'},
|
||||||
{prop: 'serial_number', label: '序列号', minWidth: '150'},
|
|
||||||
{prop: 'batch_number', label: '批号', minWidth: '150'},
|
// 新的合并列,修改 label 为 "序列号/批号"
|
||||||
|
{prop: 'sn_bn', label: '序列号/批号', minWidth: '160'},
|
||||||
|
|
||||||
{prop: 'status', label: '状态', minWidth: '100'},
|
{prop: 'status', label: '状态', minWidth: '100'},
|
||||||
{prop: 'inspection_status', label: '到检', minWidth: '100'},
|
{prop: 'inspection_status', label: '到检', minWidth: '100'},
|
||||||
{prop: 'qty_inbound', label: '入库量', minWidth: '100'},
|
{prop: 'qty_inbound', label: '入库量', minWidth: '100'},
|
||||||
@ -664,49 +492,31 @@ const stockColumns = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const allColumns = [...baseColumns, ...stockColumns]
|
const allColumns = [...baseColumns, ...stockColumns]
|
||||||
|
// [修改] 更新 key 以强制用户获取新默认值
|
||||||
|
const STORAGE_KEY_COLS = 'stock_buy_visible_columns_v2'
|
||||||
|
|
||||||
const STORAGE_KEY_COLS = 'stock_buy_visible_columns'
|
// [修改] 默认列配置:加入 'sn_bn' 和 'warehouse_loc'
|
||||||
|
// 同时这里也要对应上方的顺序变化,先 material_type 后 category
|
||||||
const defaultColumns = [
|
const defaultColumns = [
|
||||||
'material_name', 'category', 'material_type', 'spec_model', 'unit',
|
'material_name', 'material_type', 'category', 'spec_model', 'unit',
|
||||||
'inbound_date', 'serial_number', 'batch_number', 'status', 'inspection_status',
|
'inbound_date', 'sn_bn', 'warehouse_loc', 'status', 'inspection_status',
|
||||||
'unit_price', 'total_price', 'supplier_name', 'purchaser', 'qty_stock', 'qty_available', 'arrival_photo', 'inspection_report'
|
'unit_price', 'total_price', 'supplier_name', 'purchaser', 'qty_stock', 'qty_available', 'arrival_photo', 'inspection_report'
|
||||||
]
|
]
|
||||||
|
|
||||||
const getSavedColumns = () => {
|
const getSavedColumns = () => { try { const saved = localStorage.getItem(STORAGE_KEY_COLS); return saved ? JSON.parse(saved) : defaultColumns } catch (e) { return defaultColumns } }
|
||||||
try {
|
|
||||||
const saved = localStorage.getItem(STORAGE_KEY_COLS)
|
|
||||||
return saved ? JSON.parse(saved) : defaultColumns
|
|
||||||
} catch (e) {
|
|
||||||
return defaultColumns
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const visibleColumnProps = ref(getSavedColumns())
|
const visibleColumnProps = ref(getSavedColumns())
|
||||||
|
watch(visibleColumnProps, (newVal) => { localStorage.setItem(STORAGE_KEY_COLS, JSON.stringify(newVal)) }, {deep: true})
|
||||||
watch(visibleColumnProps, (newVal) => {
|
|
||||||
localStorage.setItem(STORAGE_KEY_COLS, JSON.stringify(newVal))
|
|
||||||
}, {deep: true})
|
|
||||||
|
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
id: undefined,
|
id: undefined, base_id: undefined as number | undefined, material_name: '', spec_model: '', category: '', unit: '', material_type: '',
|
||||||
base_id: undefined as number | undefined,
|
sku: '', barcode: '', in_date: '', serial_number: '', batch_number: '', status: '在库', inspection_status: '未检',
|
||||||
material_name: '', spec_model: '', category: '', unit: '', material_type: '',
|
in_quantity: 1, stock_quantity: 1, available_quantity: 1, warehouse_location: '',
|
||||||
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,
|
unit_price: 0, total_price: 0, currency: 'CNY', exchange_rate: 1.00,
|
||||||
supplier_name: '', purchaser: '', purchaser_email: '',
|
supplier_name: '', purchaser: '', purchaser_email: '', source_link: '', detail_link: '',
|
||||||
source_link: '', detail_link: '',
|
arrival_photo: [] as string[], inspection_report: [] as string[]
|
||||||
arrival_photo: [] as string[],
|
|
||||||
inspection_report: [] as string[]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// ------------------------------------
|
// ... (以下逻辑保持不变)
|
||||||
// 历史记录管理器
|
|
||||||
// ------------------------------------
|
|
||||||
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) => {
|
const saveToHistory = (key: string, value: string) => {
|
||||||
if (!value) return
|
if (!value) return
|
||||||
@ -737,9 +547,6 @@ const getMaterialHistory = () => {
|
|||||||
try { return JSON.parse(localStorage.getItem(HISTORY_KEYS.MATERIAL) || '[]') } 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) => (item.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0) }
|
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 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 mixedSearch = (queryString: string, tableField: string, storageKey: string, cb: any) => {
|
||||||
@ -759,18 +566,9 @@ const querySearchPurchaser = (qs: string, cb: any) => mixedSearch(qs, 'purchaser
|
|||||||
const handlePurchaserSelect = (item: any) => saveToHistory(HISTORY_KEYS.PURCHASER, item.value)
|
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 querySearchEmail = (qs: string, cb: any) => mixedSearch(qs, 'purchaser_email', HISTORY_KEYS.EMAIL, cb)
|
||||||
const handleEmailSelect = (item: any) => saveToHistory(HISTORY_KEYS.EMAIL, item.value)
|
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 querySearchCurrency = (queryString: string, cb: any) => { cb(queryString ? currencyOptions.filter(createFilter(queryString)) : currencyOptions) }
|
||||||
// 如果输入为空,返回所有选项
|
|
||||||
const results = queryString ? currencyOptions.filter(createFilter(queryString)) : currencyOptions
|
|
||||||
cb(results)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------
|
|
||||||
// Material Search Logic
|
|
||||||
// ------------------------------------
|
|
||||||
const handleMaterialDropdownVisible = (visible: boolean) => { if (visible && materialOptions.value.length === 0) handleSearchMaterial('') }
|
const handleMaterialDropdownVisible = (visible: boolean) => { if (visible && materialOptions.value.length === 0) handleSearchMaterial('') }
|
||||||
const handleSearchMaterial = async (query: string) => {
|
const handleSearchMaterial = async (query: string) => {
|
||||||
searchLoading.value = true
|
searchLoading.value = true
|
||||||
@ -797,9 +595,6 @@ const onMaterialSelected = (val: number) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------
|
|
||||||
// Validation Logic
|
|
||||||
// ------------------------------------
|
|
||||||
const validateUnique = (rule: any, value: string, callback: any) => {
|
const validateUnique = (rule: any, value: string, callback: any) => {
|
||||||
if (!value) return callback()
|
if (!value) return callback()
|
||||||
const isDuplicate = tableData.value.some((row: any) => {
|
const isDuplicate = tableData.value.some((row: any) => {
|
||||||
@ -830,18 +625,11 @@ const checkHistoryAndSetMode = async (baseId: number) => {
|
|||||||
if (historyItems.length > 0) {
|
if (historyItems.length > 0) {
|
||||||
modeLocked.value = true
|
modeLocked.value = true
|
||||||
const latest = historyItems.sort((a: any, b: any) => b.id - a.id)[0]
|
const latest = historyItems.sort((a: any, b: any) => b.id - a.id)[0]
|
||||||
if (latest.serial_number) {
|
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 = ''; form.batch_number = incrementBatchNumber(latest.batch_number || '000000') }
|
||||||
} else {
|
} else { modeLocked.value = false; entryMode.value = 'batch'; form.batch_number = '000001' }
|
||||||
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') }
|
if (formRef.value) { formRef.value.clearValidate('serial_number'); formRef.value.clearValidate('batch_number') }
|
||||||
} catch (e) {
|
} catch (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) => {
|
const incrementBatchNumber = (batchStr: string) => {
|
||||||
if (!batchStr || !/^\d+$/.test(batchStr)) return '000001'
|
if (!batchStr || !/^\d+$/.test(batchStr)) return '000001'
|
||||||
@ -856,7 +644,8 @@ watch(() => [form.in_quantity, form.unit_price], () => { form.total_price = Numb
|
|||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res: any = await getBuyList(queryParams)
|
const params = { ...queryParams, statuses: queryParams.statuses.join(',') }
|
||||||
|
const res: any = await getBuyList(params)
|
||||||
tableData.value = res.data.items || []
|
tableData.value = res.data.items || []
|
||||||
total.value = res.data.total || 0
|
total.value = res.data.total || 0
|
||||||
} finally { loading.value = false }
|
} finally { loading.value = false }
|
||||||
@ -873,12 +662,10 @@ const handleCreate = () => {
|
|||||||
materialOptions.value = []
|
materialOptions.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
// [核心逻辑] 更新表单时的多图回显处理
|
|
||||||
const handleUpdate = (row: any) => {
|
const handleUpdate = (row: any) => {
|
||||||
dialogStatus.value = 'update'
|
dialogStatus.value = 'update'
|
||||||
resetForm()
|
resetForm()
|
||||||
modeLocked.value = true
|
modeLocked.value = true
|
||||||
|
|
||||||
Object.assign(form, {
|
Object.assign(form, {
|
||||||
id: row.id, base_id: row.base_id, material_name: row.material_name, spec_model: row.spec_model, category: row.category,
|
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,
|
unit: row.unit, material_type: row.material_type, sku: row.sku, barcode: row.barcode, in_date: row.inbound_date,
|
||||||
@ -887,61 +674,29 @@ const handleUpdate = (row: any) => {
|
|||||||
unit_price: Number(row.unit_price), total_price: Number(row.total_price), currency: row.currency, exchange_rate: Number(row.exchange_rate),
|
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,
|
supplier_name: row.supplier_name, purchaser: row.purchaser, purchaser_email: row.purchaser_email,
|
||||||
source_link: row.source_link, detail_link: row.detail_link,
|
source_link: row.source_link, detail_link: row.detail_link,
|
||||||
// 后端返回的是数组
|
arrival_photo: row.arrival_photo || [], inspection_report: row.inspection_report || []
|
||||||
arrival_photo: row.arrival_photo || [],
|
|
||||||
inspection_report: row.inspection_report || []
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 1. 同步图片列表
|
|
||||||
arrivalFileList.value = form.arrival_photo.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }))
|
arrivalFileList.value = form.arrival_photo.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }))
|
||||||
|
|
||||||
// 2. 分离检测报告中的图片和链接
|
|
||||||
const reports = form.inspection_report || []
|
const reports = form.inspection_report || []
|
||||||
const reportImgs = reports.filter(r => !isExternalLink(r))
|
const reportImgs = reports.filter(r => !isExternalLink(r))
|
||||||
const reportLinks = reports.filter(r => isExternalLink(r))
|
const reportLinks = reports.filter(r => isExternalLink(r))
|
||||||
|
|
||||||
reportFileList.value = reportImgs.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }))
|
reportFileList.value = reportImgs.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }))
|
||||||
inspection_report_url.value = reportLinks.length > 0 ? reportLinks[0] : ''
|
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 = '' }
|
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 = '' }
|
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 }]
|
materialOptions.value = [{ id: row.base_id, name: row.material_name, spec: row.spec_model, category: row.category }]
|
||||||
visible.value = true
|
visible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------
|
const getImageUrl = (url: string) => { return !url ? '' : (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 getImageUrl = (url: string) => {
|
const hasExternalLink = (list: string[]) => { return !list ? false : list.some(item => isExternalLink(item)) }
|
||||||
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 beforeAvatarUpload = (rawFile: any) => {
|
const beforeAvatarUpload = (rawFile: any) => {
|
||||||
if (rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/png') { ElMessage.error('仅支持 JPG/PNG'); 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 }
|
if (rawFile.size / 1024 / 1024 > 5) { ElMessage.error('图片不能超过 5MB'); return false }
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通用上传
|
|
||||||
const customUpload = async (options: any, targetField: 'arrival_photo' | 'inspection_report') => {
|
const customUpload = async (options: any, targetField: 'arrival_photo' | 'inspection_report') => {
|
||||||
const { file, onSuccess, onError } = options
|
const { file, onSuccess, onError } = options
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
@ -950,129 +705,52 @@ const customUpload = async (options: any, targetField: 'arrival_photo' | 'inspec
|
|||||||
const res: any = await uploadFile(formData)
|
const res: any = await uploadFile(formData)
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
const newUrl = res.data.url
|
const newUrl = res.data.url
|
||||||
form[targetField].push(newUrl) // 添加到表单数组
|
form[targetField].push(newUrl)
|
||||||
ElMessage.success('上传成功')
|
ElMessage.success('上传成功')
|
||||||
onSuccess(res)
|
onSuccess(res)
|
||||||
} else {
|
} else { ElMessage.error(res.msg || '上传失败'); onError(new Error(res.msg)) }
|
||||||
ElMessage.error(res.msg || '上传失败')
|
|
||||||
onError(new Error(res.msg))
|
|
||||||
}
|
|
||||||
} catch (e) { ElMessage.error('网络错误'); onError(e) }
|
} catch (e) { ElMessage.error('网络错误'); onError(e) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除图片 (从数组移除 + 物理删除)
|
|
||||||
const handleRemoveImage = async (uploadFile: any, targetField: 'arrival_photo' | 'inspection_report') => {
|
const handleRemoveImage = async (uploadFile: any, targetField: 'arrival_photo' | 'inspection_report') => {
|
||||||
try {
|
try {
|
||||||
const urlToRemove = form[targetField].find(u => getImageUrl(u) === uploadFile.url) || uploadFile.url
|
const urlToRemove = form[targetField].find(u => getImageUrl(u) === uploadFile.url) || uploadFile.url
|
||||||
|
|
||||||
// 1. 从前端数组移除
|
|
||||||
form[targetField] = form[targetField].filter(u => u !== urlToRemove)
|
form[targetField] = form[targetField].filter(u => u !== urlToRemove)
|
||||||
|
if (!isExternalLink(urlToRemove)) { const filename = urlToRemove.split('/').pop(); if (filename) await deleteFile(filename) }
|
||||||
// 2. 调用后端物理删除 (仅对本地文件)
|
|
||||||
if (!isExternalLink(urlToRemove)) {
|
|
||||||
const filename = urlToRemove.split('/').pop()
|
|
||||||
if (filename) await deleteFile(filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
ElMessage.success('已删除')
|
ElMessage.success('已删除')
|
||||||
} catch (e) { console.error(e) }
|
} catch (e) { console.error(e) }
|
||||||
}
|
}
|
||||||
|
const handlePreviewPicture = (uploadFile: any) => { dialogImageUrl.value = uploadFile.url!; dialogVisibleImage.value = true }
|
||||||
// 预览大图
|
const triggerCamera = (field: 'arrival_photo' | 'inspection_report') => { currentCameraField.value = field; if (cameraInputRef.value) cameraInputRef.value.click() }
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 处理拍照回调
|
|
||||||
const handleCameraFile = async (event: Event) => {
|
const handleCameraFile = async (event: Event) => {
|
||||||
const input = event.target as HTMLInputElement
|
const input = event.target as HTMLInputElement
|
||||||
if (input.files && input.files[0]) {
|
if (input.files && input.files[0]) {
|
||||||
const file = input.files[0]
|
const file = input.files[0]
|
||||||
|
if (!beforeAvatarUpload(file)) { input.value = ''; return }
|
||||||
// 校验
|
const formData = new FormData(); formData.append('file', file)
|
||||||
if (!beforeAvatarUpload(file)) {
|
|
||||||
input.value = ''
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const formData = new FormData()
|
|
||||||
formData.append('file', file)
|
|
||||||
const loadingMsg = ElMessage.loading({ message: '照片上传中...', duration: 0 })
|
const loadingMsg = ElMessage.loading({ message: '照片上传中...', duration: 0 })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. 上传文件
|
|
||||||
const res: any = await uploadFile(formData)
|
const res: any = await uploadFile(formData)
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
const newUrl = res.data.url
|
const newUrl = res.data.url; const field = currentCameraField.value
|
||||||
const field = currentCameraField.value
|
|
||||||
|
|
||||||
// 2. 更新表单数据数组
|
|
||||||
form[field].push(newUrl)
|
form[field].push(newUrl)
|
||||||
|
if (field === 'arrival_photo') arrivalFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) })
|
||||||
// 3. 同步更新 el-upload 的 fileList,确保界面立即显示缩略图
|
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('拍照上传成功')
|
ElMessage.success('拍照上传成功')
|
||||||
} else {
|
} else { ElMessage.error(res.msg || '上传失败') }
|
||||||
ElMessage.error(res.msg || '上传失败')
|
} catch (e) { ElMessage.error('网络错误,上传失败') } finally { loadingMsg.close(); input.value = '' }
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
ElMessage.error('网络错误,上传失败')
|
|
||||||
} finally {
|
|
||||||
loadingMsg.close()
|
|
||||||
input.value = '' // 清空 input 防止无法连续拍同一场景
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------
|
|
||||||
// 提交逻辑
|
|
||||||
// ------------------------------------
|
|
||||||
const submitForm = async () => {
|
const submitForm = async () => {
|
||||||
if (!formRef.value) return
|
if (!formRef.value) return
|
||||||
await formRef.value.validate(async (valid: boolean) => {
|
await formRef.value.validate(async (valid: boolean) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
|
|
||||||
// [核心新增] 提交前,将 inspection_report_url 加入到 form.inspection_report 数组中
|
|
||||||
// 注意:这里我们做个临时的合并,避免重复添加
|
|
||||||
const finalReportList = [...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)
|
||||||
// 如果输入框有值,且数组里还没这个值,就加进去
|
|
||||||
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))
|
const onlyImages = finalReportList.filter(item => !isExternalLink(item))
|
||||||
if (inspection_report_url.value) {
|
if (inspection_report_url.value) onlyImages.push(inspection_report_url.value)
|
||||||
onlyImages.push(inspection_report_url.value)
|
const payload = { ...form, inspection_report: onlyImages, in_quantity: Number(form.in_quantity), unit_price: Number(form.unit_price) }
|
||||||
}
|
|
||||||
|
|
||||||
// 更新 form 用于提交
|
|
||||||
// 拷贝一份 payload 防止污染响应式对象
|
|
||||||
const payload = {
|
|
||||||
...form,
|
|
||||||
inspection_report: onlyImages,
|
|
||||||
in_quantity: Number(form.in_quantity),
|
|
||||||
unit_price: Number(form.unit_price)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (dialogStatus.value === 'create') {
|
if (dialogStatus.value === 'create') {
|
||||||
const res: any = await createBuyInbound(payload)
|
const res: any = await createBuyInbound(payload)
|
||||||
@ -1082,42 +760,23 @@ const submitForm = async () => {
|
|||||||
try { await executePrint(res.data); ElMessage.success('打印指令已发送') }
|
try { await executePrint(res.data); ElMessage.success('打印指令已发送') }
|
||||||
catch (printErr: any) { ElMessage.warning('打印失败:' + (printErr.msg || '未知错误')) }
|
catch (printErr: any) { ElMessage.warning('打印失败:' + (printErr.msg || '未知错误')) }
|
||||||
}
|
}
|
||||||
} else {
|
} else { await updateBuyInbound(form.id!, payload); ElMessage.success('更新成功') }
|
||||||
await updateBuyInbound(form.id!, payload)
|
saveToHistory(HISTORY_KEYS.SUPPLIER, form.supplier_name); saveToHistory(HISTORY_KEYS.PURCHASER, form.purchaser); saveToHistory(HISTORY_KEYS.EMAIL, form.purchaser_email)
|
||||||
ElMessage.success('更新成功')
|
await fetchData(); visible.value = false
|
||||||
}
|
} catch (e: any) { ElMessage.error(e.msg || '操作失败') } finally { submitting.value = false }
|
||||||
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 }
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
const handleDelete = async (row: any) => { try { await deleteBuyInbound(row.id); ElMessage.success('删除成功'); fetchData() } catch (e) { ElMessage.error('删除失败') } }
|
||||||
const handleDelete = async (row: any) => {
|
|
||||||
try { await deleteBuyInbound(row.id); ElMessage.success('删除成功'); fetchData() } catch (e) { ElMessage.error('删除失败') }
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePrint = async (row: any) => {
|
const handlePrint = async (row: any) => {
|
||||||
printVisible.value = true; printLoading.value = true; previewUrl.value = ''
|
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 }
|
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 }
|
try { const res: any = await getLabelPreview(currentPrintData.value); previewUrl.value = res.data }
|
||||||
catch (e) { ElMessage.error('预览失败') } finally { printLoading.value = false }
|
catch (e) { ElMessage.error('预览失败') } finally { printLoading.value = false }
|
||||||
}
|
}
|
||||||
const confirmPrint = async () => {
|
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 } }
|
||||||
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 = () => {
|
const resetForm = () => {
|
||||||
materialOptions.value = []
|
materialOptions.value = []; arrivalFileList.value = []; reportFileList.value = []; inspection_report_url.value = ''
|
||||||
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: [] })
|
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 getStatusType = (status: string) => { const map: any = {'在库': 'success', '出库': 'info', '损耗': 'danger'}; return map[status] || 'warning' }
|
||||||
@ -1129,13 +788,15 @@ onMounted(() => fetchData())
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.buy-module { background: #f5f7fa; padding: 20px; min-height: 100vh; }
|
.buy-module { background: #f5f7fa; padding: 20px; min-height: 100vh; }
|
||||||
.header-tools { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; background: #fff; padding: 15px 20px; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); }
|
.header-tools { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; background: #fff; padding: 15px 20px; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); }
|
||||||
.left-tools { flex: 0 0 350px; }
|
.left-tools { display: flex; gap: 10px; align-items: center; flex: 1; }
|
||||||
.right-tools { display: flex; gap: 10px; align-items: center; }
|
.right-tools { display: flex; gap: 10px; align-items: center; }
|
||||||
.action-btn { font-weight: 500; }
|
.action-btn { font-weight: 500; }
|
||||||
.modern-table { border-radius: 8px; overflow: hidden; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); }
|
.modern-table { border-radius: 8px; overflow: hidden; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); }
|
||||||
:deep(.table-header-gray th) { background-color: #f8f9fb !important; color: #606266; font-weight: 600; height: 50px; }
|
:deep(.table-header-gray th) { background-color: #f8f9fb !important; color: #606266; font-weight: 600; height: 50px; }
|
||||||
.tag-sn { color: #409EFF; font-weight: bold; font-family: monospace; }
|
.tag-sn { color: #409EFF; font-weight: bold; font-family: monospace; background: #ecf5ff; padding: 0 4px; border-radius: 4px; margin-right: 4px; font-size: 12px; }
|
||||||
.tag-bn { color: #67C23A; font-weight: bold; font-family: monospace; }
|
.tag-bn { color: #67C23A; font-weight: bold; font-family: monospace; background: #f0f9eb; padding: 0 4px; border-radius: 4px; margin-right: 4px; font-size: 12px; }
|
||||||
|
.id-cell { display: flex; align-items: center; }
|
||||||
|
.id-text { font-family: monospace; color: #606266; }
|
||||||
.money-text { font-family: 'Consolas', monospace; color: #303133; }
|
.money-text { font-family: 'Consolas', monospace; color: #303133; }
|
||||||
.stock-num { font-weight: bold; color: #333; font-size: 15px; }
|
.stock-num { font-weight: bold; color: #333; font-size: 15px; }
|
||||||
.avail-num { font-weight: bold; color: #67C23A; font-size: 15px; }
|
.avail-num { font-weight: bold; color: #67C23A; font-size: 15px; }
|
||||||
@ -1172,30 +833,10 @@ onMounted(() => fetchData())
|
|||||||
.more-images-badge { margin-left: 5px; background: #909399; color: #fff; border-radius: 10px; padding: 0 6px; font-size: 12px; }
|
.more-images-badge { margin-left: 5px; background: #909399; color: #fff; border-radius: 10px; padding: 0 6px; font-size: 12px; }
|
||||||
.clickable-text { color: #409EFF; cursor: pointer; font-weight: 500; text-decoration: underline; }
|
.clickable-text { color: #409EFF; cursor: pointer; font-weight: 500; text-decoration: underline; }
|
||||||
.clickable-text:hover { color: #66b1ff; }
|
.clickable-text:hover { color: #66b1ff; }
|
||||||
|
|
||||||
/* 容器:让上传组件和拍照按钮横向排列 */
|
|
||||||
.upload-container { display: flex; flex-wrap: wrap; gap: 8px; }
|
.upload-container { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||||
|
|
||||||
/* 隐藏 el-upload 的 input 样式,只展示图片卡片 */
|
|
||||||
:deep(.el-upload--picture-card) { width: 100px; height: 100px; line-height: 100px; }
|
:deep(.el-upload--picture-card) { width: 100px; height: 100px; line-height: 100px; }
|
||||||
:deep(.el-upload-list--picture-card .el-upload-list__item) { width: 100px; height: 100px; }
|
:deep(.el-upload-list--picture-card .el-upload-list__item) { width: 100px; height: 100px; }
|
||||||
|
.camera-card { width: 100px; height: 100px; background-color: #fbfdff; border: 1px dashed #c0ccda; border-radius: 6px; box-sizing: border-box; display: flex; flex-direction: column; justify-content: center; align-items: center; cursor: pointer; transition: all 0.3s; color: #8c939d; }
|
||||||
/* 拍照按钮卡片样式 (模仿 el-upload--picture-card) */
|
|
||||||
.camera-card {
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
background-color: #fbfdff;
|
|
||||||
border: 1px dashed #c0ccda;
|
|
||||||
border-radius: 6px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
color: #8c939d;
|
|
||||||
}
|
|
||||||
.camera-card:hover { border-color: #409EFF; color: #409EFF; }
|
.camera-card:hover { border-color: #409EFF; color: #409EFF; }
|
||||||
.camera-card .text { font-size: 12px; margin-top: 5px; }
|
.camera-card .text { font-size: 12px; margin-top: 5px; }
|
||||||
.camera-card .el-icon { font-size: 24px; }
|
.camera-card .el-icon { font-size: 24px; }
|
||||||
|
|||||||
Reference in New Issue
Block a user