针对于采购页面进行优化逻辑
This commit is contained in:
@ -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
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
@ -1,77 +1,111 @@
|
|||||||
|
# app/services/inbound/buy_service.py
|
||||||
from app.extensions import db
|
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_
|
||||||
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("缺少必要的物料名称或规格型号")
|
|
||||||
|
|
||||||
# 1. 关联逻辑:通过规格型号(spec_model)查找基础库
|
# 过滤条件:名称或规格包含关键词,且 is_enabled 为 True
|
||||||
material = MaterialBase.query.filter_by(spec_model=data['spec_model']).first()
|
query = MaterialBase.query.filter(
|
||||||
|
MaterialBase.is_enabled == True,
|
||||||
# 如果不存在,则新建 MaterialBase
|
or_(
|
||||||
if not material:
|
MaterialBase.name.ilike(f'%{keyword}%'),
|
||||||
material = MaterialBase(
|
MaterialBase.spec_model.ilike(f'%{keyword}%')
|
||||||
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:
|
||||||
|
# 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'):
|
if data.get('in_date'):
|
||||||
try:
|
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:
|
except ValueError:
|
||||||
try:
|
pass # 格式错误则使用当前日期
|
||||||
in_date_val = datetime.strptime(data['in_date'], '%Y-%m-%d').date()
|
|
||||||
except ValueError:
|
|
||||||
in_date_val = datetime.utcnow().date()
|
|
||||||
|
|
||||||
# --- 修改部分:增加字段兼容性,确保前端传参能被正确读取 ---
|
# 3. 数据转换
|
||||||
in_qty = float(data.get('in_quantity') or data.get('qty_inbound') or 0)
|
in_qty = float(data.get('in_quantity') or 0)
|
||||||
u_price = float(data.get('unit_price') or data.get('price_unit') or 0)
|
u_price = float(data.get('unit_price') or 0)
|
||||||
# ---------------------------------------------------
|
|
||||||
|
|
||||||
# 3. 创建 StockBuy
|
# 4. 创建 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_email=data.get('buyer_email'),
|
# [字段映射] 前端 -> DB
|
||||||
original_link=data.get('original_link'),
|
buyer_name=data.get('purchaser'),
|
||||||
|
buyer_email=data.get('purchaser_email'),
|
||||||
|
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)
|
||||||
@ -80,105 +114,182 @@ class BuyInboundService:
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
|
print(f"Insert Error: {str(e)}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_inbound(stock_id, data):
|
def update_inbound(stock_id, data):
|
||||||
"""更新入库:支持级联更新基础信息 + 自动重算总价"""
|
"""
|
||||||
|
更新入库逻辑
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
|
print(f"----- UPDATE DEBUG: ID={stock_id} -----")
|
||||||
|
|
||||||
stock = StockBuy.query.get(stock_id)
|
stock = StockBuy.query.get(stock_id)
|
||||||
if not stock:
|
if not stock:
|
||||||
raise ValueError("记录不存在")
|
raise ValueError("记录不存在")
|
||||||
|
|
||||||
# 1. 更新普通字段 (增加对 warehouse_loc 的兼容)
|
# 1. 字段映射字典:前端Key -> Model属性名
|
||||||
if 'serial_number' in data: stock.serial_number = data['serial_number']
|
# 使用映射可以避免写大量 if...else,且逻辑更清晰
|
||||||
if 'batch_number' in data: stock.batch_number = data['batch_number']
|
field_mapping = {
|
||||||
if 'warehouse_location' in data: stock.warehouse_location = data['warehouse_location']
|
'sku': 'sku',
|
||||||
if 'warehouse_loc' in data: stock.warehouse_location = data['warehouse_loc']
|
'barcode': 'barcode',
|
||||||
if 'supplier_name' in data: stock.supplier_name = data['supplier_name']
|
'warehouse_location': 'warehouse_location',
|
||||||
if 'status' in data: stock.status = data['status']
|
'serial_number': 'serial_number',
|
||||||
if 'inspection_status' in data: stock.inspection_status = data['inspection_status']
|
'batch_number': 'batch_number',
|
||||||
if 'arrival_photo' in data: stock.arrival_photo = data['arrival_photo']
|
'status': 'status',
|
||||||
if 'remark' in data: stock.remark = data['remark']
|
'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:
|
'purchaser': 'buyer_name',
|
||||||
if 'material_name' in data: stock.material.name = data['material_name']
|
'purchaser_email': 'buyer_email',
|
||||||
if 'category' in data: stock.material.category = data['category']
|
'source_link': 'original_link'
|
||||||
if 'unit' in data: stock.material.unit = data['unit']
|
}
|
||||||
|
|
||||||
# 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
|
qty_changed = False
|
||||||
price_changed = False
|
price_changed = False
|
||||||
|
|
||||||
# (A) 数量变更 -> 更新库存和可用量
|
# (A) 处理入库数量变更
|
||||||
new_qty_input = data.get('in_quantity') or data.get('qty_inbound')
|
if 'in_quantity' in data:
|
||||||
if new_qty_input is not None:
|
new_qty = float(data['in_quantity'])
|
||||||
new_qty = float(new_qty_input)
|
|
||||||
old_qty = float(stock.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.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) 单价变更
|
# (B) 处理单价变更
|
||||||
new_price_input = data.get('unit_price') or data.get('price_unit')
|
if 'unit_price' in data:
|
||||||
if new_price_input is not None:
|
new_price = float(data['unit_price'])
|
||||||
new_price = float(new_price_input)
|
old_price = float(stock.unit_price)
|
||||||
if new_price != float(stock.unit_price):
|
|
||||||
|
if new_price != old_price:
|
||||||
|
print(f"Price Changed: {old_price} -> {new_price}")
|
||||||
stock.unit_price = new_price
|
stock.unit_price = new_price
|
||||||
price_changed = True
|
price_changed = True
|
||||||
|
|
||||||
# (C) 重算总价
|
# (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)
|
||||||
|
print(f"New Total Price: {stock.total_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)
|
# 使用 Outer Join 确保即使 MaterialBase 物理删除,库存记录也不会报错或消失
|
||||||
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)
|
||||||
|
|
||||||
|
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}
|
return {"total": pagination.total, "items": items}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"查询列表失败: {e}")
|
# 打印错误到 Docker 日志
|
||||||
|
print(f"List Error: {e}")
|
||||||
|
traceback.print_exc()
|
||||||
return {"total": 0, "items": []}
|
return {"total": 0, "items": []}
|
||||||
@ -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 }
|
||||||
|
})
|
||||||
}
|
}
|
||||||
@ -1,34 +1,49 @@
|
|||||||
<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">
|
|
||||||
<el-button type="primary" @click="handleAdd">
|
|
||||||
<el-icon style="margin-right: 5px"><Plus /></el-icon>新增基础信息
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="filter-container">
|
<div class="filter-container">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="queryParams.keyword"
|
v-model="queryParams.keyword"
|
||||||
placeholder="请输入基础信息名称或规格"
|
placeholder="请输入名称或规格 (支持模糊搜索)"
|
||||||
style="width: 220px; margin-right: 10px;"
|
style="width: 240px; margin-right: 10px;"
|
||||||
clearable
|
clearable
|
||||||
@keyup.enter="handleQuery"
|
@input="handleInputSearch"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<el-select v-model="queryParams.category" placeholder="基础信息类别" clearable style="width: 150px; margin-right: 10px;">
|
<el-select
|
||||||
<el-option label="采购件" value="PURCHASE" />
|
v-model="queryParams.category"
|
||||||
<el-option label="自制件" value="SELF_MADE" />
|
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>
|
||||||
|
|
||||||
<el-select v-model="queryParams.type" placeholder="物料类型" clearable style="width: 150px; margin-right: 10px;">
|
<el-select
|
||||||
<el-option label="电子料" value="ELEC" />
|
v-model="queryParams.type"
|
||||||
<el-option label="结构件" value="STRUCT" />
|
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>
|
||||||
|
|
||||||
<el-select v-model="queryParams.isEnabled" placeholder="状态" clearable style="width: 100px; margin-right: 10px;">
|
<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="1" />
|
||||||
<el-option label="禁用" :value="0" />
|
<el-option label="禁用" :value="0" />
|
||||||
</el-select>
|
</el-select>
|
||||||
@ -37,63 +52,76 @@
|
|||||||
<el-button plain @click="resetQuery">重置</el-button>
|
<el-button plain @click="resetQuery">重置</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<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-tooltip content="刷新" placement="top">
|
||||||
|
<el-button circle :icon="Refresh" @click="getList" />
|
||||||
|
</el-tooltip>
|
||||||
|
|
||||||
|
<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-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
|
<el-table
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
: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 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) => {
|
const handleStatusChange = (row: MaterialBaseVO) => {
|
||||||
row.statusLoading = true;
|
row.statusLoading = true;
|
||||||
const text = row.isEnabled === 1 ? "启用" : "停用";
|
const text = row.isEnabled === 1 ? "启用" : "停用";
|
||||||
|
const updateData = { id: row.id, isEnabled: row.isEnabled };
|
||||||
const updateData = {
|
|
||||||
id: row.id,
|
|
||||||
isEnabled: row.isEnabled
|
|
||||||
};
|
|
||||||
|
|
||||||
// 调用 API 文件中的 updateMaterialBase
|
|
||||||
updateMaterialBase(updateData)
|
updateMaterialBase(updateData)
|
||||||
.then(() => {
|
.then(() => ElMessage.success(`已${text} "${row.name}"`))
|
||||||
ElMessage.success(`已${text} "${row.name}"`);
|
.catch(() => { row.isEnabled = row.isEnabled === 1 ? 0 : 1; })
|
||||||
})
|
.finally(() => { row.statusLoading = false; });
|
||||||
.catch(() => {
|
|
||||||
// 失败回滚
|
|
||||||
row.isEnabled = row.isEnabled === 1 ? 0 : 1;
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
row.statusLoading = false;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 删除
|
|
||||||
const handleDelete = (row: MaterialBaseVO) => {
|
const handleDelete = (row: MaterialBaseVO) => {
|
||||||
ElMessageBox.confirm(
|
ElMessageBox.confirm(
|
||||||
`是否确认删除名称为 "${row.name}" 的数据项? \n如果该物料已有库存记录,删除将会被拒绝。`,
|
`是否确认删除名称为 "${row.name}" 的数据项?`,
|
||||||
"警告",
|
"警告",
|
||||||
{
|
{ confirmButtonText: "确定", cancelButtonText: "取消", type: "warning" }
|
||||||
confirmButtonText: "确定",
|
|
||||||
cancelButtonText: "取消",
|
|
||||||
type: "warning"
|
|
||||||
}
|
|
||||||
).then(() => {
|
).then(() => {
|
||||||
// 调用 API 文件中的 delMaterialBase
|
delMaterialBase(row.id).then(() => {
|
||||||
delMaterialBase(row.id)
|
|
||||||
.then(() => {
|
|
||||||
ElMessage.success("删除成功");
|
ElMessage.success("删除成功");
|
||||||
if (tableData.value.length === 1 && queryParams.pageNum > 1) {
|
if (tableData.value.length === 1 && queryParams.pageNum > 1) queryParams.pageNum--;
|
||||||
queryParams.pageNum--;
|
|
||||||
}
|
|
||||||
getList();
|
getList();
|
||||||
});
|
});
|
||||||
}).catch(() => {
|
}).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>
|
||||||
@ -1,23 +1,40 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="buy-module">
|
<div class="buy-module">
|
||||||
<div class="header-tools">
|
<div class="header-tools">
|
||||||
<div class="left-actions">
|
<div class="left-tools">
|
||||||
<el-button type="primary" :icon="Plus" @click="handleCreate" class="big-font-btn">采购入库登记</el-button>
|
<el-input
|
||||||
<el-button :icon="Refresh" @click="fetchData" class="big-font-btn">刷新数据</el-button>
|
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>
|
</div>
|
||||||
|
|
||||||
<el-popover placement="bottom" title="显示列配置" :width="400" trigger="click">
|
<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>
|
<template #reference>
|
||||||
<el-button :icon="Setting" class="big-font-btn">自定义表格表头</el-button>
|
<el-button :icon="Setting" class="big-font-btn">自定义表头</el-button>
|
||||||
</template>
|
</template>
|
||||||
<el-checkbox-group v-model="visibleColumnProps" class="column-selector">
|
<el-checkbox-group v-model="visibleColumnProps" class="column-selector">
|
||||||
<el-divider content-position="left">基础层字段</el-divider>
|
<el-row>
|
||||||
<el-checkbox v-for="c in baseColumns" :key="c.prop" :label="c.prop">{{ c.label }}</el-checkbox>
|
<el-col :span="24"><el-divider content-position="left">基础信息 (只读)</el-divider></el-col>
|
||||||
<el-divider content-position="left">库存/财务层字段</el-divider>
|
<el-col :span="8" v-for="c in baseColumns" :key="c.prop"><el-checkbox :label="c.prop">{{ c.label }}</el-checkbox></el-col>
|
||||||
<el-checkbox v-for="c in stockColumns" :key="c.prop" :label="c.prop">{{ c.label }}</el-checkbox>
|
<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-checkbox-group>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
@ -25,46 +42,46 @@
|
|||||||
border
|
border
|
||||||
stripe
|
stripe
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
size="default"
|
|
||||||
highlight-current-row
|
|
||||||
class="custom-big-table"
|
class="custom-big-table"
|
||||||
|
highlight-current-row
|
||||||
>
|
>
|
||||||
<template v-for="col in allColumns" :key="col.prop">
|
<template v-for="col in allColumns" :key="col.prop">
|
||||||
<el-table-column
|
<el-table-column
|
||||||
v-if="visibleColumnProps.includes(col.prop)"
|
v-if="visibleColumnProps.includes(col.prop)"
|
||||||
:prop="col.prop"
|
:prop="col.prop"
|
||||||
:label="col.label"
|
:label="col.label"
|
||||||
:min-width="col.minWidth || '150'"
|
:min-width="col.minWidth || '140'"
|
||||||
show-overflow-tooltip
|
show-overflow-tooltip
|
||||||
>
|
>
|
||||||
<template #default="scope" v-if="col.prop === 'serial_batch'">
|
<template #default="scope" v-if="['serial_number', 'batch_number'].includes(col.prop)">
|
||||||
<span v-if="scope.row.serial_number" style="color: #409EFF; font-weight: bold;">
|
<span v-if="scope.row[col.prop]" :class="col.prop === 'serial_number' ? 'text-sn' : 'text-bn'">
|
||||||
SN: {{ scope.row.serial_number }}
|
{{ scope.row[col.prop] }}
|
||||||
</span>
|
|
||||||
<span v-else-if="scope.row.batch_number" style="color: #67C23A; font-weight: bold;">
|
|
||||||
BN: {{ scope.row.batch_number }}
|
|
||||||
</span>
|
</span>
|
||||||
<span v-else>-</span>
|
<span v-else>-</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #default="scope" v-else-if="col.prop === 'status'">
|
<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 }}
|
{{ scope.row.status }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #default="scope" v-else-if="['price_unit', 'price_total'].includes(col.prop)">
|
<template #default="scope" v-else-if="col.prop.includes('link')">
|
||||||
{{ formatMoney(scope.row[col.prop]) }}
|
<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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||||
<template #default="{ row }">
|
<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)">
|
<el-popconfirm title="确定删除该条入库记录吗?" @confirm="handleDelete(row)">
|
||||||
<template #reference>
|
<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>
|
</template>
|
||||||
</el-popconfirm>
|
</el-popconfirm>
|
||||||
</template>
|
</template>
|
||||||
@ -84,118 +101,197 @@
|
|||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="visible"
|
v-model="visible"
|
||||||
:title="dialogStatus === 'create' ? '新增采购件入库' : '编辑入库信息'"
|
:title="dialogStatus === 'create' ? '新增采购入库' : '编辑入库信息'"
|
||||||
width="950px"
|
width="1100px"
|
||||||
top="3vh"
|
top="3vh"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
:close-on-click-modal="false"
|
: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">
|
||||||
|
|
||||||
|
<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>1. 基础核心层 (Material Base)</b></el-divider>
|
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="8">
|
<el-col :span="12" v-if="dialogStatus === 'create'">
|
||||||
<el-form-item label="名称" prop="material_name">
|
<el-form-item label="搜索关联" prop="base_id">
|
||||||
<el-input v-model="form.material_name" :disabled="dialogStatus === 'update'" placeholder="必填" />
|
<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-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="8">
|
|
||||||
<el-form-item label="规格型号" prop="spec_model">
|
<el-col :span="12" v-if="dialogStatus === 'create'">
|
||||||
<el-input v-model="form.spec_model" :disabled="dialogStatus === 'update'" placeholder="必填: 内部货号" />
|
<el-alert title="注:只能入库状态为'启用'的基础物料。" type="warning" :closable="false" show-icon />
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
</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>
|
</el-row>
|
||||||
|
|
||||||
<el-divider content-position="left"><b>2. 实体库存层 (Stock Buy)</b></el-divider>
|
<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>
|
||||||
|
|
||||||
|
<div class="section-block">
|
||||||
|
<el-divider content-position="left"><el-icon><House /></el-icon> <b>2. 库存实体信息</b></el-divider>
|
||||||
|
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="8">
|
<el-col :span="6"><el-form-item label="编码/SKU" prop="sku"><el-input v-model="form.sku" /></el-form-item></el-col>
|
||||||
<el-form-item label="编码/SKU" prop="sku">
|
|
||||||
<el-input v-model="form.sku" placeholder="选填" />
|
<el-col :span="6">
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="8">
|
|
||||||
<el-form-item label="入库日期" prop="in_date">
|
<el-form-item label="入库日期" prop="in_date">
|
||||||
<el-input v-model="form.in_date" disabled placeholder="系统自动生成">
|
<el-date-picker
|
||||||
<template #suffix><el-icon><Calendar /></el-icon></template>
|
v-model="form.in_date"
|
||||||
</el-input>
|
type="date"
|
||||||
</el-form-item>
|
value-format="YYYY-MM-DD"
|
||||||
</el-col>
|
style="width:100%"
|
||||||
<el-col :span="8">
|
disabled
|
||||||
<el-form-item label="库位" prop="warehouse_location">
|
placeholder="自动生成"
|
||||||
<el-input v-model="form.warehouse_location" />
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</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>
|
</el-row>
|
||||||
|
|
||||||
<el-row :gutter="20" style="background-color: #fffbf0; border-radius: 4px; padding-top:10px;">
|
<div class="sn-bn-row">
|
||||||
<el-col :span="12">
|
<el-row style="margin-bottom: 15px; padding-left: 20px;">
|
||||||
<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">
|
<el-col :span="24">
|
||||||
<div style="font-size: 14px; color: #e6a23c; margin-left: 140px; margin-bottom: 10px; line-height: 1;">
|
<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>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-row :gutter="20" style="margin-top: 10px;">
|
<el-row :gutter="20">
|
||||||
<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="10">
|
||||||
<el-col :span="12"><el-form-item label="照片/到货图" prop="arrival_photo"><el-input v-model="form.arrival_photo" /></el-form-item></el-col>
|
<el-form-item label="批号" prop="batch_number">
|
||||||
</el-row>
|
<el-input
|
||||||
|
v-model="form.batch_number"
|
||||||
<el-row :gutter="20" style="background-color: #f8fcfd; padding-top: 18px; border-radius: 4px; margin-top: 10px;">
|
placeholder="系统自动生成"
|
||||||
<el-col :span="8">
|
:disabled="entryMode === 'serial'"
|
||||||
<el-form-item label="入库量" prop="in_quantity">
|
clearable
|
||||||
<el-input-number
|
>
|
||||||
v-model="form.in_quantity"
|
<template #prefix><span style="color:#67C23A; font-weight:bold">BN</span></template>
|
||||||
:min="1"
|
</el-input>
|
||||||
:step="1"
|
|
||||||
:precision="0"
|
|
||||||
style="width:100%"
|
|
||||||
controls-position="right"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="8">
|
<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-form-item label="库存数量" prop="stock_quantity">
|
||||||
<el-input-number v-model="form.stock_quantity" disabled style="width:100%" :controls="false" />
|
<el-input-number v-model="form.stock_quantity" disabled style="width:100%" :controls="false" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="8">
|
<el-col :span="6">
|
||||||
<el-form-item label="可用数量" prop="available_quantity">
|
<el-form-item label="可用数量" prop="available_quantity">
|
||||||
<el-input-number v-model="form.available_quantity" disabled style="width:100%" :controls="false" />
|
<el-input-number v-model="form.available_quantity" disabled style="width:100%" :controls="false" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
<el-col :span="6">
|
||||||
|
<el-form-item label="库存状态" prop="status">
|
||||||
<el-divider content-position="left"><b>3. 财务与商务信息</b></el-divider>
|
<el-select v-model="form.status" style="width:100%">
|
||||||
<el-row :gutter="20">
|
<el-option label="在库" value="在库" />
|
||||||
<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-option label="出库" value="出库" />
|
||||||
<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-option label="损耗" value="损耗" />
|
||||||
<el-col :span="8"><el-form-item label="供应商" prop="supplier_name"><el-input v-model="form.supplier_name" /></el-form-item></el-col>
|
</el-select>
|
||||||
</el-row>
|
|
||||||
|
|
||||||
<el-form-item label="备注" prop="remark">
|
|
||||||
<el-input v-model="form.remark" type="textarea" :rows="2" />
|
|
||||||
</el-form-item>
|
</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>
|
</el-form>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="visible = false" style="font-size: 15px;">取消</el-button>
|
<el-button @click="visible = false" class="big-font-btn">取消</el-button>
|
||||||
<el-button type="primary" :loading="submitting" @click="submitForm" style="font-size: 15px;">
|
<el-button type="primary" :loading="submitting" @click="submitForm" class="big-font-btn">
|
||||||
{{ dialogStatus === 'create' ? '确认入库' : '保存修改' }}
|
{{ dialogStatus === 'create' ? '确认入库' : '保存修改' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
@ -205,106 +301,218 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted, watch } from 'vue'
|
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 { ElMessage } from 'element-plus'
|
||||||
import dayjs from 'dayjs'
|
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 loading = ref(false)
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
|
const searchLoading = ref(false)
|
||||||
const dialogStatus = ref<'create' | 'update'>('create')
|
const dialogStatus = ref<'create' | 'update'>('create')
|
||||||
const tableData = ref([])
|
const tableData = ref([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const formRef = ref()
|
const formRef = ref()
|
||||||
const queryParams = reactive({ page: 1, pageSize: 15 })
|
const queryParams = reactive({ page: 1, pageSize: 15, keyword: '' })
|
||||||
|
const materialOptions = ref<any[]>([])
|
||||||
|
|
||||||
// --- 1. 列定义 ---
|
// 新增:入库模式控制
|
||||||
|
const entryMode = ref('batch')
|
||||||
|
const modeLocked = ref(false)
|
||||||
|
|
||||||
|
// 列定义 (保持不变)
|
||||||
const baseColumns = [
|
const baseColumns = [
|
||||||
{ prop: 'material_name', label: '物料名称', minWidth: '200' },
|
{ prop: 'material_name', label: '名称' },
|
||||||
{ prop: 'category', label: '类别', minWidth: '120' },
|
{ prop: 'category', label: '类别' },
|
||||||
{ prop: 'spec_model', label: '规格型号', minWidth: '200' },
|
{ prop: 'material_type', label: '类型' },
|
||||||
{ prop: 'unit', label: '单位', minWidth: '100' }
|
{ prop: 'spec_model', label: '规格型号' },
|
||||||
|
{ prop: 'unit', label: '单位' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const stockColumns = [
|
const stockColumns = [
|
||||||
{ prop: 'sku', label: '编码/SKU', minWidth: '180' },
|
{ prop: 'id', label: 'ID', minWidth: '60' },
|
||||||
{ prop: 'inbound_date', label: '入库日期', minWidth: '160' },
|
{ prop: 'base_id', label: 'BaseID', minWidth: '80' },
|
||||||
{ prop: 'serial_batch', label: '序列号/批号', minWidth: '220' },
|
{ prop: 'sku', label: 'SKU', minWidth: '120' },
|
||||||
{ prop: 'qty_stock', label: '库存数', minWidth: '120' },
|
{ prop: 'inbound_date', label: '入库日期', minWidth: '120' },
|
||||||
{ prop: 'qty_available', label: '可用数', minWidth: '120' },
|
{ prop: 'barcode', label: '条码', minWidth: '120' },
|
||||||
{ prop: 'qty_inbound', label: '入库量', minWidth: '120' },
|
{ prop: 'serial_number', label: '序列号', minWidth: '150' },
|
||||||
{ prop: 'price_unit', label: '单价', minWidth: '150' },
|
{ prop: 'batch_number', label: '批号', minWidth: '150' },
|
||||||
{ prop: 'price_total', label: '总价', minWidth: '150' },
|
{ prop: 'status', label: '状态', minWidth: '100' },
|
||||||
{ prop: 'status', label: '状态', minWidth: '120' },
|
{ prop: 'inspection_status', label: '到检', minWidth: '100' },
|
||||||
{ prop: 'warehouse_loc', label: '库位', minWidth: '150' },
|
{ prop: 'qty_inbound', label: '入库量', minWidth: '100' },
|
||||||
{ prop: 'supplier_name', label: '供应商', minWidth: '200' }
|
{ 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]
|
const allColumns = [...baseColumns, ...stockColumns]
|
||||||
|
|
||||||
// --- 2. 默认展示列 ---
|
|
||||||
const visibleColumnProps = ref([
|
const visibleColumnProps = ref([
|
||||||
'material_name', 'spec_model', 'inbound_date',
|
'material_name', 'category', 'material_type', 'spec_model', 'unit',
|
||||||
'serial_batch', 'qty_stock', 'qty_available', 'status'
|
'inbound_date', 'serial_number', 'batch_number', 'status', 'inspection_status',
|
||||||
|
'unit_price', 'total_price', 'supplier_name', 'purchaser'
|
||||||
])
|
])
|
||||||
|
|
||||||
// --- 3. 表单对象 ---
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
id: undefined,
|
id: undefined,
|
||||||
material_name: '', category: '', spec_model: '', unit: '个',
|
base_id: undefined as number | undefined,
|
||||||
material_type: '采购件', visibility_level: 0,
|
material_name: '', spec_model: '', category: '', unit: '', material_type: '',
|
||||||
sku: '', in_date: '',
|
sku: '', barcode: '', in_date: '',
|
||||||
serial_number: '', batch_number: '',
|
serial_number: '', batch_number: '',
|
||||||
status: '在库', inspection_status: '未检',
|
status: '在库', inspection_status: '未检',
|
||||||
in_quantity: 1, stock_quantity: 1, available_quantity: 1,
|
in_quantity: 1, stock_quantity: 1, available_quantity: 1,
|
||||||
warehouse_location: '', unit_price: 0, total_price: 0,
|
warehouse_location: '',
|
||||||
supplier_name: '', arrival_photo: '', remark: ''
|
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 {
|
} else {
|
||||||
if (formRef.value) {
|
callback()
|
||||||
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')
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
callback()
|
callback()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
material_name: [{ required: true, message: '必填', trigger: 'blur' }],
|
base_id: [{ required: true, message: '请搜索并选择基础物料', trigger: 'change' }],
|
||||||
spec_model: [{ required: true, message: '必填', trigger: 'blur' }],
|
in_quantity: [{ required: true, message: '请输入入库数量', trigger: 'blur' }],
|
||||||
in_quantity: [{ required: true, message: '必填', trigger: 'blur' }],
|
in_date: [{ required: true, message: '请选择日期', trigger: 'change' }],
|
||||||
serial_number: [{ validator: validateIdentity, trigger: 'blur' }],
|
serial_number: [{ validator: validateIdentity, trigger: 'blur' }, { validator: validateUnique, trigger: 'blur' }],
|
||||||
batch_number: [{ validator: validateIdentity, 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) => {
|
const checkHistoryAndSetMode = async (baseId: number) => {
|
||||||
if (newVal !== undefined) {
|
try {
|
||||||
if (dialogStatus.value === 'create') {
|
const res: any = await getBuyList({ page: 1, pageSize: 1000 })
|
||||||
form.stock_quantity = newVal
|
const allItems = res.data.items || []
|
||||||
form.available_quantity = newVal
|
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)
|
||||||
}
|
}
|
||||||
form.total_price = Number((newVal * form.unit_price).toFixed(4))
|
} else {
|
||||||
|
modeLocked.value = false
|
||||||
|
entryMode.value = 'batch'
|
||||||
|
form.batch_number = '000001'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(formRef.value) {
|
||||||
|
formRef.value.clearValidate('serial_number')
|
||||||
|
formRef.value.clearValidate('batch_number')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
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) => {
|
// CRUD 操作
|
||||||
if (newVal !== undefined) {
|
|
||||||
form.total_price = Number((newVal * (form.in_quantity || 0)).toFixed(4))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// --- 6. 核心操作 ---
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@ -317,44 +525,67 @@ const fetchData = async () => {
|
|||||||
const handleCreate = () => {
|
const handleCreate = () => {
|
||||||
dialogStatus.value = 'create'
|
dialogStatus.value = 'create'
|
||||||
resetForm()
|
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
|
visible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUpdate = (row: any) => {
|
const handleUpdate = (row: any) => {
|
||||||
dialogStatus.value = 'update'
|
dialogStatus.value = 'update'
|
||||||
resetForm()
|
resetForm()
|
||||||
|
|
||||||
|
modeLocked.value = true
|
||||||
|
|
||||||
form.id = row.id
|
form.id = row.id
|
||||||
|
form.base_id = row.base_id
|
||||||
form.material_name = row.material_name
|
form.material_name = row.material_name
|
||||||
form.spec_model = row.spec_model
|
form.spec_model = row.spec_model
|
||||||
form.category = row.category
|
form.category = row.category
|
||||||
form.unit = row.unit
|
form.unit = row.unit
|
||||||
|
form.material_type = row.material_type
|
||||||
|
|
||||||
form.sku = row.sku
|
form.sku = row.sku
|
||||||
|
form.barcode = row.barcode
|
||||||
|
// 编辑时回显原有时间,disabled 属性确保不可修改
|
||||||
form.in_date = row.inbound_date
|
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.in_quantity = Number(row.qty_inbound) || 0
|
||||||
form.stock_quantity = Number(row.qty_stock) || 0
|
form.stock_quantity = Number(row.qty_stock) || 0
|
||||||
form.available_quantity = Number(row.qty_available) || 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.unit_price = Number(row.unit_price) || 0
|
||||||
form.warehouse_location = row.warehouse_loc || ''
|
form.total_price = Number(row.total_price) || 0
|
||||||
form.serial_number = row.serial_number || ''
|
form.currency = row.currency
|
||||||
form.batch_number = row.batch_number || ''
|
form.exchange_rate = Number(row.exchange_rate)
|
||||||
form.supplier_name = row.supplier_name || ''
|
form.supplier_name = row.supplier_name
|
||||||
form.status = row.status || '在库'
|
form.purchaser = row.purchaser
|
||||||
form.remark = row.remark || ''
|
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
|
visible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDelete = async (row: any) => {
|
// 核心修复:修复编辑不生效的问题
|
||||||
try {
|
|
||||||
await deleteBuyInbound(row.id)
|
|
||||||
ElMessage.success('删除成功')
|
|
||||||
fetchData()
|
|
||||||
} catch (e: any) {
|
|
||||||
ElMessage.error('删除失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitForm = async () => {
|
const submitForm = async () => {
|
||||||
if (!formRef.value) return
|
if (!formRef.value) return
|
||||||
await formRef.value.validate(async (valid: boolean) => {
|
await formRef.value.validate(async (valid: boolean) => {
|
||||||
@ -365,57 +596,103 @@ const submitForm = async () => {
|
|||||||
await createBuyInbound(form)
|
await createBuyInbound(form)
|
||||||
ElMessage.success('入库成功')
|
ElMessage.success('入库成功')
|
||||||
} else {
|
} 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('更新成功')
|
ElMessage.success('更新成功')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. 更新成功后,先刷新数据
|
||||||
|
await fetchData()
|
||||||
|
|
||||||
|
// 3. 数据刷新完毕,再关闭弹窗
|
||||||
visible.value = false
|
visible.value = false
|
||||||
fetchData()
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
ElMessage.error('提交失败')
|
ElMessage.error(e.msg || '操作失败')
|
||||||
} finally { submitting.value = false }
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (row: any) => {
|
||||||
|
try {
|
||||||
|
await deleteBuyInbound(row.id)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
fetchData()
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error('删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
|
materialOptions.value = []
|
||||||
Object.assign(form, {
|
Object.assign(form, {
|
||||||
id: undefined,
|
id: undefined, base_id: undefined,
|
||||||
material_name: '', category: '', spec_model: '', unit: '个',
|
material_name: '', spec_model: '', category: '', unit: '', material_type: '',
|
||||||
material_type: '采购件', visibility_level: 0,
|
sku: '', barcode: '', in_date: '',
|
||||||
sku: '', in_date: '',
|
|
||||||
serial_number: '', batch_number: '',
|
serial_number: '', batch_number: '',
|
||||||
status: '在库', inspection_status: '未检',
|
status: '在库', inspection_status: '未检',
|
||||||
in_quantity: 1, stock_quantity: 1, available_quantity: 1,
|
in_quantity: 1, stock_quantity: 1, available_quantity: 1,
|
||||||
warehouse_location: '', unit_price: 0, total_price: 0,
|
warehouse_location: '',
|
||||||
supplier_name: '', arrival_photo: '', remark: ''
|
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) => {
|
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)
|
const num = Number(val)
|
||||||
return isNaN(num) ? '-' : `¥ ${num.toFixed(2)}`
|
return isNaN(num) ? '-' : `${currency} ${num.toFixed(2)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => fetchData())
|
onMounted(() => fetchData())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.buy-module { background: #fff; border-radius: 8px; padding: 15px; }
|
.buy-module { background: #fff; 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; }
|
|
||||||
|
|
||||||
.custom-big-table { font-size: 15px !important; }
|
/* 头部布局优化:左搜右操作 */
|
||||||
:deep(.el-table .el-table__cell) { padding: 12px 0; }
|
.header-tools { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; }
|
||||||
:deep(.el-table th .cell) { font-size: 22px; font-weight: bold; }
|
.left-tools { flex: 0 0 300px; /* 左侧固定宽度或自适应 */ }
|
||||||
:deep(.el-table td .cell) { line-height: 1.5; font-size: 15px; }
|
.right-tools { display: flex; gap: 10px; align-items: center; }
|
||||||
.big-font-btn { font-size: 15px !important; padding: 15px 25px !important; }
|
|
||||||
:deep(.el-form-item__label) { font-size: 15px !important; }
|
.search-input { width: 100%; }
|
||||||
:deep(.el-input__inner) { font-size: 15px !important; height: 45px; }
|
|
||||||
:deep(.el-divider__text) { font-size: 15px !important; }
|
.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>
|
</style>
|
||||||
Reference in New Issue
Block a user