针对于采购页面进行优化逻辑

This commit is contained in:
dxc
2026-01-28 11:22:08 +08:00
parent 87864a1c4f
commit 6f4917f57e
6 changed files with 1325 additions and 534 deletions

View File

@ -2,49 +2,59 @@ from flask import Blueprint, request, jsonify
from app.services.inbound.buy_service import BuyInboundService from app.services.inbound.buy_service import BuyInboundService
import traceback import traceback
# 定义蓝图
inbound_buy_bp = Blueprint('inbound_buy', __name__) 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']) @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) result = BuyInboundService.get_list(page, limit)
return jsonify({ return jsonify({"code": 200, "msg": "success", "data": result})
"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. 新增入库 (POST) # 2. 新增入库
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@inbound_buy_bp.route('/submit', methods=['POST']) @inbound_buy_bp.route('/submit', methods=['POST'])
def submit(): def submit():
try: try:
data = request.get_json() data = request.get_json()
if not data: if not data:
return jsonify({"code": 400, "msg": "No data provided"}), 400 return jsonify({"code": 400, "msg": "No data"}), 400
BuyInboundService.handle_inbound(data) BuyInboundService.handle_inbound(data)
return jsonify({"code": 200, "msg": "入库成功"}) return jsonify({"code": 200, "msg": "入库成功"})
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
return jsonify({"code": 500, "msg": str(e)}), 500 return jsonify({"code": 500, "msg": str(e)}), 500
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# 3. 更新入库 (PUT) # 3. 更新入库
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@inbound_buy_bp.route('/<int:id>', methods=['PUT']) @inbound_buy_bp.route('/<int:id>', methods=['PUT'])
def update_buy(id): def update_buy(id):
@ -53,12 +63,10 @@ def update_buy(id):
BuyInboundService.update_inbound(id, data) BuyInboundService.update_inbound(id, data)
return jsonify({"code": 200, "msg": "更新成功"}) return jsonify({"code": 200, "msg": "更新成功"})
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
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# 4. 删除入库 (DELETE) # 4. 删除
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@inbound_buy_bp.route('/<int:id>', methods=['DELETE']) @inbound_buy_bp.route('/<int:id>', methods=['DELETE'])
def delete_buy(id): def delete_buy(id):
@ -66,5 +74,4 @@ def delete_buy(id):
BuyInboundService.delete_inbound(id) BuyInboundService.delete_inbound(id)
return jsonify({"code": 200, "msg": "删除成功"}) return jsonify({"code": 200, "msg": "删除成功"})
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

View File

@ -1,69 +1,112 @@
#stock.py # app/models/stock.py
from app.extensions import db from app.extensions import db
from datetime import datetime from datetime import datetime
class StockBuy(db.Model): class StockBuy(db.Model):
"""
采购入库库存表
对应数据库表: stock_buy
"""
__tablename__ = 'stock_buy' __tablename__ = 'stock_buy'
# 主键
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
# 【核心关联】 # 【核心关联】外键关联 material_base 表
# 这里明确指定了 base_id 是外键,关联 material_base 表的 id
base_id = db.Column(db.Integer, db.ForeignKey('material_base.id'), nullable=False) base_id = db.Column(db.Integer, db.ForeignKey('material_base.id'), nullable=False)
# --- 身份标识 ---
sku = db.Column(db.String(100)) sku = db.Column(db.String(100))
in_date = db.Column(db.Date) in_date = db.Column(db.Date)
serial_number = db.Column(db.String(100)) barcode = db.Column(db.String(100)) # 条码
batch_number = 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) in_quantity = db.Column(db.Numeric(19, 4), default=0)
stock_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) available_quantity = db.Column(db.Numeric(19, 4), default=0)
# 状态与位置 # --- 状态与位置 ---
status = db.Column(db.String(50)) status = db.Column(db.String(50)) # 在库/出库/损耗
inspection_status = db.Column(db.String(50)) inspection_status = db.Column(db.String(50)) # 未检/合格/不合格
warehouse_location = db.Column(db.String(100)) warehouse_location = db.Column(db.String(100))
# 财务与商务 # --- 财务与商务 ---
unit_price = db.Column(db.Numeric(19, 4), default=0) unit_price = db.Column(db.Numeric(19, 4), default=0)
total_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') currency = db.Column(db.String(20), default='CNY')
exchange_rate = db.Column(db.Numeric(15, 6), default=1.0) exchange_rate = db.Column(db.Numeric(15, 6), default=1.0)
supplier_name = db.Column(db.String(255)) supplier_name = db.Column(db.String(255))
# [关键映射区]Python属性名 = DB列名
# 前端传 purchaser -> 存入 buyer_name
buyer_name = db.Column(db.String(100)) buyer_name = db.Column(db.String(100))
# 前端传 purchaser_email -> 存入 buyer_email
buyer_email = db.Column(db.String(100)) buyer_email = db.Column(db.String(100))
# 前端传 source_link -> 存入 original_link
original_link = db.Column(db.Text) original_link = db.Column(db.Text)
detail_link = db.Column(db.Text) detail_link = db.Column(db.Text)
arrival_photo = db.Column(db.Text) arrival_photo = db.Column(db.Text)
# 【核心关联】 # [这就是报错缺失的字段],请确保执行了 ALTER TABLE
# 建立对象级别的连接,方便通过 stock.material 访问基础信息 remark = db.Column(db.Text)
# 【关系定义】
# 建立与 MaterialBase 的关系,方便通过 stock.material 访问基础信息
material = db.relationship('MaterialBase', back_populates='stock_buys') material = db.relationship('MaterialBase', back_populates='stock_buys')
def to_dict(self): def to_dict(self):
"""序列化""" """
序列化:将模型转换为字典,主要用于单条查询或内部调用
列表查询主要依赖 Service 层的手动构建以提高性能
"""
return { return {
'id': self.id, 'id': self.id,
'base_id': self.base_id, # 前端需要这个ID来判断关联 'base_id': self.base_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,
# 级联基础信息 (防止 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, '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, 'serial_number': self.serial_number,
'batch_number': self.batch_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, 'warehouse_loc': self.warehouse_location,
'status': self.status, 'status': self.status,
'price_unit': float(self.unit_price) if self.unit_price else 0, 'inspection_status': self.inspection_status,
'price_total': float(self.total_price) if self.total_price else 0, 'remark': self.remark,
'supplier_name': self.supplier_name
# 数量 (转为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
} }

View File

@ -2,76 +2,88 @@ from app.extensions import db
from app.models.material import MaterialBase from app.models.material import MaterialBase
from app.models.stock import StockBuy from app.models.stock import StockBuy
from datetime import datetime from datetime import datetime
from sqlalchemy import or_, func # 引入 func 用于聚合计算
import traceback import traceback
class BuyInboundService: class BuyInboundService:
@staticmethod @staticmethod
def handle_inbound(data): def search_base_material(keyword):
"""新增入库:自动关联/创建基础信息 + 创建库存记录"""
try: try:
# 0. 基础校验 if not keyword:
if not data.get('spec_model') or not data.get('material_name'): return []
raise ValueError("缺少必要的物料名称或规格型号") query = MaterialBase.query.filter(
MaterialBase.is_enabled == True,
# 1. 关联逻辑:通过规格型号(spec_model)查找基础库 or_(
material = MaterialBase.query.filter_by(spec_model=data['spec_model']).first() MaterialBase.name.ilike(f'%{keyword}%'),
MaterialBase.spec_model.ilike(f'%{keyword}%')
# 如果不存在,则新建 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
) )
db.session.add(material) ).limit(20)
db.session.flush() # 立即执行,拿到 material.id
# 2. 处理日期 (兼容性处理) results = []
in_date_val = None 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:
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} 的基础物料不存在")
in_date_val = datetime.utcnow().date()
if data.get('in_date'): if data.get('in_date'):
try: try:
in_date_val = datetime.strptime(data['in_date'], '%Y-%m-%d %H:%M:%S').date() if len(str(data['in_date'])) > 10:
in_date_val = datetime.strptime(str(data['in_date'])[:10], '%Y-%m-%d').date()
else:
in_date_val = datetime.strptime(str(data['in_date']), '%Y-%m-%d').date()
except ValueError: except ValueError:
try: pass
in_date_val = datetime.strptime(data['in_date'], '%Y-%m-%d').date()
except ValueError:
in_date_val = datetime.utcnow().date()
# --- 修改部分:增加字段兼容性,确保前端传参能被正确读取 --- in_qty = float(data.get('in_quantity') or 0)
in_qty = float(data.get('in_quantity') or data.get('qty_inbound') or 0) u_price = float(data.get('unit_price') or 0)
u_price = float(data.get('unit_price') or data.get('price_unit') or 0)
# ---------------------------------------------------
# 3. 创建 StockBuy
new_stock = StockBuy( new_stock = StockBuy(
base_id=material.id, base_id=material.id,
sku=data.get('sku'), sku=data.get('sku'),
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'),
barcode=data.get('barcode'),
status='在库', status='在库',
inspection_status=data.get('inspection_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,
warehouse_location=data.get('warehouse_location') or data.get('warehouse_loc'), inspection_status=data.get('inspection_status', '未检'),
warehouse_location=data.get('warehouse_location'),
unit_price=u_price, unit_price=u_price,
total_price=in_qty * u_price, total_price=in_qty * u_price,
currency=data.get('currency', 'CNY'), currency=data.get('currency', 'CNY'),
exchange_rate=data.get('exchange_rate', 1.0), exchange_rate=data.get('exchange_rate', 1.0),
supplier_name=data.get('supplier_name'), supplier_name=data.get('supplier_name'),
buyer_name=data.get('buyer_name'), buyer_name=data.get('purchaser'),
buyer_email=data.get('buyer_email'), buyer_email=data.get('purchaser_email'),
original_link=data.get('original_link'), original_link=data.get('source_link'),
detail_link=data.get('detail_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) db.session.add(new_stock)
@ -84,101 +96,180 @@ 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} -----")
print(f"Payload: {data}")
stock = StockBuy.query.get(stock_id) stock = StockBuy.query.get(stock_id)
if not stock: if not stock:
raise ValueError("记录不存在") raise ValueError("记录不存在")
# 1. 更新普通字段 (增加对 warehouse_loc 的兼容) field_mapping = {
if 'serial_number' in data: stock.serial_number = data['serial_number'] 'sku': 'sku',
if 'batch_number' in data: stock.batch_number = data['batch_number'] 'barcode': 'barcode',
if 'warehouse_location' in data: stock.warehouse_location = data['warehouse_location'] 'warehouse_location': 'warehouse_location',
if 'warehouse_loc' in data: stock.warehouse_location = data['warehouse_loc'] 'serial_number': 'serial_number',
if 'supplier_name' in data: stock.supplier_name = data['supplier_name'] 'batch_number': 'batch_number',
if 'status' in data: stock.status = data['status'] 'status': 'status',
if 'inspection_status' in data: stock.inspection_status = data['inspection_status'] 'inspection_status': 'inspection_status',
if 'arrival_photo' in data: stock.arrival_photo = data['arrival_photo'] 'supplier_name': 'supplier_name',
if 'remark' in data: stock.remark = data['remark'] 'detail_link': 'detail_link',
'arrival_photo': 'arrival_photo',
'remark': 'remark',
'currency': 'currency',
'exchange_rate': 'exchange_rate',
'purchaser': 'buyer_name',
'purchaser_email': 'buyer_email',
'source_link': 'original_link'
}
# 2. 级联更新基础信息 (MaterialBase) for frontend_key, db_attr in field_mapping.items():
if stock.material: if frontend_key in data:
if 'material_name' in data: stock.material.name = data['material_name'] setattr(stock, db_attr, data[frontend_key])
if 'category' in data: stock.material.category = data['category']
if 'unit' in data: stock.material.unit = data['unit']
# 3. 核心逻辑:数量与价格联动 (增加对前端返回字段名 qty_inbound 和 price_unit 的识别)
qty_changed = False qty_changed = False
price_changed = False price_changed = False
# (A) 数量变更 -> 更新库存和可用量 if 'in_quantity' in data:
new_qty_input = data.get('in_quantity') or data.get('qty_inbound') new_qty = float(data['in_quantity'])
if new_qty_input is not None:
new_qty = float(new_qty_input)
old_qty = float(stock.in_quantity) old_qty = float(stock.in_quantity)
diff = new_qty - old_qty if new_qty != old_qty:
diff = new_qty - old_qty
if diff != 0:
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 qty_changed = True
# (B) 单价变更 if 'unit_price' in data:
new_price_input = data.get('unit_price') or data.get('price_unit') new_price = float(data['unit_price'])
if new_price_input is not None: old_price = float(stock.unit_price)
new_price = float(new_price_input) if new_price != old_price:
if new_price != float(stock.unit_price):
stock.unit_price = new_price stock.unit_price = new_price
price_changed = True price_changed = True
# (C) 重算总价
if qty_changed or price_changed: 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
@staticmethod @staticmethod
def delete_inbound(stock_id): def delete_inbound(stock_id):
"""删除逻辑孤儿策略如果MaterialBase无其他引用则一并删除"""
try: try:
stock = StockBuy.query.get(stock_id) stock = StockBuy.query.get(stock_id)
if not stock: if not stock:
raise ValueError("记录不存在") raise ValueError("记录不存在")
# 1. 记下 base_id
material_id = stock.base_id
# 2. 删除库存记录
db.session.delete(stock) 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() db.session.commit()
return True return True
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
print(f"删除失败: {e}")
raise e raise e
@staticmethod @staticmethod
def get_list(page, limit): def get_list(page, limit, keyword=None):
try: try:
pagination = StockBuy.query.order_by(StockBuy.id.desc()).paginate(page=page, per_page=limit) # 1. 查询分页数据
items = [item.to_dict() for item in pagination.items] 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)
# ---------------------------------------------------------------------
# 新增逻辑:计算总库存
# ---------------------------------------------------------------------
# 2. 提取当前页所有涉及的 base_id
current_items = pagination.items
base_ids = list(set([item.base_id for item in current_items if item.base_id]))
# 3. 聚合查询:一次性查出这些 base_id 对应的 stock_quantity 和 available_quantity 总和
stock_map = {}
if base_ids:
# SELECT base_id, SUM(stock_quantity), SUM(available_quantity) FROM stock_buy WHERE base_id IN (...) GROUP BY base_id
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)
}
# ---------------------------------------------------------------------
items = []
for item in current_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 ''
# 获取该物料的统计数据
stats = stock_map.get(item.base_id, {'total_stock': 0, 'total_avail': 0})
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),
# --- [新增] 聚合统计数据 (用于列表显示) ---
'sum_stock': stats['total_stock'],
'sum_available': stats['total_avail'],
'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,
'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} return {"total": pagination.total, "items": items}
except Exception as e: except Exception as e:
print(f"查询列表失败: {e}") print(f"List Error: {e}")
traceback.print_exc()
return {"total": 0, "items": []} return {"total": 0, "items": []}

View File

@ -1,19 +1,45 @@
import request from '@/utils/request' import request from '@/utils/request'
// 1. 获取列表
export function getBuyList(params: any) { 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) { 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) { 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) { 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 }
})
} }

View File

@ -1,40 +1,97 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<el-card shadow="never"> <el-card shadow="never">
<template #header> <div class="filter-wrapper">
<div class="card-header"> <div class="filter-container">
<el-button type="primary" @click="handleAdd"> <el-input
<el-icon style="margin-right: 5px"><Plus /></el-icon>新增基础信息 v-model="queryParams.keyword"
</el-button> 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> </div>
</template>
<div class="filter-container"> <div class="right-toolbar">
<el-input <el-button type="primary" @click="handleAdd" style="margin-right: 10px">
v-model="queryParams.keyword" <el-icon style="margin-right: 5px"><Plus /></el-icon>新增
placeholder="请输入基础信息名称或规格" </el-button>
style="width: 220px; margin-right: 10px;"
clearable
@keyup.enter="handleQuery"
/>
<el-select v-model="queryParams.category" placeholder="基础信息类别" clearable style="width: 150px; margin-right: 10px;"> <el-tooltip content="刷新" placement="top">
<el-option label="采购件" value="PURCHASE" /> <el-button circle :icon="Refresh" @click="getList" />
<el-option label="自制件" value="SELF_MADE" /> </el-tooltip>
</el-select>
<el-select v-model="queryParams.type" placeholder="物料类型" clearable style="width: 150px; margin-right: 10px;"> <el-dropdown trigger="click" @command="handleSizeChange">
<el-option label="电子料" value="ELEC" /> <el-button circle :icon="Rank" style="margin-left: 8px" title="表格密度" />
<el-option label="结构件" value="STRUCT" /> <template #dropdown>
</el-select> <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-popover placement="bottom" :width="150" trigger="click">
<el-option label="启用" :value="1" /> <template #reference>
<el-option label="禁用" :value="0" /> <el-button circle :icon="Setting" style="margin-left: 8px" title="列设置" />
</el-select> </template>
<div class="column-setting-list">
<el-button type="primary" plain @click="handleQuery">搜索</el-button> <div style="font-weight: bold; margin-bottom: 5px; border-bottom: 1px solid #eee; padding-bottom: 5px">
<el-button plain @click="resetQuery">重置</el-button> 列展示设置
</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> </div>
<el-table <el-table
@ -42,58 +99,29 @@
:data="tableData" :data="tableData"
border border
stripe 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 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 prop="name" label="基础信息名称" min-width="150" 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 prop="category" label="类别" width="100" align="center"> </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"> <template #default="scope">
<el-tag v-if="scope.row.category === 'PURCHASE'">采购件</el-tag> <el-button v-if="scope.row.generalImage" link type="primary" :icon="Picture" title="查看图片" @click="openLink(scope.row.generalImage)" />
<el-tag v-else-if="scope.row.category === 'SELF_MADE'" type="success">自制件</el-tag> <el-button v-if="scope.row.generalManual" link type="primary" :icon="Document" title="查看说明书" @click="openLink(scope.row.generalManual)" />
<el-tag v-else type="info">{{ scope.row.category || '-' }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column v-if="columns.isEnabled.visible" prop="isEnabled" label="是否启用" min-width="100" align="center">
<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">
<template #default="scope"> <template #default="scope">
<el-switch <el-switch
v-model="scope.row.isEnabled" v-model="scope.row.isEnabled"
@ -104,8 +132,7 @@
/> />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" min-width="150" fixed="right" align="center">
<el-table-column label="操作" width="150" fixed="right" align="center">
<template #default="scope"> <template #default="scope">
<el-button link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button> <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> <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;"> <div style="margin-top: 20px; text-align: right;">
<el-pagination <el-pagination
background background
layout="total, prev, pager, next, sizes" layout="total, sizes, prev, pager, next, jumper"
:total="total" :total="total"
v-model:current-page="queryParams.pageNum" v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize" v-model:page-size="queryParams.pageSize"
@ -126,20 +153,101 @@
/> />
</div> </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> </el-card>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'; import { ref, reactive, onMounted, nextTick } from 'vue';
import { Plus, Picture, Document } from '@element-plus/icons-vue'; import { Plus, Picture, Document, Refresh, Setting, Rank } from '@element-plus/icons-vue';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import type { FormInstance, FormRules } from 'element-plus';
// 【关键修改】引入刚才定义的 API 文件
import { import {
listMaterialBase, listMaterialBase,
delMaterialBase, addMaterialBase,
updateMaterialBase updateMaterialBase,
delMaterialBase
} from '@/api/material_base'; } from '@/api/material_base';
// --- 类型定义 --- // --- 类型定义 ---
@ -153,8 +261,8 @@ interface MaterialBaseVO {
visibilityLevel: number; visibilityLevel: number;
generalManual?: string; generalManual?: string;
generalImage?: string; generalImage?: string;
isEnabled: number; // 1 or 0 isEnabled: number;
statusLoading?: boolean; // 辅助字段 statusLoading?: boolean;
} }
interface QueryParams { interface QueryParams {
@ -170,6 +278,23 @@ interface QueryParams {
const loading = ref(false); const loading = ref(false);
const total = ref(0); const total = ref(0);
const tableData = ref<MaterialBaseVO[]>([]); 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>({ const queryParams = reactive<QueryParams>({
pageNum: 1, pageNum: 1,
@ -180,18 +305,78 @@ const queryParams = reactive<QueryParams>({
isEnabled: undefined 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 = () => { const getList = () => {
loading.value = true; loading.value = true;
// 调用 API 文件中的 listMaterialBase
listMaterialBase(queryParams) listMaterialBase(queryParams)
.then((response: any) => { .then((response: any) => {
// 我们的 request.ts 已经处理了 code!=200这里直接拿 data
if (response && response.data) { if (response && response.data) {
tableData.value = response.data.items; tableData.value = response.data.items;
total.value = response.data.total; total.value = response.data.total;
extractDynamicOptions(tableData.value);
} else { } else {
tableData.value = []; tableData.value = [];
total.value = 0; 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 = () => { const handleQuery = () => {
queryParams.pageNum = 1; queryParams.pageNum = 1;
getList(); getList();
}; };
// 重置
const resetQuery = () => { const resetQuery = () => {
queryParams.keyword = ''; queryParams.keyword = '';
queryParams.category = ''; queryParams.category = '';
@ -221,75 +413,111 @@ const resetQuery = () => {
handleQuery(); handleQuery();
}; };
// 新增 const handleSizeChange = (command: 'large' | 'default' | 'small') => {
tableSize.value = command;
};
const handleAdd = () => { const handleAdd = () => {
ElMessage.info("请实现新增弹窗逻辑"); resetForm();
// 逻辑dialogVisible.value = true dialog.title = '新增基础信息';
dialog.visible = true;
}; };
// 编辑
const handleEdit = (row: MaterialBaseVO) => { const handleEdit = (row: MaterialBaseVO) => {
console.log("点击编辑", row); resetForm();
ElMessage.info(`准备编辑 ID: ${row.id}`); dialog.title = '编辑基础信息';
// 逻辑:调用 getMaterialBase(row.id) 回显数据 dialog.visible = true;
}; nextTick(() => {
Object.assign(form.value, row);
// 状态切换 (实时保存)
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(() => {
// 取消
}); });
}; };
// 打开链接 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) => { const openLink = (url: string) => {
if (!url) return; if (!url) return;
window.open(url, '_blank'); window.open(url, '_blank');
} }
// 初始化
onMounted(() => { onMounted(() => {
getList(); getList();
}); });
@ -299,15 +527,25 @@ onMounted(() => {
.app-container { .app-container {
padding: 20px; padding: 20px;
} }
.card-header { .filter-wrapper {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: flex-start;
margin-bottom: 20px;
flex-wrap: wrap;
} }
.filter-container { .filter-container {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
margin-bottom: 20px; gap: 10px;
}
.right-toolbar {
display: flex;
align-items: center;
}
.column-setting-list {
display: flex;
flex-direction: column;
} }
</style> </style>

File diff suppressed because it is too large Load Diff