针对于采购页面进行优化逻辑
This commit is contained in:
@ -2,49 +2,59 @@ from flask import Blueprint, request, jsonify
|
||||
from app.services.inbound.buy_service import BuyInboundService
|
||||
import traceback
|
||||
|
||||
# 定义蓝图
|
||||
inbound_buy_bp = Blueprint('inbound_buy', __name__)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 0. 基础物料搜索 (新增)
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_buy_bp.route('/search-base', methods=['GET'])
|
||||
def search_base():
|
||||
"""
|
||||
供前端下拉框远程搜索使用
|
||||
Query Param: keyword (名称或规格)
|
||||
"""
|
||||
try:
|
||||
keyword = request.args.get('keyword', '')
|
||||
data = BuyInboundService.search_base_material(keyword)
|
||||
return jsonify({
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": data
|
||||
})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 1. 获取列表 (GET)
|
||||
# 1. 获取列表
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_buy_bp.route('/list', methods=['GET'])
|
||||
def get_list():
|
||||
try:
|
||||
page = request.args.get('page', 1, type=int)
|
||||
limit = request.args.get('pageSize', 15, type=int)
|
||||
|
||||
result = BuyInboundService.get_list(page, limit)
|
||||
return jsonify({
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": result
|
||||
})
|
||||
return jsonify({"code": 200, "msg": "success", "data": result})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 2. 新增入库 (POST)
|
||||
# 2. 新增入库
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_buy_bp.route('/submit', methods=['POST'])
|
||||
def submit():
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({"code": 400, "msg": "No data provided"}), 400
|
||||
|
||||
return jsonify({"code": 400, "msg": "No data"}), 400
|
||||
BuyInboundService.handle_inbound(data)
|
||||
return jsonify({"code": 200, "msg": "入库成功"})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 3. 更新入库 (PUT)
|
||||
# 3. 更新入库
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_buy_bp.route('/<int:id>', methods=['PUT'])
|
||||
def update_buy(id):
|
||||
@ -53,12 +63,10 @@ def update_buy(id):
|
||||
BuyInboundService.update_inbound(id, data)
|
||||
return jsonify({"code": 200, "msg": "更新成功"})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 4. 删除入库 (DELETE)
|
||||
# 4. 删除
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_buy_bp.route('/<int:id>', methods=['DELETE'])
|
||||
def delete_buy(id):
|
||||
@ -66,5 +74,4 @@ def delete_buy(id):
|
||||
BuyInboundService.delete_inbound(id)
|
||||
return jsonify({"code": 200, "msg": "删除成功"})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
@ -1,69 +1,112 @@
|
||||
#stock.py
|
||||
# app/models/stock.py
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class StockBuy(db.Model):
|
||||
"""
|
||||
采购入库库存表
|
||||
对应数据库表: stock_buy
|
||||
"""
|
||||
__tablename__ = 'stock_buy'
|
||||
|
||||
# 主键
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
# 【核心关联】
|
||||
# 这里明确指定了 base_id 是外键,关联 material_base 表的 id
|
||||
# 【核心关联】外键关联 material_base 表
|
||||
base_id = db.Column(db.Integer, db.ForeignKey('material_base.id'), nullable=False)
|
||||
|
||||
# --- 身份标识 ---
|
||||
sku = db.Column(db.String(100))
|
||||
in_date = db.Column(db.Date)
|
||||
serial_number = db.Column(db.String(100))
|
||||
batch_number = db.Column(db.String(100))
|
||||
barcode = db.Column(db.String(100)) # 条码
|
||||
serial_number = db.Column(db.String(100)) # 序列号
|
||||
batch_number = db.Column(db.String(100)) # 批号
|
||||
|
||||
# 数量
|
||||
# --- 数量 ---
|
||||
in_quantity = db.Column(db.Numeric(19, 4), default=0)
|
||||
stock_quantity = db.Column(db.Numeric(19, 4), default=0)
|
||||
available_quantity = db.Column(db.Numeric(19, 4), default=0)
|
||||
|
||||
# 状态与位置
|
||||
status = db.Column(db.String(50))
|
||||
inspection_status = db.Column(db.String(50))
|
||||
# --- 状态与位置 ---
|
||||
status = db.Column(db.String(50)) # 在库/出库/损耗
|
||||
inspection_status = db.Column(db.String(50)) # 未检/合格/不合格
|
||||
warehouse_location = db.Column(db.String(100))
|
||||
|
||||
# 财务与商务
|
||||
# --- 财务与商务 ---
|
||||
unit_price = db.Column(db.Numeric(19, 4), default=0)
|
||||
total_price = db.Column(db.Numeric(19, 4), default=0)
|
||||
currency = db.Column(db.String(20), default='CNY')
|
||||
exchange_rate = db.Column(db.Numeric(15, 6), default=1.0)
|
||||
|
||||
supplier_name = db.Column(db.String(255))
|
||||
|
||||
# [关键映射区]:Python属性名 = DB列名
|
||||
# 前端传 purchaser -> 存入 buyer_name
|
||||
buyer_name = db.Column(db.String(100))
|
||||
|
||||
# 前端传 purchaser_email -> 存入 buyer_email
|
||||
buyer_email = db.Column(db.String(100))
|
||||
|
||||
# 前端传 source_link -> 存入 original_link
|
||||
original_link = db.Column(db.Text)
|
||||
|
||||
detail_link = db.Column(db.Text)
|
||||
arrival_photo = db.Column(db.Text)
|
||||
|
||||
# 【核心关联】
|
||||
# 建立对象级别的连接,方便通过 stock.material 访问基础信息
|
||||
# [这就是报错缺失的字段],请确保执行了 ALTER TABLE
|
||||
remark = db.Column(db.Text)
|
||||
|
||||
# 【关系定义】
|
||||
# 建立与 MaterialBase 的关系,方便通过 stock.material 访问基础信息
|
||||
material = db.relationship('MaterialBase', back_populates='stock_buys')
|
||||
|
||||
def to_dict(self):
|
||||
"""序列化"""
|
||||
"""
|
||||
序列化:将模型转换为字典,主要用于单条查询或内部调用
|
||||
列表查询主要依赖 Service 层的手动构建以提高性能
|
||||
"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'base_id': self.base_id, # 前端需要这个ID来判断关联
|
||||
'material_name': self.material.name if self.material else None,
|
||||
'spec_model': self.material.spec_model if self.material else None,
|
||||
'category': self.material.category if self.material else None,
|
||||
'unit': self.material.unit if self.material else None,
|
||||
'material_type': self.material.material_type if self.material else None,
|
||||
'base_id': self.base_id,
|
||||
|
||||
# 级联基础信息 (防止 None 报错)
|
||||
'material_name': self.material.name if self.material else '',
|
||||
'spec_model': self.material.spec_model if self.material else '',
|
||||
'category': self.material.category if self.material else '',
|
||||
'unit': self.material.unit if self.material else '',
|
||||
'material_type': self.material.material_type if self.material else '',
|
||||
|
||||
# 实体信息
|
||||
'sku': self.sku,
|
||||
'inbound_date': self.in_date.strftime('%Y-%m-%d') if self.in_date else None,
|
||||
'inbound_date': self.in_date.strftime('%Y-%m-%d') if self.in_date else '',
|
||||
'barcode': self.barcode,
|
||||
'serial_number': self.serial_number,
|
||||
'batch_number': self.batch_number,
|
||||
'qty_inbound': float(self.in_quantity) if self.in_quantity else 0,
|
||||
'qty_stock': float(self.stock_quantity) if self.stock_quantity else 0,
|
||||
'qty_available': float(self.available_quantity) if self.available_quantity else 0,
|
||||
'warehouse_loc': self.warehouse_location,
|
||||
'status': self.status,
|
||||
'price_unit': float(self.unit_price) if self.unit_price else 0,
|
||||
'price_total': float(self.total_price) if self.total_price else 0,
|
||||
'supplier_name': self.supplier_name
|
||||
'inspection_status': self.inspection_status,
|
||||
'remark': self.remark,
|
||||
|
||||
# 数量 (转为float防止json序列化报错)
|
||||
'in_quantity': float(self.in_quantity or 0),
|
||||
'qty_inbound': float(self.in_quantity or 0), # 兼容字段
|
||||
'stock_quantity': float(self.stock_quantity or 0),
|
||||
'qty_stock': float(self.stock_quantity or 0), # 兼容字段
|
||||
'available_quantity': float(self.available_quantity or 0),
|
||||
'qty_available': float(self.available_quantity or 0), # 兼容字段
|
||||
|
||||
# 财务
|
||||
'unit_price': float(self.unit_price or 0),
|
||||
'total_price': float(self.total_price or 0),
|
||||
'currency': self.currency,
|
||||
'exchange_rate': float(self.exchange_rate or 1.0),
|
||||
|
||||
# 商务 (字段映射)
|
||||
'supplier_name': self.supplier_name,
|
||||
'purchaser': self.buyer_name, # 映射回前端
|
||||
'purchaser_email': self.buyer_email, # 映射回前端
|
||||
'source_link': self.original_link, # 映射回前端
|
||||
'detail_link': self.detail_link,
|
||||
'arrival_photo': self.arrival_photo
|
||||
}
|
||||
@ -1,77 +1,111 @@
|
||||
# app/services/inbound/buy_service.py
|
||||
from app.extensions import db
|
||||
from app.models.material import MaterialBase
|
||||
from app.models.stock import StockBuy
|
||||
from datetime import datetime
|
||||
from sqlalchemy import or_
|
||||
import traceback
|
||||
|
||||
|
||||
class BuyInboundService:
|
||||
@staticmethod
|
||||
def handle_inbound(data):
|
||||
"""新增入库:自动关联/创建基础信息 + 创建库存记录"""
|
||||
def search_base_material(keyword):
|
||||
"""
|
||||
搜索基础物料库
|
||||
"""
|
||||
try:
|
||||
# 0. 基础校验
|
||||
if not data.get('spec_model') or not data.get('material_name'):
|
||||
raise ValueError("缺少必要的物料名称或规格型号")
|
||||
if not keyword:
|
||||
return []
|
||||
|
||||
# 1. 关联逻辑:通过规格型号(spec_model)查找基础库
|
||||
material = MaterialBase.query.filter_by(spec_model=data['spec_model']).first()
|
||||
|
||||
# 如果不存在,则新建 MaterialBase
|
||||
if not material:
|
||||
material = MaterialBase(
|
||||
name=data['material_name'],
|
||||
spec_model=data['spec_model'],
|
||||
category=data.get('category'),
|
||||
material_type='采购件',
|
||||
unit=data.get('unit'),
|
||||
visibility_level=data.get('visibility_level', 0),
|
||||
manual_link=data.get('manual_link'),
|
||||
product_image=data.get('product_image'),
|
||||
is_enabled=True
|
||||
# 过滤条件:名称或规格包含关键词,且 is_enabled 为 True
|
||||
query = MaterialBase.query.filter(
|
||||
MaterialBase.is_enabled == True,
|
||||
or_(
|
||||
MaterialBase.name.ilike(f'%{keyword}%'),
|
||||
MaterialBase.spec_model.ilike(f'%{keyword}%')
|
||||
)
|
||||
db.session.add(material)
|
||||
db.session.flush() # 立即执行,拿到 material.id
|
||||
).limit(20)
|
||||
|
||||
# 2. 处理日期 (兼容性处理)
|
||||
in_date_val = None
|
||||
results = []
|
||||
for item in query.all():
|
||||
results.append({
|
||||
'id': item.id,
|
||||
'name': item.name,
|
||||
'spec': item.spec_model,
|
||||
'category': item.category,
|
||||
'unit': item.unit,
|
||||
'type': item.material_type,
|
||||
'status': '启用'
|
||||
})
|
||||
return results
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def handle_inbound(data):
|
||||
"""
|
||||
新增入库逻辑
|
||||
"""
|
||||
try:
|
||||
# 1. 核心校验
|
||||
base_id = data.get('base_id')
|
||||
if not base_id:
|
||||
raise ValueError("必须选择基础物料进行入库 (缺少 base_id)")
|
||||
|
||||
material = MaterialBase.query.get(base_id)
|
||||
if not material:
|
||||
raise ValueError(f"ID为 {base_id} 的基础物料不存在")
|
||||
|
||||
# 2. 处理日期 (防止空字符串报错)
|
||||
in_date_val = datetime.utcnow().date()
|
||||
if data.get('in_date'):
|
||||
try:
|
||||
in_date_val = datetime.strptime(data['in_date'], '%Y-%m-%d %H:%M:%S').date()
|
||||
date_str = str(data['in_date'])
|
||||
# 截取前10位 YYYY-MM-DD
|
||||
if len(date_str) > 10:
|
||||
in_date_val = datetime.strptime(date_str[:10], '%Y-%m-%d').date()
|
||||
else:
|
||||
in_date_val = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
except ValueError:
|
||||
try:
|
||||
in_date_val = datetime.strptime(data['in_date'], '%Y-%m-%d').date()
|
||||
except ValueError:
|
||||
in_date_val = datetime.utcnow().date()
|
||||
pass # 格式错误则使用当前日期
|
||||
|
||||
# --- 修改部分:增加字段兼容性,确保前端传参能被正确读取 ---
|
||||
in_qty = float(data.get('in_quantity') or data.get('qty_inbound') or 0)
|
||||
u_price = float(data.get('unit_price') or data.get('price_unit') or 0)
|
||||
# ---------------------------------------------------
|
||||
# 3. 数据转换
|
||||
in_qty = float(data.get('in_quantity') or 0)
|
||||
u_price = float(data.get('unit_price') or 0)
|
||||
|
||||
# 3. 创建 StockBuy
|
||||
# 4. 创建 StockBuy
|
||||
new_stock = StockBuy(
|
||||
base_id=material.id,
|
||||
sku=data.get('sku'),
|
||||
in_date=in_date_val,
|
||||
serial_number=data.get('serial_number'),
|
||||
batch_number=data.get('batch_number'),
|
||||
barcode=data.get('barcode'),
|
||||
|
||||
# --- 状态与数量强制逻辑 ---
|
||||
status='在库',
|
||||
inspection_status=data.get('inspection_status'),
|
||||
in_quantity=in_qty,
|
||||
stock_quantity=in_qty,
|
||||
available_quantity=in_qty,
|
||||
warehouse_location=data.get('warehouse_location') or data.get('warehouse_loc'),
|
||||
stock_quantity=in_qty, # 初始库存 = 入库量
|
||||
available_quantity=in_qty, # 初始可用 = 入库量
|
||||
inspection_status=data.get('inspection_status', '未检'),
|
||||
# ----------------
|
||||
|
||||
warehouse_location=data.get('warehouse_location'),
|
||||
unit_price=u_price,
|
||||
total_price=in_qty * u_price,
|
||||
currency=data.get('currency', 'CNY'),
|
||||
exchange_rate=data.get('exchange_rate', 1.0),
|
||||
supplier_name=data.get('supplier_name'),
|
||||
buyer_name=data.get('buyer_name'),
|
||||
buyer_email=data.get('buyer_email'),
|
||||
original_link=data.get('original_link'),
|
||||
|
||||
# [字段映射] 前端 -> DB
|
||||
buyer_name=data.get('purchaser'),
|
||||
buyer_email=data.get('purchaser_email'),
|
||||
original_link=data.get('source_link'),
|
||||
|
||||
detail_link=data.get('detail_link'),
|
||||
arrival_photo=data.get('arrival_photo')
|
||||
arrival_photo=data.get('arrival_photo'),
|
||||
remark=data.get('remark')
|
||||
)
|
||||
|
||||
db.session.add(new_stock)
|
||||
@ -80,105 +114,182 @@ class BuyInboundService:
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
print(f"Insert Error: {str(e)}")
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
def update_inbound(stock_id, data):
|
||||
"""更新入库:支持级联更新基础信息 + 自动重算总价"""
|
||||
"""
|
||||
更新入库逻辑
|
||||
"""
|
||||
try:
|
||||
print(f"----- UPDATE DEBUG: ID={stock_id} -----")
|
||||
|
||||
stock = StockBuy.query.get(stock_id)
|
||||
if not stock:
|
||||
raise ValueError("记录不存在")
|
||||
|
||||
# 1. 更新普通字段 (增加对 warehouse_loc 的兼容)
|
||||
if 'serial_number' in data: stock.serial_number = data['serial_number']
|
||||
if 'batch_number' in data: stock.batch_number = data['batch_number']
|
||||
if 'warehouse_location' in data: stock.warehouse_location = data['warehouse_location']
|
||||
if 'warehouse_loc' in data: stock.warehouse_location = data['warehouse_loc']
|
||||
if 'supplier_name' in data: stock.supplier_name = data['supplier_name']
|
||||
if 'status' in data: stock.status = data['status']
|
||||
if 'inspection_status' in data: stock.inspection_status = data['inspection_status']
|
||||
if 'arrival_photo' in data: stock.arrival_photo = data['arrival_photo']
|
||||
if 'remark' in data: stock.remark = data['remark']
|
||||
# 1. 字段映射字典:前端Key -> Model属性名
|
||||
# 使用映射可以避免写大量 if...else,且逻辑更清晰
|
||||
field_mapping = {
|
||||
'sku': 'sku',
|
||||
'barcode': 'barcode',
|
||||
'warehouse_location': 'warehouse_location',
|
||||
'serial_number': 'serial_number',
|
||||
'batch_number': 'batch_number',
|
||||
'status': 'status',
|
||||
'inspection_status': 'inspection_status',
|
||||
'supplier_name': 'supplier_name',
|
||||
'detail_link': 'detail_link',
|
||||
'arrival_photo': 'arrival_photo',
|
||||
'remark': 'remark',
|
||||
'currency': 'currency',
|
||||
'exchange_rate': 'exchange_rate',
|
||||
|
||||
# 2. 级联更新基础信息 (MaterialBase)
|
||||
if stock.material:
|
||||
if 'material_name' in data: stock.material.name = data['material_name']
|
||||
if 'category' in data: stock.material.category = data['category']
|
||||
if 'unit' in data: stock.material.unit = data['unit']
|
||||
# 关键映射
|
||||
'purchaser': 'buyer_name',
|
||||
'purchaser_email': 'buyer_email',
|
||||
'source_link': 'original_link'
|
||||
}
|
||||
|
||||
# 3. 核心逻辑:数量与价格联动 (增加对前端返回字段名 qty_inbound 和 price_unit 的识别)
|
||||
# 遍历更新 (排除日期、数量、单价,下面单独处理)
|
||||
for frontend_key, db_attr in field_mapping.items():
|
||||
if frontend_key in data:
|
||||
setattr(stock, db_attr, data[frontend_key])
|
||||
|
||||
# 2. 核心数值逻辑 (数量 & 单价 & 总价)
|
||||
qty_changed = False
|
||||
price_changed = False
|
||||
|
||||
# (A) 数量变更 -> 更新库存和可用量
|
||||
new_qty_input = data.get('in_quantity') or data.get('qty_inbound')
|
||||
if new_qty_input is not None:
|
||||
new_qty = float(new_qty_input)
|
||||
# (A) 处理入库数量变更
|
||||
if 'in_quantity' in data:
|
||||
new_qty = float(data['in_quantity'])
|
||||
old_qty = float(stock.in_quantity)
|
||||
diff = new_qty - old_qty
|
||||
|
||||
if diff != 0:
|
||||
if new_qty != old_qty:
|
||||
print(f"Quantity Changed: {old_qty} -> {new_qty}")
|
||||
diff = new_qty - old_qty
|
||||
stock.in_quantity = new_qty
|
||||
# 联动更新库存和可用量
|
||||
stock.stock_quantity = float(stock.stock_quantity) + diff
|
||||
stock.available_quantity = float(stock.available_quantity) + diff
|
||||
qty_changed = True
|
||||
|
||||
# (B) 单价变更
|
||||
new_price_input = data.get('unit_price') or data.get('price_unit')
|
||||
if new_price_input is not None:
|
||||
new_price = float(new_price_input)
|
||||
if new_price != float(stock.unit_price):
|
||||
# (B) 处理单价变更
|
||||
if 'unit_price' in data:
|
||||
new_price = float(data['unit_price'])
|
||||
old_price = float(stock.unit_price)
|
||||
|
||||
if new_price != old_price:
|
||||
print(f"Price Changed: {old_price} -> {new_price}")
|
||||
stock.unit_price = new_price
|
||||
price_changed = True
|
||||
|
||||
# (C) 重算总价
|
||||
# (C) 强制重算总价
|
||||
if qty_changed or price_changed:
|
||||
stock.total_price = float(stock.in_quantity) * float(stock.unit_price)
|
||||
print(f"New Total Price: {stock.total_price}")
|
||||
|
||||
db.session.commit()
|
||||
print("----- UPDATE SUCCESS -----")
|
||||
return stock
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
print(f"----- UPDATE FAILED: {str(e)} -----")
|
||||
traceback.print_exc()
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
def delete_inbound(stock_id):
|
||||
"""删除逻辑:孤儿策略(如果MaterialBase无其他引用则一并删除)"""
|
||||
try:
|
||||
stock = StockBuy.query.get(stock_id)
|
||||
if not stock:
|
||||
raise ValueError("记录不存在")
|
||||
|
||||
# 1. 记下 base_id
|
||||
material_id = stock.base_id
|
||||
|
||||
# 2. 删除库存记录
|
||||
db.session.delete(stock)
|
||||
db.session.flush()
|
||||
|
||||
# 3. 检查是否还有残留
|
||||
remaining_count = StockBuy.query.filter_by(base_id=material_id).count()
|
||||
|
||||
if remaining_count == 0:
|
||||
print(f"触发级联删除: MaterialBase ID {material_id} 已无关联,执行清理。")
|
||||
material = MaterialBase.query.get(material_id)
|
||||
if material:
|
||||
db.session.delete(material)
|
||||
|
||||
db.session.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
print(f"删除失败: {e}")
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
def get_list(page, limit):
|
||||
def get_list(page, limit, keyword=None):
|
||||
"""
|
||||
列表查询
|
||||
"""
|
||||
try:
|
||||
pagination = StockBuy.query.order_by(StockBuy.id.desc()).paginate(page=page, per_page=limit)
|
||||
items = [item.to_dict() for item in pagination.items]
|
||||
# 使用 Outer Join 确保即使 MaterialBase 物理删除,库存记录也不会报错或消失
|
||||
query = db.session.query(StockBuy).outerjoin(MaterialBase, StockBuy.base_id == MaterialBase.id)
|
||||
|
||||
# 搜索逻辑
|
||||
if keyword:
|
||||
query = query.filter(
|
||||
or_(
|
||||
MaterialBase.name.ilike(f'%{keyword}%'),
|
||||
MaterialBase.spec_model.ilike(f'%{keyword}%'),
|
||||
StockBuy.batch_number.ilike(f'%{keyword}%'),
|
||||
StockBuy.serial_number.ilike(f'%{keyword}%'),
|
||||
StockBuy.sku.ilike(f'%{keyword}%')
|
||||
)
|
||||
)
|
||||
|
||||
pagination = query.order_by(StockBuy.id.desc()).paginate(page=page, per_page=limit, error_out=False)
|
||||
|
||||
items = []
|
||||
for item in pagination.items:
|
||||
# 获取关联的基础信息,如果关联不存在则给默认值
|
||||
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 ''
|
||||
|
||||
# 构建返回字典
|
||||
d = {
|
||||
'id': item.id,
|
||||
'base_id': item.base_id,
|
||||
'material_name': mat_name,
|
||||
'spec_model': mat_spec,
|
||||
'category': mat_cat,
|
||||
'unit': mat_unit,
|
||||
'material_type': mat_type,
|
||||
|
||||
'sku': item.sku,
|
||||
'inbound_date': str(item.in_date) if item.in_date else '',
|
||||
'barcode': item.barcode,
|
||||
'serial_number': item.serial_number,
|
||||
'batch_number': item.batch_number,
|
||||
'status': item.status,
|
||||
'inspection_status': item.inspection_status,
|
||||
|
||||
'qty_inbound': float(item.in_quantity or 0),
|
||||
'qty_stock': float(item.stock_quantity or 0),
|
||||
'qty_available': float(item.available_quantity or 0),
|
||||
|
||||
'warehouse_loc': item.warehouse_location,
|
||||
'unit_price': float(item.unit_price or 0),
|
||||
'total_price': float(item.total_price or 0),
|
||||
|
||||
'currency': item.currency,
|
||||
'exchange_rate': float(item.exchange_rate or 1),
|
||||
'supplier_name': item.supplier_name,
|
||||
|
||||
# [关键映射] DB -> Frontend
|
||||
'purchaser': item.buyer_name,
|
||||
'purchaser_email': item.buyer_email,
|
||||
'source_link': item.original_link,
|
||||
|
||||
'detail_link': item.detail_link,
|
||||
'arrival_photo': item.arrival_photo,
|
||||
'remark': item.remark
|
||||
}
|
||||
items.append(d)
|
||||
|
||||
return {"total": pagination.total, "items": items}
|
||||
except Exception as e:
|
||||
print(f"查询列表失败: {e}")
|
||||
# 打印错误到 Docker 日志
|
||||
print(f"List Error: {e}")
|
||||
traceback.print_exc()
|
||||
return {"total": 0, "items": []}
|
||||
@ -1,19 +1,45 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 1. 获取列表
|
||||
export function getBuyList(params: any) {
|
||||
return request({ url: '/inbound/buy/list', method: 'get', params })
|
||||
return request({
|
||||
url: '/inbound/buy/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 2. 新增入库
|
||||
export function createBuyInbound(data: any) {
|
||||
return request({ url: '/inbound/buy/submit', method: 'post', data })
|
||||
return request({
|
||||
url: '/inbound/buy/submit',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 新增:更新接口
|
||||
// 3. 更新入库
|
||||
export function updateBuyInbound(id: number, data: any) {
|
||||
return request({ url: `/inbound/buy/${id}`, method: 'put', data })
|
||||
return request({
|
||||
url: `/inbound/buy/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 新增:删除接口
|
||||
// 4. 删除入库
|
||||
export function deleteBuyInbound(id: number) {
|
||||
return request({ url: `/inbound/buy/${id}`, method: 'delete' })
|
||||
return request({
|
||||
url: `/inbound/buy/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 5. 搜索基础物料
|
||||
export function searchMaterialBase(keyword: string) {
|
||||
return request({
|
||||
url: '/inbound/buy/search-base',
|
||||
method: 'get',
|
||||
params: { keyword }
|
||||
})
|
||||
}
|
||||
@ -1,40 +1,97 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon style="margin-right: 5px"><Plus /></el-icon>新增基础信息
|
||||
</el-button>
|
||||
<div class="filter-wrapper">
|
||||
<div class="filter-container">
|
||||
<el-input
|
||||
v-model="queryParams.keyword"
|
||||
placeholder="请输入名称或规格 (支持模糊搜索)"
|
||||
style="width: 240px; margin-right: 10px;"
|
||||
clearable
|
||||
@input="handleInputSearch"
|
||||
/>
|
||||
|
||||
<el-select
|
||||
v-model="queryParams.category"
|
||||
placeholder="类别"
|
||||
clearable
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
style="width: 140px; margin-right: 10px;"
|
||||
@change="handleQuery"
|
||||
>
|
||||
<el-option v-for="item in categoryOptions" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
|
||||
<el-select
|
||||
v-model="queryParams.type"
|
||||
placeholder="类型"
|
||||
clearable
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
style="width: 140px; margin-right: 10px;"
|
||||
@change="handleQuery"
|
||||
>
|
||||
<el-option v-for="item in typeOptions" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
|
||||
<el-select
|
||||
v-model="queryParams.isEnabled"
|
||||
placeholder="状态"
|
||||
clearable
|
||||
style="width: 100px; margin-right: 10px;"
|
||||
@change="handleQuery"
|
||||
>
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
|
||||
<el-button type="primary" plain @click="handleQuery">搜索</el-button>
|
||||
<el-button plain @click="resetQuery">重置</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="filter-container">
|
||||
<el-input
|
||||
v-model="queryParams.keyword"
|
||||
placeholder="请输入基础信息名称或规格"
|
||||
style="width: 220px; margin-right: 10px;"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
<div class="right-toolbar">
|
||||
<el-button type="primary" @click="handleAdd" style="margin-right: 10px">
|
||||
<el-icon style="margin-right: 5px"><Plus /></el-icon>新增
|
||||
</el-button>
|
||||
|
||||
<el-select v-model="queryParams.category" placeholder="基础信息类别" clearable style="width: 150px; margin-right: 10px;">
|
||||
<el-option label="采购件" value="PURCHASE" />
|
||||
<el-option label="自制件" value="SELF_MADE" />
|
||||
</el-select>
|
||||
<el-tooltip content="刷新" placement="top">
|
||||
<el-button circle :icon="Refresh" @click="getList" />
|
||||
</el-tooltip>
|
||||
|
||||
<el-select v-model="queryParams.type" placeholder="物料类型" clearable style="width: 150px; margin-right: 10px;">
|
||||
<el-option label="电子料" value="ELEC" />
|
||||
<el-option label="结构件" value="STRUCT" />
|
||||
</el-select>
|
||||
<el-dropdown trigger="click" @command="handleSizeChange">
|
||||
<el-button circle :icon="Rank" style="margin-left: 8px" title="表格密度" />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="large">宽松 (默认)</el-dropdown-item>
|
||||
<el-dropdown-item command="default">中等</el-dropdown-item>
|
||||
<el-dropdown-item command="small">紧凑</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
|
||||
<el-select v-model="queryParams.isEnabled" placeholder="状态" clearable style="width: 100px; margin-right: 10px;">
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
|
||||
<el-button type="primary" plain @click="handleQuery">搜索</el-button>
|
||||
<el-button plain @click="resetQuery">重置</el-button>
|
||||
<el-popover placement="bottom" :width="150" trigger="click">
|
||||
<template #reference>
|
||||
<el-button circle :icon="Setting" style="margin-left: 8px" title="列设置" />
|
||||
</template>
|
||||
<div class="column-setting-list">
|
||||
<div style="font-weight: bold; margin-bottom: 5px; border-bottom: 1px solid #eee; padding-bottom: 5px">
|
||||
列展示设置
|
||||
</div>
|
||||
<el-checkbox v-model="columns.id.visible" label="ID" />
|
||||
<el-checkbox v-model="columns.name.visible" label="名称" />
|
||||
<el-checkbox v-model="columns.category.visible" label="类别" />
|
||||
<el-checkbox v-model="columns.type.visible" label="类型" />
|
||||
<el-checkbox v-model="columns.spec.visible" label="规格型号" />
|
||||
<el-checkbox v-model="columns.unit.visible" label="单位" />
|
||||
<el-checkbox v-model="columns.visibilityLevel.visible" label="可见等级" />
|
||||
<el-checkbox v-model="columns.files.visible" label="资料" />
|
||||
<el-checkbox v-model="columns.isEnabled.visible" label="状态" />
|
||||
</div>
|
||||
</el-popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
@ -42,58 +99,29 @@
|
||||
:data="tableData"
|
||||
border
|
||||
stripe
|
||||
style="width: 100%; margin-top: 20px"
|
||||
:size="tableSize"
|
||||
style="width: 100%; margin-top: 15px"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
|
||||
<el-table-column prop="name" label="基础信息名称" min-width="150" show-overflow-tooltip />
|
||||
|
||||
<el-table-column prop="category" label="类别" width="100" align="center">
|
||||
<el-table-column v-if="columns.id.visible" prop="id" label="ID" min-width="80" align="center" fixed="left" />
|
||||
<el-table-column v-if="columns.name.visible" prop="name" label="基础信息名称" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column v-if="columns.category.visible" prop="category" label="类别" min-width="120" align="center" show-overflow-tooltip>
|
||||
<template #default="scope">{{ scope.row.category || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="columns.type.visible" prop="type" label="类型" min-width="120" align="center" show-overflow-tooltip>
|
||||
<template #default="scope">{{ scope.row.type || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="columns.spec.visible" prop="spec" label="规格型号" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column v-if="columns.unit.visible" prop="unit" label="单位" min-width="80" align="center" />
|
||||
<el-table-column v-if="columns.visibilityLevel.visible" prop="visibilityLevel" label="可见等级" min-width="100" align="center">
|
||||
<template #default="scope">L{{ scope.row.visibilityLevel }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="columns.files.visible" label="资料" min-width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.category === 'PURCHASE'">采购件</el-tag>
|
||||
<el-tag v-else-if="scope.row.category === 'SELF_MADE'" type="success">自制件</el-tag>
|
||||
<el-tag v-else type="info">{{ scope.row.category || '-' }}</el-tag>
|
||||
<el-button v-if="scope.row.generalImage" link type="primary" :icon="Picture" title="查看图片" @click="openLink(scope.row.generalImage)" />
|
||||
<el-button v-if="scope.row.generalManual" link type="primary" :icon="Document" title="查看说明书" @click="openLink(scope.row.generalManual)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="type" label="类型" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.type === 'ELEC'">电子料</span>
|
||||
<span v-else-if="scope.row.type === 'STRUCT'">结构件</span>
|
||||
<span v-else>{{ scope.row.type || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="spec" label="规格型号" min-width="150" show-overflow-tooltip />
|
||||
|
||||
<el-table-column prop="unit" label="单位" width="70" align="center" />
|
||||
|
||||
<el-table-column prop="visibilityLevel" label="可见等级" width="90" align="center">
|
||||
<template #default="scope">
|
||||
L{{ scope.row.visibilityLevel }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="资料" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-if="scope.row.generalImage"
|
||||
link type="primary"
|
||||
:icon="Picture"
|
||||
title="查看图片"
|
||||
@click="openLink(scope.row.generalImage)"
|
||||
/>
|
||||
<el-button
|
||||
v-if="scope.row.generalManual"
|
||||
link type="primary"
|
||||
:icon="Document"
|
||||
title="查看说明书"
|
||||
@click="openLink(scope.row.generalManual)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="isEnabled" label="是否启用" width="100" align="center">
|
||||
<el-table-column v-if="columns.isEnabled.visible" prop="isEnabled" label="是否启用" min-width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-switch
|
||||
v-model="scope.row.isEnabled"
|
||||
@ -104,8 +132,7 @@
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="150" fixed="right" align="center">
|
||||
<el-table-column label="操作" min-width="150" fixed="right" align="center">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
|
||||
@ -116,7 +143,7 @@
|
||||
<div style="margin-top: 20px; text-align: right;">
|
||||
<el-pagination
|
||||
background
|
||||
layout="total, prev, pager, next, sizes"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
v-model:current-page="queryParams.pageNum"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
@ -126,20 +153,101 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialog.visible"
|
||||
:title="dialog.title"
|
||||
width="600px"
|
||||
append-to-body
|
||||
@close="cancel"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
|
||||
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入基础信息名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="类别" prop="category">
|
||||
<el-autocomplete
|
||||
v-model="form.category"
|
||||
:fetch-suggestions="querySearchCategory"
|
||||
placeholder="可输入或选择"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="类型" prop="type">
|
||||
<el-autocomplete
|
||||
v-model="form.type"
|
||||
:fetch-suggestions="querySearchType"
|
||||
placeholder="可输入或选择"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="规格型号" prop="spec">
|
||||
<el-input v-model="form.spec" placeholder="请输入规格型号" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="计量单位" prop="unit">
|
||||
<el-input v-model="form.unit" placeholder="如: 个, 台, 米" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="可见等级" prop="visibilityLevel">
|
||||
<el-input-number v-model="form.visibilityLevel" :min="0" :max="9" label="等级" />
|
||||
<span style="margin-left: 10px; color: #999; font-size: 12px;">(0为最低,9为最高)</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="说明书链接" prop="generalManual">
|
||||
<el-input v-model="form.generalManual" placeholder="请输入说明书URL链接" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="产品图链接" prop="generalImage">
|
||||
<el-input v-model="form.generalImage" placeholder="请输入图片URL链接" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态" prop="isEnabled">
|
||||
<el-radio-group v-model="form.isEnabled">
|
||||
<el-radio :value="1">启用</el-radio>
|
||||
<el-radio :value="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
<el-button type="primary" @click="submitForm" :loading="submitLoading">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { Plus, Picture, Document } from '@element-plus/icons-vue';
|
||||
import { ref, reactive, onMounted, nextTick } from 'vue';
|
||||
import { Plus, Picture, Document, Refresh, Setting, Rank } from '@element-plus/icons-vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import type { FormInstance, FormRules } from 'element-plus';
|
||||
|
||||
// 【关键修改】引入刚才定义的 API 文件
|
||||
import {
|
||||
listMaterialBase,
|
||||
delMaterialBase,
|
||||
updateMaterialBase
|
||||
addMaterialBase,
|
||||
updateMaterialBase,
|
||||
delMaterialBase
|
||||
} from '@/api/material_base';
|
||||
|
||||
// --- 类型定义 ---
|
||||
@ -153,8 +261,8 @@ interface MaterialBaseVO {
|
||||
visibilityLevel: number;
|
||||
generalManual?: string;
|
||||
generalImage?: string;
|
||||
isEnabled: number; // 1 or 0
|
||||
statusLoading?: boolean; // 辅助字段
|
||||
isEnabled: number;
|
||||
statusLoading?: boolean;
|
||||
}
|
||||
|
||||
interface QueryParams {
|
||||
@ -170,6 +278,23 @@ interface QueryParams {
|
||||
const loading = ref(false);
|
||||
const total = ref(0);
|
||||
const tableData = ref<MaterialBaseVO[]>([]);
|
||||
const submitLoading = ref(false);
|
||||
const tableSize = ref<'large' | 'default' | 'small'>('large');
|
||||
|
||||
const columns = reactive({
|
||||
id: { visible: true },
|
||||
name: { visible: true },
|
||||
category: { visible: true },
|
||||
type: { visible: true },
|
||||
spec: { visible: true },
|
||||
unit: { visible: true },
|
||||
visibilityLevel: { visible: true },
|
||||
files: { visible: true },
|
||||
isEnabled: { visible: true }
|
||||
});
|
||||
|
||||
const categoryOptions = ref<string[]>([]);
|
||||
const typeOptions = ref<string[]>([]);
|
||||
|
||||
const queryParams = reactive<QueryParams>({
|
||||
pageNum: 1,
|
||||
@ -180,18 +305,78 @@ const queryParams = reactive<QueryParams>({
|
||||
isEnabled: undefined
|
||||
});
|
||||
|
||||
// --- 弹窗与表单相关 ---
|
||||
const dialog = reactive({
|
||||
visible: false,
|
||||
title: ''
|
||||
});
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
const initForm = {
|
||||
id: undefined,
|
||||
name: '',
|
||||
category: '',
|
||||
type: '',
|
||||
spec: '',
|
||||
unit: '',
|
||||
visibilityLevel: 0,
|
||||
generalManual: '',
|
||||
generalImage: '',
|
||||
isEnabled: 1
|
||||
};
|
||||
|
||||
const form = ref({...initForm});
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
name: [{ required: true, message: '请输入基础信息名称', trigger: 'blur' }],
|
||||
category: [{ required: true, message: '请选择或输入类别', trigger: 'change' }],
|
||||
type: [{ required: true, message: '请选择或输入类型', trigger: 'change' }],
|
||||
spec: [{ required: true, message: '请输入规格型号', trigger: 'blur' }],
|
||||
unit: [{ required: true, message: '请输入单位', trigger: 'blur' }]
|
||||
});
|
||||
|
||||
// --- 业务逻辑方法 ---
|
||||
|
||||
// 获取数据
|
||||
const extractDynamicOptions = (items: MaterialBaseVO[]) => {
|
||||
if (!items || items.length === 0) return;
|
||||
const newCategories = new Set(categoryOptions.value);
|
||||
const newTypes = new Set(typeOptions.value);
|
||||
items.forEach(item => {
|
||||
if (item.category) newCategories.add(item.category);
|
||||
if (item.type) newTypes.add(item.type);
|
||||
});
|
||||
categoryOptions.value = Array.from(newCategories);
|
||||
typeOptions.value = Array.from(newTypes);
|
||||
};
|
||||
|
||||
// 【核心新增】Autocomplete 的建议查询方法
|
||||
// 格式化数据以适配 el-autocomplete 的回调参数格式 [{ value: 'abc' }]
|
||||
const querySearchCategory = (queryString: string, cb: any) => {
|
||||
const results = queryString
|
||||
? categoryOptions.value.filter(item => item.toLowerCase().includes(queryString.toLowerCase()))
|
||||
: categoryOptions.value;
|
||||
// el-autocomplete 默认只展示 value 属性
|
||||
const formattedResults = results.map(item => ({ value: item }));
|
||||
cb(formattedResults);
|
||||
};
|
||||
|
||||
const querySearchType = (queryString: string, cb: any) => {
|
||||
const results = queryString
|
||||
? typeOptions.value.filter(item => item.toLowerCase().includes(queryString.toLowerCase()))
|
||||
: typeOptions.value;
|
||||
const formattedResults = results.map(item => ({ value: item }));
|
||||
cb(formattedResults);
|
||||
};
|
||||
|
||||
const getList = () => {
|
||||
loading.value = true;
|
||||
// 调用 API 文件中的 listMaterialBase
|
||||
listMaterialBase(queryParams)
|
||||
.then((response: any) => {
|
||||
// 我们的 request.ts 已经处理了 code!=200,这里直接拿 data
|
||||
if (response && response.data) {
|
||||
tableData.value = response.data.items;
|
||||
total.value = response.data.total;
|
||||
extractDynamicOptions(tableData.value);
|
||||
} else {
|
||||
tableData.value = [];
|
||||
total.value = 0;
|
||||
@ -206,13 +391,20 @@ const getList = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// 搜索
|
||||
let searchTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
const handleInputSearch = () => {
|
||||
if (searchTimer) clearTimeout(searchTimer);
|
||||
searchTimer = setTimeout(() => {
|
||||
queryParams.pageNum = 1;
|
||||
getList();
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNum = 1;
|
||||
getList();
|
||||
};
|
||||
|
||||
// 重置
|
||||
const resetQuery = () => {
|
||||
queryParams.keyword = '';
|
||||
queryParams.category = '';
|
||||
@ -221,75 +413,111 @@ const resetQuery = () => {
|
||||
handleQuery();
|
||||
};
|
||||
|
||||
// 新增
|
||||
const handleSizeChange = (command: 'large' | 'default' | 'small') => {
|
||||
tableSize.value = command;
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
ElMessage.info("请实现新增弹窗逻辑");
|
||||
// 逻辑:dialogVisible.value = true
|
||||
resetForm();
|
||||
dialog.title = '新增基础信息';
|
||||
dialog.visible = true;
|
||||
};
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row: MaterialBaseVO) => {
|
||||
console.log("点击编辑", row);
|
||||
ElMessage.info(`准备编辑 ID: ${row.id}`);
|
||||
// 逻辑:调用 getMaterialBase(row.id) 回显数据
|
||||
};
|
||||
|
||||
// 状态切换 (实时保存)
|
||||
const handleStatusChange = (row: MaterialBaseVO) => {
|
||||
row.statusLoading = true;
|
||||
const text = row.isEnabled === 1 ? "启用" : "停用";
|
||||
|
||||
const updateData = {
|
||||
id: row.id,
|
||||
isEnabled: row.isEnabled
|
||||
};
|
||||
|
||||
// 调用 API 文件中的 updateMaterialBase
|
||||
updateMaterialBase(updateData)
|
||||
.then(() => {
|
||||
ElMessage.success(`已${text} "${row.name}"`);
|
||||
})
|
||||
.catch(() => {
|
||||
// 失败回滚
|
||||
row.isEnabled = row.isEnabled === 1 ? 0 : 1;
|
||||
})
|
||||
.finally(() => {
|
||||
row.statusLoading = false;
|
||||
});
|
||||
};
|
||||
|
||||
// 删除
|
||||
const handleDelete = (row: MaterialBaseVO) => {
|
||||
ElMessageBox.confirm(
|
||||
`是否确认删除名称为 "${row.name}" 的数据项? \n如果该物料已有库存记录,删除将会被拒绝。`,
|
||||
"警告",
|
||||
{
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}
|
||||
).then(() => {
|
||||
// 调用 API 文件中的 delMaterialBase
|
||||
delMaterialBase(row.id)
|
||||
.then(() => {
|
||||
ElMessage.success("删除成功");
|
||||
if (tableData.value.length === 1 && queryParams.pageNum > 1) {
|
||||
queryParams.pageNum--;
|
||||
}
|
||||
getList();
|
||||
});
|
||||
}).catch(() => {
|
||||
// 取消
|
||||
resetForm();
|
||||
dialog.title = '编辑基础信息';
|
||||
dialog.visible = true;
|
||||
nextTick(() => {
|
||||
Object.assign(form.value, row);
|
||||
});
|
||||
};
|
||||
|
||||
// 打开链接
|
||||
const checkDuplicate = async (name: string, spec: string): Promise<boolean> => {
|
||||
try {
|
||||
const nameRes: any = await listMaterialBase({ pageNum: 1, pageSize: 100, keyword: name });
|
||||
if (nameRes.data?.items?.some((item: MaterialBaseVO) => item.name === name)) {
|
||||
ElMessage.error(`添加失败:已存在名称为 "${name}" 的基础信息!`);
|
||||
return true;
|
||||
}
|
||||
const specRes: any = await listMaterialBase({ pageNum: 1, pageSize: 100, keyword: spec });
|
||||
if (specRes.data?.items?.some((item: MaterialBaseVO) => item.spec === spec)) {
|
||||
ElMessage.error(`添加失败:已存在规格/编号为 "${spec}" 的基础信息!`);
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const submitForm = async () => {
|
||||
if (!formRef.value) return;
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true;
|
||||
try {
|
||||
if (!form.value.id) {
|
||||
const isDuplicate = await checkDuplicate(form.value.name, form.value.spec);
|
||||
if (isDuplicate) {
|
||||
submitLoading.value = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
const requestApi = form.value.id ? updateMaterialBase : addMaterialBase;
|
||||
const actionText = form.value.id ? '修改' : '新增';
|
||||
await requestApi(form.value);
|
||||
ElMessage.success(`${actionText}成功`);
|
||||
dialog.visible = false;
|
||||
getList();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
dialog.visible = false;
|
||||
resetForm();
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
form.value = {...initForm};
|
||||
if (formRef.value) formRef.value.resetFields();
|
||||
};
|
||||
|
||||
const handleStatusChange = (row: MaterialBaseVO) => {
|
||||
row.statusLoading = true;
|
||||
const text = row.isEnabled === 1 ? "启用" : "停用";
|
||||
const updateData = { id: row.id, isEnabled: row.isEnabled };
|
||||
updateMaterialBase(updateData)
|
||||
.then(() => ElMessage.success(`已${text} "${row.name}"`))
|
||||
.catch(() => { row.isEnabled = row.isEnabled === 1 ? 0 : 1; })
|
||||
.finally(() => { row.statusLoading = false; });
|
||||
};
|
||||
|
||||
const handleDelete = (row: MaterialBaseVO) => {
|
||||
ElMessageBox.confirm(
|
||||
`是否确认删除名称为 "${row.name}" 的数据项?`,
|
||||
"警告",
|
||||
{ confirmButtonText: "确定", cancelButtonText: "取消", type: "warning" }
|
||||
).then(() => {
|
||||
delMaterialBase(row.id).then(() => {
|
||||
ElMessage.success("删除成功");
|
||||
if (tableData.value.length === 1 && queryParams.pageNum > 1) queryParams.pageNum--;
|
||||
getList();
|
||||
});
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
const openLink = (url: string) => {
|
||||
if (!url) return;
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
@ -299,15 +527,25 @@ onMounted(() => {
|
||||
.app-container {
|
||||
padding: 20px;
|
||||
}
|
||||
.card-header {
|
||||
.filter-wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.filter-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
gap: 10px;
|
||||
}
|
||||
.right-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.column-setting-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
@ -1,22 +1,39 @@
|
||||
<template>
|
||||
<div class="buy-module">
|
||||
<div class="header-tools">
|
||||
<div class="left-actions">
|
||||
<el-button type="primary" :icon="Plus" @click="handleCreate" class="big-font-btn">采购入库登记</el-button>
|
||||
<el-button :icon="Refresh" @click="fetchData" class="big-font-btn">刷新数据</el-button>
|
||||
<div class="left-tools">
|
||||
<el-input
|
||||
v-model="queryParams.keyword"
|
||||
placeholder="搜索物料名称/规格/单号..."
|
||||
class="search-input"
|
||||
clearable
|
||||
@clear="fetchData"
|
||||
@keyup.enter="fetchData"
|
||||
>
|
||||
<template #append>
|
||||
<el-button :icon="Search" @click="fetchData" />
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<el-popover placement="bottom" title="显示列配置" :width="400" trigger="click">
|
||||
<template #reference>
|
||||
<el-button :icon="Setting" class="big-font-btn">自定义表格表头</el-button>
|
||||
</template>
|
||||
<el-checkbox-group v-model="visibleColumnProps" class="column-selector">
|
||||
<el-divider content-position="left">基础层字段</el-divider>
|
||||
<el-checkbox v-for="c in baseColumns" :key="c.prop" :label="c.prop">{{ c.label }}</el-checkbox>
|
||||
<el-divider content-position="left">库存/财务层字段</el-divider>
|
||||
<el-checkbox v-for="c in stockColumns" :key="c.prop" :label="c.prop">{{ c.label }}</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-popover>
|
||||
<div class="right-tools">
|
||||
<el-button type="primary" :icon="Plus" @click="handleCreate" class="big-font-btn">采购入库登记</el-button>
|
||||
<el-button :icon="Refresh" @click="fetchData" class="big-font-btn">刷新数据</el-button>
|
||||
|
||||
<el-popover placement="bottom-end" title="显示列配置" :width="600" trigger="click">
|
||||
<template #reference>
|
||||
<el-button :icon="Setting" class="big-font-btn">自定义表头</el-button>
|
||||
</template>
|
||||
<el-checkbox-group v-model="visibleColumnProps" class="column-selector">
|
||||
<el-row>
|
||||
<el-col :span="24"><el-divider content-position="left">基础信息 (只读)</el-divider></el-col>
|
||||
<el-col :span="8" v-for="c in baseColumns" :key="c.prop"><el-checkbox :label="c.prop">{{ c.label }}</el-checkbox></el-col>
|
||||
<el-col :span="24"><el-divider content-position="left">库存与商务</el-divider></el-col>
|
||||
<el-col :span="8" v-for="c in stockColumns" :key="c.prop"><el-checkbox :label="c.prop">{{ c.label }}</el-checkbox></el-col>
|
||||
</el-row>
|
||||
</el-checkbox-group>
|
||||
</el-popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
@ -25,46 +42,46 @@
|
||||
border
|
||||
stripe
|
||||
style="width: 100%"
|
||||
size="default"
|
||||
highlight-current-row
|
||||
class="custom-big-table"
|
||||
highlight-current-row
|
||||
>
|
||||
<template v-for="col in allColumns" :key="col.prop">
|
||||
<el-table-column
|
||||
v-if="visibleColumnProps.includes(col.prop)"
|
||||
:prop="col.prop"
|
||||
:label="col.label"
|
||||
:min-width="col.minWidth || '150'"
|
||||
:min-width="col.minWidth || '140'"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="scope" v-if="col.prop === 'serial_batch'">
|
||||
<span v-if="scope.row.serial_number" style="color: #409EFF; font-weight: bold;">
|
||||
SN: {{ scope.row.serial_number }}
|
||||
</span>
|
||||
<span v-else-if="scope.row.batch_number" style="color: #67C23A; font-weight: bold;">
|
||||
BN: {{ scope.row.batch_number }}
|
||||
</span>
|
||||
<template #default="scope" v-if="['serial_number', 'batch_number'].includes(col.prop)">
|
||||
<span v-if="scope.row[col.prop]" :class="col.prop === 'serial_number' ? 'text-sn' : 'text-bn'">
|
||||
{{ scope.row[col.prop] }}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="col.prop === 'status'">
|
||||
<el-tag size="large" :type="getStatusType(scope.row.status)" style="font-size: 18px;">
|
||||
<el-tag :type="getStatusType(scope.row.status)" effect="dark" size="large">
|
||||
{{ scope.row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="['price_unit', 'price_total'].includes(col.prop)">
|
||||
{{ formatMoney(scope.row[col.prop]) }}
|
||||
<template #default="scope" v-else-if="col.prop.includes('link')">
|
||||
<el-link v-if="scope.row[col.prop]" type="primary" :href="scope.row[col.prop]" target="_blank">查看</el-link>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="['unit_price', 'total_price'].includes(col.prop)">
|
||||
{{ formatMoney(scope.row[col.prop], scope.row.currency) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
|
||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="large" @click="handleUpdate(row)" style="font-size: 15px;">编辑</el-button>
|
||||
<el-button link type="primary" size="large" @click="handleUpdate(row)">编辑</el-button>
|
||||
<el-popconfirm title="确定删除该条入库记录吗?" @confirm="handleDelete(row)">
|
||||
<template #reference>
|
||||
<el-button link type="danger" size="large" style="font-size: 15px;">删除</el-button>
|
||||
<el-button link type="danger" size="large">删除</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
@ -84,118 +101,197 @@
|
||||
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogStatus === 'create' ? '新增采购件入库' : '编辑入库信息'"
|
||||
width="950px"
|
||||
:title="dialogStatus === 'create' ? '新增采购入库' : '编辑入库信息'"
|
||||
width="1100px"
|
||||
top="3vh"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form :model="form" label-width="140px" ref="formRef" :rules="rules" size="large">
|
||||
<el-form :model="form" label-width="120px" ref="formRef" :rules="rules" size="large" class="custom-form">
|
||||
|
||||
<el-divider content-position="left"><b>1. 基础核心层 (Material Base)</b></el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="名称" prop="material_name">
|
||||
<el-input v-model="form.material_name" :disabled="dialogStatus === 'update'" placeholder="必填" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="规格型号" prop="spec_model">
|
||||
<el-input v-model="form.spec_model" :disabled="dialogStatus === 'update'" placeholder="必填: 内部货号" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8"><el-form-item label="计量单位" prop="unit"><el-input v-model="form.unit" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8"><el-form-item label="类别" prop="category"><el-input v-model="form.category" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="类型" prop="material_type"><el-input v-model="form.material_type" disabled /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="可见等级" prop="visibility_level"><el-input-number v-model="form.visibility_level" :min="0" controls-position="right" style="width:100%" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<div class="section-block">
|
||||
<el-divider content-position="left"><el-icon><Box /></el-icon> <b>1. 基础物料信息 (必须已存在)</b></el-divider>
|
||||
|
||||
<el-divider content-position="left"><b>2. 实体库存层 (Stock Buy)</b></el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="编码/SKU" prop="sku">
|
||||
<el-input v-model="form.sku" placeholder="选填" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="入库日期" prop="in_date">
|
||||
<el-input v-model="form.in_date" disabled placeholder="系统自动生成">
|
||||
<template #suffix><el-icon><Calendar /></el-icon></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="库位" prop="warehouse_location">
|
||||
<el-input v-model="form.warehouse_location" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12" v-if="dialogStatus === 'create'">
|
||||
<el-form-item label="搜索关联" prop="base_id">
|
||||
<el-select
|
||||
v-model="form.base_id"
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
placeholder="输入名称或规格型号搜索..."
|
||||
:remote-method="handleSearchMaterial"
|
||||
:loading="searchLoading"
|
||||
style="width: 100%"
|
||||
@change="onMaterialSelected"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in materialOptions"
|
||||
:key="item.id"
|
||||
:label="item.name + ' - ' + item.spec"
|
||||
:value="item.id"
|
||||
>
|
||||
<span style="float: left; font-weight: bold;">{{ item.name }}</span>
|
||||
<span style="float: right; color: #8492a6; font-size: 13px">{{ item.spec }}</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-row :gutter="20" style="background-color: #fffbf0; border-radius: 4px; padding-top:10px;">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="序列号" prop="serial_number">
|
||||
<el-input v-model="form.serial_number" placeholder="设备SN码" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="批号" prop="batch_number">
|
||||
<el-input v-model="form.batch_number" placeholder="生产批次号" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<div style="font-size: 14px; color: #e6a23c; margin-left: 140px; margin-bottom: 10px; line-height: 1;">
|
||||
* 规则:序列号与批号互斥且必填其一 (填写一个会自动清空另一个)
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-col :span="12" v-if="dialogStatus === 'create'">
|
||||
<el-alert title="注:只能入库状态为'启用'的基础物料。" type="warning" :closable="false" show-icon />
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" style="margin-top: 10px;">
|
||||
<el-col :span="12"><el-form-item label="到检状态" prop="inspection_status"><el-input v-model="form.inspection_status" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="照片/到货图" prop="arrival_photo"><el-input v-model="form.arrival_photo" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20" class="read-only-area">
|
||||
<el-col :span="8"><el-form-item label="名称"><el-input v-model="form.material_name" disabled /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="规格型号"><el-input v-model="form.spec_model" disabled /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="单位"><el-input v-model="form.unit" disabled /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="类别"><el-input v-model="form.category" disabled /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="类型"><el-input v-model="form.material_type" disabled placeholder="自动关联" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<el-row :gutter="20" style="background-color: #f8fcfd; padding-top: 18px; border-radius: 4px; margin-top: 10px;">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="入库量" prop="in_quantity">
|
||||
<el-input-number
|
||||
v-model="form.in_quantity"
|
||||
:min="1"
|
||||
:step="1"
|
||||
:precision="0"
|
||||
style="width:100%"
|
||||
controls-position="right"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<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-col :span="8">
|
||||
<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-row>
|
||||
<div class="section-block">
|
||||
<el-divider content-position="left"><el-icon><House /></el-icon> <b>2. 库存实体信息</b></el-divider>
|
||||
|
||||
<el-divider content-position="left"><b>3. 财务与商务信息</b></el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8"><el-form-item label="单价" prop="unit_price"><el-input-number v-model="form.unit_price" :precision="4" style="width:100%" controls-position="right" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="总价" prop="total_price"><el-input-number v-model="form.total_price" :precision="4" style="width:100%" disabled :controls="false" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="供应商" prop="supplier_name"><el-input v-model="form.supplier_name" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6"><el-form-item label="编码/SKU" prop="sku"><el-input v-model="form.sku" /></el-form-item></el-col>
|
||||
|
||||
<el-col :span="6">
|
||||
<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
|
||||
placeholder="自动生成"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6"><el-form-item label="条码" prop="barcode"><el-input v-model="form.barcode" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="库位" prop="warehouse_location"><el-input v-model="form.warehouse_location" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
|
||||
<div class="sn-bn-row">
|
||||
<el-row style="margin-bottom: 15px; padding-left: 20px;">
|
||||
<el-col :span="24">
|
||||
<el-radio-group v-model="entryMode" @change="handleEntryModeChange" :disabled="modeLocked">
|
||||
<el-radio label="batch" size="large" border>按批号入库 (Batch)</el-radio>
|
||||
<el-radio label="serial" size="large" border>按序列号入库 (SN)</el-radio>
|
||||
</el-radio-group>
|
||||
<div v-if="modeLocked" class="locked-tip">
|
||||
<el-icon style="vertical-align: middle"><Lock /></el-icon>
|
||||
该物料已有入库记录,系统已自动匹配历史入库方式(同物料同策略),不可手动更改。
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="10">
|
||||
<el-form-item label="批号" prop="batch_number">
|
||||
<el-input
|
||||
v-model="form.batch_number"
|
||||
placeholder="系统自动生成"
|
||||
:disabled="entryMode === 'serial'"
|
||||
clearable
|
||||
>
|
||||
<template #prefix><span style="color:#67C23A; font-weight:bold">BN</span></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="10">
|
||||
<el-form-item label="序列号" prop="serial_number">
|
||||
<el-input
|
||||
v-model="form.serial_number"
|
||||
placeholder="请扫描或输入设备SN"
|
||||
:disabled="entryMode === 'batch'"
|
||||
clearable
|
||||
>
|
||||
<template #prefix><span style="color:#409EFF; font-weight:bold">SN</span></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24" class="tip-col">
|
||||
<span class="small-tip" v-if="entryMode === 'batch'">
|
||||
* <b>批号模式</b>:{{ modeLocked ? '根据历史记录' : '首次入库' }},系统已自动生成新批号。
|
||||
</span>
|
||||
<span class="small-tip" v-else>
|
||||
* <b>序列号模式</b>:{{ modeLocked ? '根据历史记录' : '首次入库' }},请手动录入唯一SN码。
|
||||
</span>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<el-row :gutter="20" style="margin-top: 15px;">
|
||||
<el-col :span="6">
|
||||
<el-form-item label="入库量" prop="in_quantity">
|
||||
<el-input-number v-model="form.in_quantity" :min="1" style="width:100%" controls-position="right" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<template v-if="dialogStatus === 'update'">
|
||||
<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-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-form-item label="库存状态" prop="status">
|
||||
<el-select v-model="form.status" style="width:100%">
|
||||
<el-option label="在库" value="在库" />
|
||||
<el-option label="出库" value="出库" />
|
||||
<el-option label="损耗" value="损耗" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</template>
|
||||
|
||||
<el-col :span="6">
|
||||
<el-form-item label="到检状态" prop="inspection_status">
|
||||
<el-select v-model="form.inspection_status" style="width:100%">
|
||||
<el-option label="未检" value="未检" />
|
||||
<el-option label="合格" value="合格" />
|
||||
<el-option label="不合格" value="不合格" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12"><el-form-item label="到货图" prop="arrival_photo"><el-input v-model="form.arrival_photo" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<div class="section-block">
|
||||
<el-divider content-position="left"><el-icon><Money /></el-icon> <b>3. 商务信息</b></el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6"><el-form-item label="币种"><el-input v-model="form.currency" /></el-form-item></el-col>
|
||||
<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-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="4" disabled :controls="false" style="width:100%"/></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8"><el-form-item label="供应商"><el-input v-model="form.supplier_name" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="采购人"><el-input v-model="form.purchaser" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="邮箱"><el-input v-model="form.purchaser_email" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12"><el-form-item label="原始链接"><el-input v-model="form.source_link" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="详情链接"><el-input v-model="form.detail_link" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false" style="font-size: 15px;">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitForm" style="font-size: 15px;">
|
||||
<el-button @click="visible = false" class="big-font-btn">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitForm" class="big-font-btn">
|
||||
{{ dialogStatus === 'create' ? '确认入库' : '保存修改' }}
|
||||
</el-button>
|
||||
</template>
|
||||
@ -205,106 +301,218 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import { Plus, Setting, Refresh, Calendar } from '@element-plus/icons-vue'
|
||||
import { Plus, Setting, Refresh, Search, Lock, Box, House, Money } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import dayjs from 'dayjs'
|
||||
import { getBuyList, createBuyInbound, updateBuyInbound, deleteBuyInbound } from '@/api/inbound/buy'
|
||||
import {
|
||||
getBuyList,
|
||||
createBuyInbound,
|
||||
updateBuyInbound,
|
||||
deleteBuyInbound,
|
||||
searchMaterialBase
|
||||
} from '@/api/inbound/buy'
|
||||
|
||||
// 状态控制
|
||||
// ------------------------------------
|
||||
// 状态与变量
|
||||
// ------------------------------------
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const visible = ref(false)
|
||||
const searchLoading = ref(false)
|
||||
const dialogStatus = ref<'create' | 'update'>('create')
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const formRef = ref()
|
||||
const queryParams = reactive({ page: 1, pageSize: 15 })
|
||||
const queryParams = reactive({ page: 1, pageSize: 15, keyword: '' })
|
||||
const materialOptions = ref<any[]>([])
|
||||
|
||||
// --- 1. 列定义 ---
|
||||
// 新增:入库模式控制
|
||||
const entryMode = ref('batch')
|
||||
const modeLocked = ref(false)
|
||||
|
||||
// 列定义 (保持不变)
|
||||
const baseColumns = [
|
||||
{ prop: 'material_name', label: '物料名称', minWidth: '200' },
|
||||
{ prop: 'category', label: '类别', minWidth: '120' },
|
||||
{ prop: 'spec_model', label: '规格型号', minWidth: '200' },
|
||||
{ prop: 'unit', label: '单位', minWidth: '100' }
|
||||
{ prop: 'material_name', label: '名称' },
|
||||
{ prop: 'category', label: '类别' },
|
||||
{ prop: 'material_type', label: '类型' },
|
||||
{ prop: 'spec_model', label: '规格型号' },
|
||||
{ prop: 'unit', label: '单位' },
|
||||
]
|
||||
|
||||
const stockColumns = [
|
||||
{ prop: 'sku', label: '编码/SKU', minWidth: '180' },
|
||||
{ prop: 'inbound_date', label: '入库日期', minWidth: '160' },
|
||||
{ prop: 'serial_batch', label: '序列号/批号', minWidth: '220' },
|
||||
{ prop: 'qty_stock', label: '库存数', minWidth: '120' },
|
||||
{ prop: 'qty_available', label: '可用数', minWidth: '120' },
|
||||
{ prop: 'qty_inbound', label: '入库量', minWidth: '120' },
|
||||
{ prop: 'price_unit', label: '单价', minWidth: '150' },
|
||||
{ prop: 'price_total', label: '总价', minWidth: '150' },
|
||||
{ prop: 'status', label: '状态', minWidth: '120' },
|
||||
{ prop: 'warehouse_loc', label: '库位', minWidth: '150' },
|
||||
{ prop: 'supplier_name', label: '供应商', minWidth: '200' }
|
||||
{ prop: 'id', label: 'ID', minWidth: '60' },
|
||||
{ prop: 'base_id', label: 'BaseID', minWidth: '80' },
|
||||
{ prop: 'sku', label: 'SKU', minWidth: '120' },
|
||||
{ prop: 'inbound_date', label: '入库日期', minWidth: '120' },
|
||||
{ prop: 'barcode', label: '条码', minWidth: '120' },
|
||||
{ prop: 'serial_number', label: '序列号', minWidth: '150' },
|
||||
{ prop: 'batch_number', label: '批号', minWidth: '150' },
|
||||
{ prop: 'status', label: '状态', minWidth: '100' },
|
||||
{ prop: 'inspection_status', label: '到检', minWidth: '100' },
|
||||
{ prop: 'qty_inbound', label: '入库量', minWidth: '100' },
|
||||
{ prop: 'qty_stock', label: '库存数', minWidth: '100' },
|
||||
{ prop: 'qty_available', label: '可用数', minWidth: '100' },
|
||||
{ prop: 'warehouse_loc', label: '库位', minWidth: '120' },
|
||||
{ prop: 'unit_price', label: '单价', minWidth: '120' },
|
||||
{ prop: 'total_price', label: '总价', minWidth: '120' },
|
||||
{ prop: 'currency', label: '币种', minWidth: '80' },
|
||||
{ prop: 'exchange_rate', label: '汇率', minWidth: '80' },
|
||||
{ prop: 'supplier_name', label: '供应商', minWidth: '150' },
|
||||
{ prop: 'purchaser', label: '采购人', minWidth: '100' },
|
||||
{ prop: 'purchaser_email', label: '邮箱', minWidth: '150' },
|
||||
{ prop: 'source_link', label: '采购链接', minWidth: '100' },
|
||||
{ prop: 'detail_link', label: '详情链接', minWidth: '100' },
|
||||
{ prop: 'arrival_photo', label: '到货图', minWidth: '100' }
|
||||
]
|
||||
|
||||
const allColumns = [...baseColumns, ...stockColumns]
|
||||
|
||||
// --- 2. 默认展示列 ---
|
||||
const visibleColumnProps = ref([
|
||||
'material_name', 'spec_model', 'inbound_date',
|
||||
'serial_batch', 'qty_stock', 'qty_available', 'status'
|
||||
'material_name', 'category', 'material_type', 'spec_model', 'unit',
|
||||
'inbound_date', 'serial_number', 'batch_number', 'status', 'inspection_status',
|
||||
'unit_price', 'total_price', 'supplier_name', 'purchaser'
|
||||
])
|
||||
|
||||
// --- 3. 表单对象 ---
|
||||
const form = reactive({
|
||||
id: undefined,
|
||||
material_name: '', category: '', spec_model: '', unit: '个',
|
||||
material_type: '采购件', visibility_level: 0,
|
||||
sku: '', in_date: '',
|
||||
base_id: undefined as number | 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,
|
||||
supplier_name: '', arrival_photo: '', remark: ''
|
||||
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: ''
|
||||
})
|
||||
|
||||
// --- 4. 校验逻辑 ---
|
||||
const validateIdentity = (rule: any, value: any, callback: any) => {
|
||||
if (!form.serial_number && !form.batch_number) {
|
||||
callback(new Error('序列号和批号至少填写一项'))
|
||||
// ------------------------------------
|
||||
// 逻辑校验规则
|
||||
// ------------------------------------
|
||||
const validateUnique = (rule: any, value: string, callback: any) => {
|
||||
if (!value) return callback()
|
||||
const isDuplicate = tableData.value.some((row: any) => {
|
||||
if (dialogStatus.value === 'update' && row.id === form.id) return false
|
||||
if (rule.field === 'serial_number' && row.serial_number === value) return true
|
||||
if (rule.field === 'batch_number' && row.batch_number === value) return true
|
||||
return false
|
||||
})
|
||||
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('序列号模式下必填'))
|
||||
} else if (entryMode.value === 'batch' && !form.batch_number && rule.field === 'batch_number') {
|
||||
callback(new Error('批号模式下必填'))
|
||||
} else {
|
||||
if (formRef.value) {
|
||||
if (rule.field === 'serial_number' && form.batch_number) formRef.value.clearValidate('batch_number')
|
||||
if (rule.field === 'batch_number' && form.serial_number) formRef.value.clearValidate('serial_number')
|
||||
}
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const rules = {
|
||||
material_name: [{ required: true, message: '必填', trigger: 'blur' }],
|
||||
spec_model: [{ required: true, message: '必填', trigger: 'blur' }],
|
||||
in_quantity: [{ required: true, message: '必填', trigger: 'blur' }],
|
||||
serial_number: [{ validator: validateIdentity, trigger: 'blur' }],
|
||||
batch_number: [{ validator: validateIdentity, trigger: 'blur' }]
|
||||
base_id: [{ required: true, message: '请搜索并选择基础物料', trigger: 'change' }],
|
||||
in_quantity: [{ required: true, message: '请输入入库数量', trigger: 'blur' }],
|
||||
in_date: [{ required: true, message: '请选择日期', trigger: 'change' }],
|
||||
serial_number: [{ validator: validateIdentity, trigger: 'blur' }, { validator: validateUnique, trigger: 'blur' }],
|
||||
batch_number: [{ validator: validateIdentity, trigger: 'blur' }, { validator: validateUnique, trigger: 'blur' }]
|
||||
}
|
||||
|
||||
// --- 5. 监听逻辑 ---
|
||||
watch(() => form.serial_number, (val) => { if (val && form.batch_number) form.batch_number = '' })
|
||||
watch(() => form.batch_number, (val) => { if (val && form.serial_number) form.serial_number = '' })
|
||||
// ------------------------------------
|
||||
// 核心逻辑函数
|
||||
// ------------------------------------
|
||||
|
||||
watch(() => form.in_quantity, (newVal) => {
|
||||
if (newVal !== undefined) {
|
||||
if (dialogStatus.value === 'create') {
|
||||
form.stock_quantity = newVal
|
||||
form.available_quantity = newVal
|
||||
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)
|
||||
|
||||
if (historyItems.length > 0) {
|
||||
modeLocked.value = true
|
||||
const latest = historyItems.sort((a: any, b: any) => b.id - a.id)[0]
|
||||
|
||||
if (latest.serial_number) {
|
||||
entryMode.value = 'serial'
|
||||
form.serial_number = ''
|
||||
form.batch_number = ''
|
||||
} else {
|
||||
entryMode.value = 'batch'
|
||||
form.serial_number = ''
|
||||
const lastBatch = latest.batch_number || '000000'
|
||||
form.batch_number = incrementBatchNumber(lastBatch)
|
||||
}
|
||||
} else {
|
||||
modeLocked.value = false
|
||||
entryMode.value = 'batch'
|
||||
form.batch_number = '000001'
|
||||
}
|
||||
form.total_price = Number((newVal * form.unit_price).toFixed(4))
|
||||
|
||||
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'
|
||||
}
|
||||
}
|
||||
|
||||
const incrementBatchNumber = (batchStr: string) => {
|
||||
if (!batchStr || !/^\d+$/.test(batchStr)) return '000001'
|
||||
const num = parseInt(batchStr, 10)
|
||||
return (num + 1).toString().padStart(6, '0')
|
||||
}
|
||||
|
||||
const handleEntryModeChange = (val: string) => {
|
||||
if (val === 'batch') {
|
||||
form.serial_number = ''
|
||||
form.batch_number = '000001'
|
||||
if(formRef.value) formRef.value.clearValidate('serial_number')
|
||||
} else {
|
||||
form.batch_number = ''
|
||||
if(formRef.value) formRef.value.clearValidate('batch_number')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearchMaterial = async (query: string) => {
|
||||
if (query) {
|
||||
searchLoading.value = true
|
||||
try {
|
||||
const res: any = await searchMaterialBase(query)
|
||||
materialOptions.value = res.data || []
|
||||
} finally {
|
||||
searchLoading.value = false
|
||||
}
|
||||
} else {
|
||||
materialOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const onMaterialSelected = (val: number) => {
|
||||
const item = materialOptions.value.find(i => i.id === val)
|
||||
if (item) {
|
||||
form.material_name = item.name
|
||||
form.spec_model = item.spec
|
||||
form.category = item.category
|
||||
form.unit = item.unit
|
||||
form.material_type = item.type
|
||||
checkHistoryAndSetMode(item.id)
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => [form.in_quantity, form.unit_price], () => {
|
||||
form.total_price = Number((form.in_quantity * form.unit_price).toFixed(4))
|
||||
})
|
||||
|
||||
watch(() => form.unit_price, (newVal) => {
|
||||
if (newVal !== undefined) {
|
||||
form.total_price = Number((newVal * (form.in_quantity || 0)).toFixed(4))
|
||||
}
|
||||
})
|
||||
|
||||
// --- 6. 核心操作 ---
|
||||
// CRUD 操作
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
@ -317,44 +525,67 @@ const fetchData = async () => {
|
||||
const handleCreate = () => {
|
||||
dialogStatus.value = 'create'
|
||||
resetForm()
|
||||
form.in_date = dayjs().format('YYYY-MM-DD HH:mm:ss')
|
||||
// 新增时自动设置当前时间,不可修改
|
||||
form.in_date = dayjs().format('YYYY-MM-DD')
|
||||
|
||||
modeLocked.value = false
|
||||
entryMode.value = 'batch'
|
||||
form.batch_number = ''
|
||||
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
const handleUpdate = (row: any) => {
|
||||
dialogStatus.value = 'update'
|
||||
resetForm()
|
||||
|
||||
modeLocked.value = true
|
||||
|
||||
form.id = row.id
|
||||
form.base_id = row.base_id
|
||||
form.material_name = row.material_name
|
||||
form.spec_model = row.spec_model
|
||||
form.category = row.category
|
||||
form.unit = row.unit
|
||||
form.material_type = row.material_type
|
||||
|
||||
form.sku = row.sku
|
||||
form.barcode = row.barcode
|
||||
// 编辑时回显原有时间,disabled 属性确保不可修改
|
||||
form.in_date = row.inbound_date
|
||||
form.warehouse_location = row.warehouse_loc
|
||||
|
||||
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 = ''
|
||||
}
|
||||
|
||||
form.status = row.status
|
||||
form.inspection_status = row.inspection_status
|
||||
form.in_quantity = Number(row.qty_inbound) || 0
|
||||
form.stock_quantity = Number(row.qty_stock) || 0
|
||||
form.available_quantity = Number(row.qty_available) || 0
|
||||
form.unit_price = Number(row.price_unit) || 0
|
||||
form.total_price = Number(row.price_total) || 0
|
||||
form.warehouse_location = row.warehouse_loc || ''
|
||||
form.serial_number = row.serial_number || ''
|
||||
form.batch_number = row.batch_number || ''
|
||||
form.supplier_name = row.supplier_name || ''
|
||||
form.status = row.status || '在库'
|
||||
form.remark = row.remark || ''
|
||||
|
||||
form.unit_price = Number(row.unit_price) || 0
|
||||
form.total_price = Number(row.total_price) || 0
|
||||
form.currency = row.currency
|
||||
form.exchange_rate = Number(row.exchange_rate)
|
||||
form.supplier_name = row.supplier_name
|
||||
form.purchaser = row.purchaser
|
||||
form.purchaser_email = row.purchaser_email
|
||||
form.source_link = row.source_link
|
||||
form.detail_link = row.detail_link
|
||||
form.arrival_photo = row.arrival_photo
|
||||
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await deleteBuyInbound(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} catch (e: any) {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 核心修复:修复编辑不生效的问题
|
||||
const submitForm = async () => {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid: boolean) => {
|
||||
@ -365,57 +596,103 @@ const submitForm = async () => {
|
||||
await createBuyInbound(form)
|
||||
ElMessage.success('入库成功')
|
||||
} else {
|
||||
await updateBuyInbound(form.id!, form)
|
||||
// 确保 ID 和数值类型正确传递
|
||||
const payload = {
|
||||
...form,
|
||||
in_quantity: Number(form.in_quantity),
|
||||
unit_price: Number(form.unit_price)
|
||||
}
|
||||
// 1. 等待更新完成
|
||||
await updateBuyInbound(form.id!, payload)
|
||||
ElMessage.success('更新成功')
|
||||
}
|
||||
|
||||
// 2. 更新成功后,先刷新数据
|
||||
await fetchData()
|
||||
|
||||
// 3. 数据刷新完毕,再关闭弹窗
|
||||
visible.value = false
|
||||
fetchData()
|
||||
|
||||
} catch (e: any) {
|
||||
ElMessage.error('提交失败')
|
||||
} finally { submitting.value = false }
|
||||
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 resetForm = () => {
|
||||
materialOptions.value = []
|
||||
Object.assign(form, {
|
||||
id: undefined,
|
||||
material_name: '', category: '', spec_model: '', unit: '个',
|
||||
material_type: '采购件', visibility_level: 0,
|
||||
sku: '', in_date: '',
|
||||
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,
|
||||
supplier_name: '', arrival_photo: '', remark: ''
|
||||
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: ''
|
||||
})
|
||||
}
|
||||
|
||||
const getStatusType = (status: string) => {
|
||||
return status === '在库' ? 'success' : 'info'
|
||||
const map: any = { '在库': 'success', '出库': 'info', '损耗': 'danger' }
|
||||
return map[status] || 'warning'
|
||||
}
|
||||
|
||||
const formatMoney = (val: any) => {
|
||||
const formatMoney = (val: any, currency = '¥') => {
|
||||
const num = Number(val)
|
||||
return isNaN(num) ? '-' : `¥ ${num.toFixed(2)}`
|
||||
return isNaN(num) ? '-' : `${currency} ${num.toFixed(2)}`
|
||||
}
|
||||
|
||||
onMounted(() => fetchData())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.buy-module { background: #fff; border-radius: 8px; padding: 15px; }
|
||||
.header-tools { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; }
|
||||
.column-selector { display: flex; flex-direction: column; padding: 10px; max-height: 400px; overflow-y: auto; }
|
||||
.pagination-container { margin-top: 15px; display: flex; justify-content: flex-end; }
|
||||
:deep(.el-divider--horizontal) { margin: 15px 0 15px 0; }
|
||||
.buy-module { background: #fff; padding: 15px; }
|
||||
|
||||
.custom-big-table { font-size: 15px !important; }
|
||||
:deep(.el-table .el-table__cell) { padding: 12px 0; }
|
||||
:deep(.el-table th .cell) { font-size: 22px; font-weight: bold; }
|
||||
:deep(.el-table td .cell) { line-height: 1.5; font-size: 15px; }
|
||||
.big-font-btn { font-size: 15px !important; padding: 15px 25px !important; }
|
||||
:deep(.el-form-item__label) { font-size: 15px !important; }
|
||||
:deep(.el-input__inner) { font-size: 15px !important; height: 45px; }
|
||||
:deep(.el-divider__text) { font-size: 15px !important; }
|
||||
/* 头部布局优化:左搜右操作 */
|
||||
.header-tools { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; }
|
||||
.left-tools { flex: 0 0 300px; /* 左侧固定宽度或自适应 */ }
|
||||
.right-tools { display: flex; gap: 10px; align-items: center; }
|
||||
|
||||
.search-input { width: 100%; }
|
||||
|
||||
.custom-big-table { font-size: 14px; }
|
||||
.section-block { margin-bottom: 20px; border: 1px solid #ebeef5; padding: 15px; border-radius: 4px; }
|
||||
.read-only-area { background-color: #f5f7fa; padding: 10px; border-radius: 4px; }
|
||||
|
||||
.sn-bn-row {
|
||||
background-color: #fffbf0;
|
||||
border: 1px dashed #e6a23c;
|
||||
padding: 15px 0 5px 0;
|
||||
border-radius: 4px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.text-sn { color: #409EFF; font-weight: bold; }
|
||||
.text-bn { color: #67C23A; font-weight: bold; }
|
||||
.small-tip { font-size: 12px; color: #909399; margin-left: 10px; }
|
||||
.tip-col { padding-left: 20px; margin-bottom: 5px; margin-top: -10px;}
|
||||
.locked-tip {
|
||||
font-size: 12px;
|
||||
color: #E6A23C;
|
||||
margin-top: 5px;
|
||||
background-color: #fdf6ec;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user