Compare commits
2 Commits
cd192624b9
...
d2d9abe201
| Author | SHA1 | Date | |
|---|---|---|---|
| d2d9abe201 | |||
| ac97c6066b |
@ -105,7 +105,17 @@ def create_app():
|
|||||||
print(f"❌ 错误: Outbound 模块导入失败: {e}")
|
print(f"❌ 错误: Outbound 模块导入失败: {e}")
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 2.6 注册 BOM 模块
|
# 2.6 注册报废模块
|
||||||
|
# -----------------------------------------------------
|
||||||
|
try:
|
||||||
|
from app.api.v1.scrap import scrap_bp
|
||||||
|
app.register_blueprint(scrap_bp, url_prefix='/api/v1/scrap')
|
||||||
|
print("✅ Scrap 模块注册成功")
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"❌ 错误: Scrap 模块导入失败: {e}")
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 2.7 注册 BOM 模块
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
try:
|
try:
|
||||||
from app.api.v1.bom import bom_bp
|
from app.api.v1.bom import bom_bp
|
||||||
|
|||||||
@ -122,7 +122,8 @@ def get_list():
|
|||||||
'isEnabled': request.args.get('isEnabled', None),
|
'isEnabled': request.args.get('isEnabled', None),
|
||||||
'orderByColumn': request.args.get('orderByColumn', ''),
|
'orderByColumn': request.args.get('orderByColumn', ''),
|
||||||
'isAsc': request.args.get('isAsc', None),
|
'isAsc': request.args.get('isAsc', None),
|
||||||
'advancedFilters': advanced_filters_list
|
'advancedFilters': advanced_filters_list,
|
||||||
|
'enableWarningSort': request.args.get('enableWarningSort', 'false').lower() == 'true'
|
||||||
}
|
}
|
||||||
|
|
||||||
user_permissions = get_current_user_permissions()
|
user_permissions = get_current_user_permissions()
|
||||||
@ -325,3 +326,76 @@ def delete(id):
|
|||||||
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
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# 2.5 批量设置预警 API (POST /api/v1/inbound/base/warning/batch-set)
|
||||||
|
# ==============================================================================
|
||||||
|
@inbound_base_bp.route('/warning/batch-set', methods=['POST'])
|
||||||
|
@permission_required('material_list:edit_warning')
|
||||||
|
def batch_set_warning():
|
||||||
|
"""
|
||||||
|
批量设置物料预警配置
|
||||||
|
请求体格式: [
|
||||||
|
{"baseId": 1, "isEnabled": true, "yellowThreshold": 10, "redThreshold": 5},
|
||||||
|
{"baseId": 2, "isEnabled": false}
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
if not isinstance(data, list):
|
||||||
|
return jsonify({"code": 400, "msg": "请求体必须为数组"})
|
||||||
|
|
||||||
|
from app.models.base import MaterialWarningSetting
|
||||||
|
|
||||||
|
updated_count = 0
|
||||||
|
created_count = 0
|
||||||
|
|
||||||
|
for item in data:
|
||||||
|
base_id = item.get('baseId')
|
||||||
|
if not base_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 查找物料是否存在
|
||||||
|
material = MaterialBase.query.get(base_id)
|
||||||
|
if not material:
|
||||||
|
current_app.logger.warning(f"物料ID {base_id} 不存在,跳过")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 查找现有预警设置
|
||||||
|
warning = MaterialWarningSetting.query.filter_by(base_id=base_id).first()
|
||||||
|
|
||||||
|
if warning:
|
||||||
|
# 更新现有记录
|
||||||
|
if 'isEnabled' in item:
|
||||||
|
warning.is_enabled = bool(item['isEnabled'])
|
||||||
|
if 'yellowThreshold' in item:
|
||||||
|
warning.yellow_threshold = item['yellowThreshold']
|
||||||
|
if 'redThreshold' in item:
|
||||||
|
warning.red_threshold = item['redThreshold']
|
||||||
|
updated_count += 1
|
||||||
|
else:
|
||||||
|
# 创建新记录
|
||||||
|
warning = MaterialWarningSetting(
|
||||||
|
base_id=base_id,
|
||||||
|
is_enabled=item.get('isEnabled', False),
|
||||||
|
yellow_threshold=item.get('yellowThreshold'),
|
||||||
|
red_threshold=item.get('redThreshold')
|
||||||
|
)
|
||||||
|
db.session.add(warning)
|
||||||
|
created_count += 1
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({
|
||||||
|
"code": 200,
|
||||||
|
"msg": "批量设置成功",
|
||||||
|
"data": {
|
||||||
|
"created": created_count,
|
||||||
|
"updated": updated_count
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
current_app.logger.error(f"批量设置预警失败: {str(e)}")
|
||||||
|
return jsonify({"code": 500, "msg": f"批量设置预警失败: {str(e)}"}), 500
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
# app/models/__init__.py
|
# app/models/__init__.py
|
||||||
|
|
||||||
# 1. 基础物料 (必须先加载,因为 buy 依赖它)
|
# 1. 基础物料 (必须先加载,因为 buy 依赖它)
|
||||||
from app.models.base import MaterialBase
|
from app.models.base import MaterialBase, MaterialWarningSetting
|
||||||
|
|
||||||
# 2. 采购入库 (现在的类名是 StockBuy)
|
# 2. 采购入库 (现在的类名是 StockBuy)
|
||||||
from app.models.inbound.buy import StockBuy
|
from app.models.inbound.buy import StockBuy
|
||||||
|
|||||||
@ -47,6 +47,9 @@ class MaterialBase(db.Model):
|
|||||||
# 4. 关联服务库存 (StockService)
|
# 4. 关联服务库存 (StockService)
|
||||||
stock_services = db.relationship('StockService', back_populates='base', lazy='dynamic')
|
stock_services = db.relationship('StockService', back_populates='base', lazy='dynamic')
|
||||||
|
|
||||||
|
# 5. 关联预警设置 (MaterialWarningSetting)
|
||||||
|
warning_settings = db.relationship('MaterialWarningSetting', back_populates='material', lazy='dynamic', cascade='all, delete-orphan')
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
"""
|
"""
|
||||||
序列化方法
|
序列化方法
|
||||||
@ -78,4 +81,30 @@ class MaterialBase(db.Model):
|
|||||||
'generalImage': parse_list(self.product_image),
|
'generalImage': parse_list(self.product_image),
|
||||||
# 【核心修改】:直接返回布尔值,不再转成 1 或 0
|
# 【核心修改】:直接返回布尔值,不再转成 1 或 0
|
||||||
'isEnabled': bool(self.is_enabled),
|
'isEnabled': bool(self.is_enabled),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MaterialWarningSetting(db.Model):
|
||||||
|
"""
|
||||||
|
物料预警设置表模型
|
||||||
|
对应数据库表: material_warning_settings
|
||||||
|
"""
|
||||||
|
__tablename__ = 'material_warning_settings'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
base_id = db.Column(db.Integer, db.ForeignKey('material_base.id'), nullable=False, comment='物料基础信息ID')
|
||||||
|
is_enabled = db.Column(db.Boolean, default=False, comment='是否启用预警')
|
||||||
|
yellow_threshold = db.Column(db.Numeric(10, 2), nullable=True, comment='黄色预警阈值')
|
||||||
|
red_threshold = db.Column(db.Numeric(10, 2), nullable=True, comment='红色预警阈值')
|
||||||
|
|
||||||
|
# 关联关系
|
||||||
|
material = db.relationship('MaterialBase', back_populates='warning_settings')
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'baseId': self.base_id,
|
||||||
|
'isEnabled': bool(self.is_enabled),
|
||||||
|
'yellowThreshold': float(self.yellow_threshold) if self.yellow_threshold is not None else None,
|
||||||
|
'redThreshold': float(self.red_threshold) if self.red_threshold is not None else None
|
||||||
}
|
}
|
||||||
@ -113,8 +113,18 @@ class MaterialBaseService:
|
|||||||
def get_list(page, limit, filters=None, user_permissions=None):
|
def get_list(page, limit, filters=None, user_permissions=None):
|
||||||
"""
|
"""
|
||||||
获取基础信息列表 (带分页、高级筛选和全字段排序)
|
获取基础信息列表 (带分页、高级筛选和全字段排序)
|
||||||
|
支持库存预警功能(如果用户有 view_warning 权限)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
# 检查用户是否有查看预警的权限
|
||||||
|
has_warning_permission = False
|
||||||
|
if user_permissions:
|
||||||
|
# 超级管理员有所有权限
|
||||||
|
if '*' in user_permissions or 'material_list:*' in user_permissions:
|
||||||
|
has_warning_permission = True
|
||||||
|
elif 'material_list:view_warning' in user_permissions:
|
||||||
|
has_warning_permission = True
|
||||||
|
|
||||||
# 构建聚合子查询
|
# 构建聚合子查询
|
||||||
buy_sub = db.session.query(
|
buy_sub = db.session.query(
|
||||||
StockBuy.base_id,
|
StockBuy.base_id,
|
||||||
@ -142,14 +152,21 @@ class MaterialBaseService:
|
|||||||
func.coalesce(semi_sub.c.semi_avail, 0) + \
|
func.coalesce(semi_sub.c.semi_avail, 0) + \
|
||||||
func.coalesce(prod_sub.c.prod_avail, 0)
|
func.coalesce(prod_sub.c.prod_avail, 0)
|
||||||
|
|
||||||
# 主查询,关联聚合子查询
|
# 导入预警设置模型
|
||||||
|
from app.models.base import MaterialWarningSetting
|
||||||
|
|
||||||
|
# 主查询,关联聚合子查询和预警设置
|
||||||
query = db.session.query(
|
query = db.session.query(
|
||||||
MaterialBase,
|
MaterialBase,
|
||||||
total_inv.label('total_inv'),
|
total_inv.label('total_inv'),
|
||||||
total_avail.label('total_avail')
|
total_avail.label('total_avail'),
|
||||||
|
MaterialWarningSetting.is_enabled.label('warning_enabled'),
|
||||||
|
MaterialWarningSetting.yellow_threshold.label('warning_yellow'),
|
||||||
|
MaterialWarningSetting.red_threshold.label('warning_red')
|
||||||
).outerjoin(buy_sub, MaterialBase.id == buy_sub.c.base_id) \
|
).outerjoin(buy_sub, MaterialBase.id == buy_sub.c.base_id) \
|
||||||
.outerjoin(semi_sub, MaterialBase.id == semi_sub.c.base_id) \
|
.outerjoin(semi_sub, MaterialBase.id == semi_sub.c.base_id) \
|
||||||
.outerjoin(prod_sub, MaterialBase.id == prod_sub.c.base_id)
|
.outerjoin(prod_sub, MaterialBase.id == prod_sub.c.base_id) \
|
||||||
|
.outerjoin(MaterialWarningSetting, MaterialBase.id == MaterialWarningSetting.base_id)
|
||||||
|
|
||||||
if filters:
|
if filters:
|
||||||
# 1. 关键词模糊搜索
|
# 1. 关键词模糊搜索
|
||||||
@ -277,7 +294,64 @@ class MaterialBaseService:
|
|||||||
# 排序处理(支持全字段)
|
# 排序处理(支持全字段)
|
||||||
order_by_column = filters.get('orderByColumn', '')
|
order_by_column = filters.get('orderByColumn', '')
|
||||||
is_asc = filters.get('isAsc', None)
|
is_asc = filters.get('isAsc', None)
|
||||||
if order_by_column:
|
|
||||||
|
# 检查是否启用了预警智能排序
|
||||||
|
enable_warning_sort = has_warning_permission and filters.get('enableWarningSort', False)
|
||||||
|
|
||||||
|
if enable_warning_sort:
|
||||||
|
# 预警智能排序:先按预警状态排序,再按缺口/余量排序
|
||||||
|
# 使用 CASE 表达式计算预警状态
|
||||||
|
# 状态: 2=红(库存<=red), 1=黄(red<库存<=yellow), 0=正常/未开启
|
||||||
|
from sqlalchemy import case
|
||||||
|
|
||||||
|
# 计算预警状态
|
||||||
|
warning_status = case(
|
||||||
|
(
|
||||||
|
(MaterialWarningSetting.is_enabled == True) &
|
||||||
|
(total_inv <= MaterialWarningSetting.red_threshold),
|
||||||
|
2 # 红色预警
|
||||||
|
),
|
||||||
|
(
|
||||||
|
(MaterialWarningSetting.is_enabled == True) &
|
||||||
|
(total_inv > MaterialWarningSetting.red_threshold) &
|
||||||
|
(total_inv <= MaterialWarningSetting.yellow_threshold),
|
||||||
|
1 # 黄色预警
|
||||||
|
),
|
||||||
|
else_=0 # 正常或未开启
|
||||||
|
).label('warning_status')
|
||||||
|
|
||||||
|
# 红色组内部:按缺口降序 (red_threshold - inventory)
|
||||||
|
red_gap = case(
|
||||||
|
(
|
||||||
|
(MaterialWarningSetting.is_enabled == True) &
|
||||||
|
(total_inv <= MaterialWarningSetting.red_threshold),
|
||||||
|
MaterialWarningSetting.red_threshold - total_inv
|
||||||
|
),
|
||||||
|
else_=0
|
||||||
|
).label('red_gap')
|
||||||
|
|
||||||
|
# 黄色组内部:按余量升序 (inventory - red_threshold),离红线越近越靠前
|
||||||
|
yellow_gap = case(
|
||||||
|
(
|
||||||
|
(MaterialWarningSetting.is_enabled == True) &
|
||||||
|
(total_inv > MaterialWarningSetting.red_threshold) &
|
||||||
|
(total_inv <= MaterialWarningSetting.yellow_threshold),
|
||||||
|
total_inv - MaterialWarningSetting.red_threshold
|
||||||
|
),
|
||||||
|
else_=999999 # 正常物料放在最后
|
||||||
|
).label('yellow_gap')
|
||||||
|
|
||||||
|
# 添加排序字段到查询
|
||||||
|
query = query.add_columns(warning_status, red_gap, yellow_gap)
|
||||||
|
|
||||||
|
# 强制排序规则:预警状态降序 -> 红色缺口降序 -> 黄色余量升序 -> 规格型号升序
|
||||||
|
query = query.order_by(
|
||||||
|
warning_status.desc(),
|
||||||
|
red_gap.desc(),
|
||||||
|
yellow_gap.asc(),
|
||||||
|
MaterialBase.spec_model.asc()
|
||||||
|
)
|
||||||
|
elif order_by_column:
|
||||||
# 字段映射
|
# 字段映射
|
||||||
sort_field_map = {
|
sort_field_map = {
|
||||||
'companyName': MaterialBase.company_name,
|
'companyName': MaterialBase.company_name,
|
||||||
@ -304,10 +378,47 @@ class MaterialBaseService:
|
|||||||
pagination = query.paginate(page=page, per_page=limit, error_out=False)
|
pagination = query.paginate(page=page, per_page=limit, error_out=False)
|
||||||
|
|
||||||
items_list = []
|
items_list = []
|
||||||
for item, inv, avail in pagination.items:
|
for row in pagination.items:
|
||||||
|
# 防弹解包逻辑:直接判断自身是否有 to_dict 方法
|
||||||
|
if hasattr(row, 'to_dict'):
|
||||||
|
# 说明查询只返回了 MaterialBase 单一对象
|
||||||
|
item = row
|
||||||
|
inv = avail = warning_enabled = warning_yellow = warning_red = None
|
||||||
|
else:
|
||||||
|
# 说明返回了 Row 对象 (包含多个字段)
|
||||||
|
item = row[0]
|
||||||
|
inv = row[1] if len(row) > 1 else None
|
||||||
|
avail = row[2] if len(row) > 2 else None
|
||||||
|
warning_enabled = row[3] if len(row) > 3 else False
|
||||||
|
warning_yellow = row[4] if len(row) > 4 else 0
|
||||||
|
warning_red = row[5] if len(row) > 5 else 0
|
||||||
|
|
||||||
|
# 安全兜底
|
||||||
|
if not hasattr(item, 'to_dict'):
|
||||||
|
continue
|
||||||
|
|
||||||
item_dict = item.to_dict()
|
item_dict = item.to_dict()
|
||||||
item_dict['inventoryCount'] = float(inv) if inv is not None else 0.0
|
item_dict['inventoryCount'] = float(inv) if inv is not None else 0
|
||||||
item_dict['availableCount'] = float(avail) if avail is not None else 0.0
|
item_dict['availableCount'] = float(avail) if avail is not None else 0
|
||||||
|
|
||||||
|
# 处理预警信息(仅当用户有权限时)
|
||||||
|
if has_warning_permission:
|
||||||
|
item_dict['warningEnabled'] = bool(warning_enabled) if warning_enabled is not None else False
|
||||||
|
item_dict['warningYellow'] = float(warning_yellow) if warning_yellow is not None else None
|
||||||
|
item_dict['warningRed'] = float(warning_red) if warning_red is not None else None
|
||||||
|
|
||||||
|
# 计算预警状态
|
||||||
|
if warning_enabled and warning_red is not None:
|
||||||
|
invQty = item_dict['inventoryCount']
|
||||||
|
if invQty <= warning_red:
|
||||||
|
item_dict['warningStatus'] = 2 # 红色
|
||||||
|
elif warning_yellow is not None and invQty <= warning_yellow:
|
||||||
|
item_dict['warningStatus'] = 1 # 黄色
|
||||||
|
else:
|
||||||
|
item_dict['warningStatus'] = 0 # 正常
|
||||||
|
else:
|
||||||
|
item_dict['warningStatus'] = 0 # 未开启或未设置
|
||||||
|
|
||||||
items_list.append(item_dict)
|
items_list.append(item_dict)
|
||||||
|
|
||||||
return {"total": pagination.total, "items": items_list}
|
return {"total": pagination.total, "items": items_list}
|
||||||
|
|||||||
@ -95,7 +95,45 @@ def audit_log(module: str, action: str = None, get_target_id_fn=None, get_target
|
|||||||
审计日志装饰器
|
审计日志装饰器
|
||||||
用法: @audit_log(module='inbound_buy', action='create')
|
用法: @audit_log(module='inbound_buy', action='create')
|
||||||
@audit_log(module='bom', action='update', get_target_id_fn=lambda: ..., get_details_fn=lambda req, resp: ...)
|
@audit_log(module='bom', action='update', get_target_id_fn=lambda: ..., get_details_fn=lambda req, resp: ...)
|
||||||
|
|
||||||
|
升级特性:
|
||||||
|
- 自动捕获请求 Payload 作为变更明细
|
||||||
|
- 自动过滤过长的 Base64 图片数据
|
||||||
|
- 支持自定义 get_details_fn 覆盖默认行为
|
||||||
"""
|
"""
|
||||||
|
# 需要过滤的图片字段
|
||||||
|
IMAGE_FIELDS = {'arrival_photo', 'product_photo', 'photo', 'image', 'signature', 'borrow_signature', 'return_signature'}
|
||||||
|
|
||||||
|
def _filter_payload(payload):
|
||||||
|
"""过滤 Payload 中的大字段,防止数据库膨胀"""
|
||||||
|
if not payload or not isinstance(payload, dict):
|
||||||
|
return payload
|
||||||
|
filtered = {}
|
||||||
|
for key, value in payload.items():
|
||||||
|
if key.lower() in IMAGE_FIELDS and isinstance(value, str) and len(value) > 100:
|
||||||
|
filtered[key] = '[图片数据已省略]'
|
||||||
|
elif isinstance(value, dict):
|
||||||
|
filtered[key] = _filter_payload(value)
|
||||||
|
elif isinstance(value, list):
|
||||||
|
filtered[key] = [
|
||||||
|
_filter_payload(item) if isinstance(item, dict) else item
|
||||||
|
for item in value
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
filtered[key] = value
|
||||||
|
return filtered
|
||||||
|
|
||||||
|
def _get_payload():
|
||||||
|
"""自动获取请求 Payload"""
|
||||||
|
# 尝试 JSON
|
||||||
|
payload = request.get_json(silent=True)
|
||||||
|
if payload:
|
||||||
|
return payload
|
||||||
|
# 尝试 Form Data
|
||||||
|
if request.form:
|
||||||
|
return request.form.to_dict()
|
||||||
|
return None
|
||||||
|
|
||||||
def wrapper(fn):
|
def wrapper(fn):
|
||||||
@wraps(fn)
|
@wraps(fn)
|
||||||
def decorator(*args, **kwargs):
|
def decorator(*args, **kwargs):
|
||||||
@ -109,7 +147,7 @@ def audit_log(module: str, action: str = None, get_target_id_fn=None, get_target
|
|||||||
ip_address = request.headers.get('X-Forwarded-For') or request.remote_addr or ''
|
ip_address = request.headers.get('X-Forwarded-For') or request.remote_addr or ''
|
||||||
if ip_address and ',' in ip_address:
|
if ip_address and ',' in ip_address:
|
||||||
ip_address = ip_address.split(',')[0].strip()
|
ip_address = ip_address.split(',')[0].strip()
|
||||||
|
|
||||||
# 获取请求信息
|
# 获取请求信息
|
||||||
http_method = request.method
|
http_method = request.method
|
||||||
url = request.url
|
url = request.url
|
||||||
@ -120,6 +158,10 @@ def audit_log(module: str, action: str = None, get_target_id_fn=None, get_target
|
|||||||
if callable(action):
|
if callable(action):
|
||||||
final_action = action()
|
final_action = action()
|
||||||
|
|
||||||
|
# 预先获取 Payload(用于后续 details 记录)
|
||||||
|
raw_payload = _get_payload()
|
||||||
|
filtered_payload = _filter_payload(raw_payload) if raw_payload else None
|
||||||
|
|
||||||
# 执行原函数
|
# 执行原函数
|
||||||
response = fn(*args, **kwargs)
|
response = fn(*args, **kwargs)
|
||||||
|
|
||||||
@ -157,10 +199,14 @@ def audit_log(module: str, action: str = None, get_target_id_fn=None, get_target
|
|||||||
# 获取 details
|
# 获取 details
|
||||||
details = None
|
details = None
|
||||||
if get_details_fn:
|
if get_details_fn:
|
||||||
|
# 优先使用自定义差异对比函数
|
||||||
try:
|
try:
|
||||||
details = get_details_fn(request, response)
|
details = get_details_fn(request, response)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
elif filtered_payload:
|
||||||
|
# 默认:记录请求 Payload
|
||||||
|
details = {'payload': filtered_payload}
|
||||||
|
|
||||||
# 保存日志
|
# 保存日志
|
||||||
log_entry = AuditLog(
|
log_entry = AuditLog(
|
||||||
|
|||||||
@ -51,4 +51,13 @@ export function delMaterialBase(id: number) {
|
|||||||
url: `/inbound/base/${id}`,
|
url: `/inbound/base/${id}`,
|
||||||
method: 'delete'
|
method: 'delete'
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 批量设置预警
|
||||||
|
export function batchSetWarning(data: any[]) {
|
||||||
|
return request({
|
||||||
|
url: '/inbound/base/warning/batch-set',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
}
|
}
|
||||||
@ -172,6 +172,19 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: 'OpRecords',
|
name: 'OpRecords',
|
||||||
component: () => import('@/views/transaction/records.vue'),
|
component: () => import('@/views/transaction/records.vue'),
|
||||||
meta: { title: '借还记录' }
|
meta: { title: '借还记录' }
|
||||||
|
},
|
||||||
|
// ★ [新增] 报废管理
|
||||||
|
{
|
||||||
|
path: 'scrap/index',
|
||||||
|
name: 'ScrapList',
|
||||||
|
component: () => import('@/views/operation/scrap/index.vue'), // ✅ 正确路径
|
||||||
|
meta: { title: '报废记录' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'scrap/create',
|
||||||
|
name: 'ScrapCreate',
|
||||||
|
component: () => import('@/views/operation/scrap/create.vue'), // ✅ 正确路径
|
||||||
|
meta: { title: '新建报废' }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@ -98,6 +98,17 @@
|
|||||||
<el-icon style="margin-right: 5px"><Download /></el-icon>导出库存统计
|
<el-icon style="margin-right: 5px"><Download /></el-icon>导出库存统计
|
||||||
</el-button>
|
</el-button>
|
||||||
|
|
||||||
|
<!-- 批量设置预警按钮 (需要 edit_warning 权限) -->
|
||||||
|
<el-button
|
||||||
|
v-if="userStore.hasPermission('material_list:edit_warning')"
|
||||||
|
type="warning"
|
||||||
|
plain
|
||||||
|
@click="handleBatchSetWarning"
|
||||||
|
style="margin-right: 10px"
|
||||||
|
>
|
||||||
|
<el-icon style="margin-right: 5px"><Bell /></el-icon>批量设置预警
|
||||||
|
</el-button>
|
||||||
|
|
||||||
<el-button v-if="userStore.hasPermission('material_list:operation')" type="primary" @click="handleAdd" style="margin-right: 10px">
|
<el-button v-if="userStore.hasPermission('material_list:operation')" type="primary" @click="handleAdd" style="margin-right: 10px">
|
||||||
<el-icon style="margin-right: 5px"><Plus /></el-icon>新增
|
<el-icon style="margin-right: 5px"><Plus /></el-icon>新增
|
||||||
</el-button>
|
</el-button>
|
||||||
@ -148,6 +159,7 @@
|
|||||||
border
|
border
|
||||||
stripe
|
stripe
|
||||||
:size="tableSize"
|
:size="tableSize"
|
||||||
|
:row-class-name="tableRowClassName"
|
||||||
@sort-change="handleSortChange"
|
@sort-change="handleSortChange"
|
||||||
style="width: 100%; margin-top: 15px"
|
style="width: 100%; margin-top: 15px"
|
||||||
>
|
>
|
||||||
@ -243,9 +255,10 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column v-if="userStore.hasPermission('material_list:operation')" label="操作" min-width="150" fixed="right" align="center">
|
<el-table-column v-if="userStore.hasPermission('material_list:operation')" label="操作" min-width="200" fixed="right" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button v-if="userStore.hasPermission('material_list:operation')" link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
|
<el-button v-if="userStore.hasPermission('material_list:operation')" link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
|
||||||
|
<el-button v-if="userStore.hasPermission('material_list:edit_warning')" link type="warning" size="small" @click="handleSetSingleWarning(scope.row)">设置预警</el-button>
|
||||||
<el-button v-if="userStore.hasPermission('material_list:operation')" link type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
|
<el-button v-if="userStore.hasPermission('material_list:operation')" link type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@ -440,13 +453,43 @@
|
|||||||
@cancel="cameraDialogVisible = false"
|
@cancel="cameraDialogVisible = false"
|
||||||
/>
|
/>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 预警设置弹窗 -->
|
||||||
|
<el-dialog v-model="warningDialog.visible" :title="warningDialog.title" width="500px" append-to-body destroy-on-close>
|
||||||
|
<el-form ref="warningFormRef" :model="warningForm" :rules="warningRules" label-width="100px">
|
||||||
|
<el-alert
|
||||||
|
v-if="warningDialog.selectedCount > 1"
|
||||||
|
:title="`正在批量设置 ${warningDialog.selectedCount} 条物料的预警`"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
style="margin-bottom: 15px"
|
||||||
|
/>
|
||||||
|
<el-form-item label="启用预警" prop="isEnabled">
|
||||||
|
<el-switch v-model="warningForm.isEnabled" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="红色阈值" prop="redThreshold" v-if="warningForm.isEnabled">
|
||||||
|
<el-input-number v-model="warningForm.redThreshold" :min="0" :precision="2" placeholder="库存≤此值为红色预警" style="width: 100%" />
|
||||||
|
<div class="form-tip">库存数量 ≤ 此值时显示红色预警</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="黄色阈值" prop="yellowThreshold" v-if="warningForm.isEnabled">
|
||||||
|
<el-input-number v-model="warningForm.yellowThreshold" :min="0" :precision="2" placeholder="库存≤此值为黄色预警" style="width: 100%" />
|
||||||
|
<div class="form-tip">红色阈值 < 库存 ≤ 此值时显示黄色预警</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="warningDialog.visible = false">取 消</el-button>
|
||||||
|
<el-button type="primary" @click="submitWarning" :loading="warningLoading">确 定</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, nextTick, computed } from 'vue';
|
import { ref, reactive, onMounted, nextTick, computed } from 'vue';
|
||||||
import { Plus, Document, Refresh, Setting, Rank, Camera, Link, Download } from '@element-plus/icons-vue';
|
import { Plus, Document, Refresh, Setting, Rank, Camera, Link, Download, Bell } from '@element-plus/icons-vue';
|
||||||
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus';
|
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus';
|
||||||
import type { FormInstance, FormRules } from 'element-plus';
|
import type { FormInstance, FormRules } from 'element-plus';
|
||||||
import { useUserStore } from '@/stores/user';
|
import { useUserStore } from '@/stores/user';
|
||||||
@ -457,7 +500,8 @@ import {
|
|||||||
updateMaterialBase,
|
updateMaterialBase,
|
||||||
delMaterialBase,
|
delMaterialBase,
|
||||||
getMaterialBaseOptions,
|
getMaterialBaseOptions,
|
||||||
exportAssetStatistics // 导入导出API
|
exportAssetStatistics,
|
||||||
|
batchSetWarning
|
||||||
} from '@/api/material_base';
|
} from '@/api/material_base';
|
||||||
import { uploadFile, deleteFile } from '@/api/common/upload';
|
import { uploadFile, deleteFile } from '@/api/common/upload';
|
||||||
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue';
|
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue';
|
||||||
@ -545,6 +589,39 @@ const cameraDialogVisible = ref(false);
|
|||||||
const cameraRef = ref<InstanceType<typeof WebRtcCamera> | null>(null);
|
const cameraRef = ref<InstanceType<typeof WebRtcCamera> | null>(null);
|
||||||
const currentCameraField = ref<'generalImage' | 'generalManual'>('generalImage');
|
const currentCameraField = ref<'generalImage' | 'generalManual'>('generalImage');
|
||||||
|
|
||||||
|
// 预警设置相关
|
||||||
|
const warningDialog = reactive({
|
||||||
|
visible: false,
|
||||||
|
title: '设置预警',
|
||||||
|
selectedCount: 0,
|
||||||
|
selectedIds: [] as number[]
|
||||||
|
});
|
||||||
|
const warningFormRef = ref<FormInstance>();
|
||||||
|
const warningLoading = ref(false);
|
||||||
|
const warningForm = reactive({
|
||||||
|
isEnabled: false,
|
||||||
|
redThreshold: undefined as number | undefined,
|
||||||
|
yellowThreshold: undefined as number | undefined
|
||||||
|
});
|
||||||
|
const warningRules = {
|
||||||
|
yellowThreshold: [
|
||||||
|
{
|
||||||
|
validator: (rule: any, value: any, callback: any) => {
|
||||||
|
if (warningForm.isEnabled && warningForm.redThreshold !== undefined && value !== undefined) {
|
||||||
|
if (value <= warningForm.redThreshold) {
|
||||||
|
callback(new Error('黄色阈值必须大于红色阈值'));
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
const columns = reactive({
|
const columns = reactive({
|
||||||
id: { visible: false },
|
id: { visible: false },
|
||||||
companyName: { visible: true },
|
companyName: { visible: true },
|
||||||
@ -1017,6 +1094,83 @@ const handleDelete = (row: MaterialBaseVO) => {
|
|||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- 预警设置函数 ---
|
||||||
|
|
||||||
|
// 批量设置预警
|
||||||
|
const handleBatchSetWarning = () => {
|
||||||
|
// 获取已选择的行(如果有选中的行则使用选中的行,否则使用当前页所有行)
|
||||||
|
const selectedRows = tableData.value.filter((row: MaterialBaseVO) => (row as any)._checked);
|
||||||
|
if (selectedRows.length > 0) {
|
||||||
|
warningDialog.selectedIds = selectedRows.map((row: MaterialBaseVO) => row.id);
|
||||||
|
warningDialog.selectedCount = selectedRows.length;
|
||||||
|
} else {
|
||||||
|
warningDialog.selectedIds = tableData.value.map((row: MaterialBaseVO) => row.id);
|
||||||
|
warningDialog.selectedCount = tableData.value.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
warningForm.isEnabled = false;
|
||||||
|
warningForm.redThreshold = undefined;
|
||||||
|
warningForm.yellowThreshold = undefined;
|
||||||
|
|
||||||
|
warningDialog.title = '批量设置预警';
|
||||||
|
warningDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 单条设置预警
|
||||||
|
const handleSetSingleWarning = (row: MaterialBaseVO) => {
|
||||||
|
warningDialog.selectedIds = [row.id];
|
||||||
|
warningDialog.selectedCount = 1;
|
||||||
|
|
||||||
|
// 如果已有预警设置则回显
|
||||||
|
warningForm.isEnabled = row.warningEnabled || false;
|
||||||
|
warningForm.redThreshold = row.warningRed;
|
||||||
|
warningForm.yellowThreshold = row.warningYellow;
|
||||||
|
|
||||||
|
warningDialog.title = '设置预警';
|
||||||
|
warningDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交预警设置
|
||||||
|
const submitWarning = async () => {
|
||||||
|
if (!warningFormRef.value) return;
|
||||||
|
|
||||||
|
await warningFormRef.value.validate();
|
||||||
|
|
||||||
|
warningLoading.value = true;
|
||||||
|
try {
|
||||||
|
const data = warningDialog.selectedIds.map(baseId => ({
|
||||||
|
baseId,
|
||||||
|
isEnabled: warningForm.isEnabled,
|
||||||
|
redThreshold: warningForm.redThreshold,
|
||||||
|
yellowThreshold: warningForm.yellowThreshold
|
||||||
|
}));
|
||||||
|
|
||||||
|
await batchSetWarning(data);
|
||||||
|
ElMessage.success('预警设置成功');
|
||||||
|
warningDialog.visible = false;
|
||||||
|
getList();
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error(error?.msg || '设置失败');
|
||||||
|
} finally {
|
||||||
|
warningLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 表格行样式(根据预警状态)
|
||||||
|
const tableRowClassName = ({ row }: { row: MaterialBaseVO }) => {
|
||||||
|
// 只有拥有 view_warning 权限且有预警状态时才显示特殊样式
|
||||||
|
if (!userStore.hasPermission('material_list:view_warning')) return '';
|
||||||
|
|
||||||
|
const status = (row as any).warningStatus;
|
||||||
|
if (status === 2) {
|
||||||
|
return 'warning-row-red'; // 红色预警
|
||||||
|
} else if (status === 1) {
|
||||||
|
return 'warning-row-yellow'; // 黄色预警
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
// --- 文件上传辅助函数 ---
|
// --- 文件上传辅助函数 ---
|
||||||
|
|
||||||
const getImageUrl = (url: string) => { return !url ? '' : (url.startsWith('http') ? url : url) }
|
const getImageUrl = (url: string) => { return !url ? '' : (url.startsWith('http') ? url : url) }
|
||||||
@ -1189,6 +1343,27 @@ onMounted(() => {
|
|||||||
.file-preview-cell { display: flex; align-items: center; justify-content: center; position: relative; }
|
.file-preview-cell { display: flex; align-items: center; justify-content: center; position: relative; }
|
||||||
.more-badge { position: absolute; top: -5px; right: -5px; background: #909399; color: #fff; border-radius: 10px; padding: 0 4px; font-size: 10px; transform: scale(0.9); }
|
.more-badge { position: absolute; top: -5px; right: -5px; background: #909399; color: #fff; border-radius: 10px; padding: 0 4px; font-size: 10px; transform: scale(0.9); }
|
||||||
|
|
||||||
|
/* 预警行样式 */
|
||||||
|
:deep(.warning-row-red) {
|
||||||
|
background-color: rgba(245, 108, 108, 0.15) !important;
|
||||||
|
}
|
||||||
|
:deep(.warning-row-red td) {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
:deep(.warning-row-yellow) {
|
||||||
|
background-color: rgba(230, 162, 60, 0.15) !important;
|
||||||
|
}
|
||||||
|
:deep(.warning-row-yellow td) {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单提示文字 */
|
||||||
|
.form-tip {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
margin-top: 4px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
Reference in New Issue
Block a user