将半成品成品同样进行新增所属公司以及内容修改
This commit is contained in:
@ -3,6 +3,7 @@ from app.extensions import db
|
|||||||
import json
|
import json
|
||||||
from app.models.base import MaterialBase
|
from app.models.base import MaterialBase
|
||||||
|
|
||||||
|
|
||||||
class StockProduct(db.Model):
|
class StockProduct(db.Model):
|
||||||
"""
|
"""
|
||||||
成品入库库存表
|
成品入库库存表
|
||||||
@ -44,7 +45,7 @@ class StockProduct(db.Model):
|
|||||||
quality_report_link = db.Column(db.Text) # 质量报告
|
quality_report_link = db.Column(db.Text) # 质量报告
|
||||||
inspection_report_link = db.Column(db.Text) # 检测报告(旧字段升级为JSON)
|
inspection_report_link = db.Column(db.Text) # 检测报告(旧字段升级为JSON)
|
||||||
|
|
||||||
# [新增] 成品实拍图 (JSON 存储)
|
# 成品实拍图 (JSON 存储)
|
||||||
product_photo = db.Column(db.Text)
|
product_photo = db.Column(db.Text)
|
||||||
|
|
||||||
detail_link = db.Column(db.Text)
|
detail_link = db.Column(db.Text)
|
||||||
@ -57,7 +58,7 @@ class StockProduct(db.Model):
|
|||||||
# 全局打印流水号
|
# 全局打印流水号
|
||||||
global_print_id = db.Column(db.Integer)
|
global_print_id = db.Column(db.Integer)
|
||||||
|
|
||||||
# 关系定义 [已修改]
|
# 关系定义
|
||||||
base = db.relationship('MaterialBase', back_populates='stock_products')
|
base = db.relationship('MaterialBase', back_populates='stock_products')
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
@ -79,7 +80,9 @@ class StockProduct(db.Model):
|
|||||||
return {
|
return {
|
||||||
'id': self.id,
|
'id': self.id,
|
||||||
'base_id': self.base_id,
|
'base_id': self.base_id,
|
||||||
# [已修改] 使用 self.base
|
|
||||||
|
# [新增] 公司名称
|
||||||
|
'company_name': self.base.company_name if self.base else '',
|
||||||
'material_name': self.base.name if self.base else '',
|
'material_name': self.base.name if self.base else '',
|
||||||
'spec_model': self.base.spec_model if self.base else '',
|
'spec_model': self.base.spec_model if self.base else '',
|
||||||
'category': self.base.category if self.base else '',
|
'category': self.base.category if self.base else '',
|
||||||
@ -115,7 +118,6 @@ class StockProduct(db.Model):
|
|||||||
|
|
||||||
'quality_status': self.quality_status,
|
'quality_status': self.quality_status,
|
||||||
|
|
||||||
# [核心修改] 三个图片/链接字段全部解析为数组
|
|
||||||
'product_photo': parse_img_list(self.product_photo),
|
'product_photo': parse_img_list(self.product_photo),
|
||||||
'quality_report_link': parse_img_list(self.quality_report_link),
|
'quality_report_link': parse_img_list(self.quality_report_link),
|
||||||
'inspection_report_link': parse_img_list(self.inspection_report_link),
|
'inspection_report_link': parse_img_list(self.inspection_report_link),
|
||||||
|
|||||||
@ -3,6 +3,7 @@ from app.extensions import db
|
|||||||
import json
|
import json
|
||||||
from app.models.base import MaterialBase
|
from app.models.base import MaterialBase
|
||||||
|
|
||||||
|
|
||||||
class StockSemi(db.Model):
|
class StockSemi(db.Model):
|
||||||
"""
|
"""
|
||||||
半成品入库库存表
|
半成品入库库存表
|
||||||
@ -43,19 +44,19 @@ class StockSemi(db.Model):
|
|||||||
|
|
||||||
quality_status = db.Column(db.String(50))
|
quality_status = db.Column(db.String(50))
|
||||||
|
|
||||||
# [修改] 质量报告 (存储 JSON 字符串: 图片列表 + 链接)
|
# 质量报告 (存储 JSON 字符串: 图片列表 + 链接)
|
||||||
quality_report_link = db.Column(db.Text)
|
quality_report_link = db.Column(db.Text)
|
||||||
|
|
||||||
# [新增] 到货图片 (存储 JSON 字符串)
|
# 到货图片 (存储 JSON 字符串)
|
||||||
arrival_photo = db.Column(db.Text)
|
arrival_photo = db.Column(db.Text)
|
||||||
|
|
||||||
detail_link = db.Column(db.Text)
|
detail_link = db.Column(db.Text)
|
||||||
remark = db.Column(db.Text)
|
remark = db.Column(db.Text)
|
||||||
|
|
||||||
# [新增] 全局打印流水号
|
# 全局打印流水号
|
||||||
global_print_id = db.Column(db.Integer)
|
global_print_id = db.Column(db.Integer)
|
||||||
|
|
||||||
# 关系定义 [已修改]
|
# 关系定义
|
||||||
base = db.relationship('MaterialBase', back_populates='stock_semis')
|
base = db.relationship('MaterialBase', back_populates='stock_semis')
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
@ -78,7 +79,9 @@ class StockSemi(db.Model):
|
|||||||
return {
|
return {
|
||||||
'id': self.id,
|
'id': self.id,
|
||||||
'base_id': self.base_id,
|
'base_id': self.base_id,
|
||||||
# [已修改] 使用 self.base
|
|
||||||
|
# [新增] 公司名称
|
||||||
|
'company_name': self.base.company_name if self.base else '',
|
||||||
'material_name': self.base.name if self.base else '',
|
'material_name': self.base.name if self.base else '',
|
||||||
'spec_model': self.base.spec_model if self.base else '',
|
'spec_model': self.base.spec_model if self.base else '',
|
||||||
'category': self.base.category if self.base else '',
|
'category': self.base.category if self.base else '',
|
||||||
@ -115,7 +118,6 @@ class StockSemi(db.Model):
|
|||||||
|
|
||||||
'quality_status': self.quality_status,
|
'quality_status': self.quality_status,
|
||||||
|
|
||||||
# [修改] 解析 JSON 字符串为数组返回给前端
|
|
||||||
'quality_report_link': parse_img_list(self.quality_report_link),
|
'quality_report_link': parse_img_list(self.quality_report_link),
|
||||||
'arrival_photo': parse_img_list(self.arrival_photo),
|
'arrival_photo': parse_img_list(self.arrival_photo),
|
||||||
|
|
||||||
|
|||||||
@ -33,10 +33,12 @@ class ProductInboundService:
|
|||||||
try:
|
try:
|
||||||
query = MaterialBase.query.filter(MaterialBase.is_enabled == True)
|
query = MaterialBase.query.filter(MaterialBase.is_enabled == True)
|
||||||
if keyword:
|
if keyword:
|
||||||
|
kw = f'%{keyword}%'
|
||||||
query = query.filter(
|
query = query.filter(
|
||||||
or_(
|
or_(
|
||||||
MaterialBase.name.ilike(f'%{keyword}%'),
|
MaterialBase.name.ilike(kw),
|
||||||
MaterialBase.spec_model.ilike(f'%{keyword}%')
|
MaterialBase.spec_model.ilike(kw),
|
||||||
|
MaterialBase.company_name.ilike(kw) # [新增]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
query = query.order_by(MaterialBase.id.desc()).limit(20)
|
query = query.order_by(MaterialBase.id.desc()).limit(20)
|
||||||
@ -44,6 +46,7 @@ class ProductInboundService:
|
|||||||
for item in query.all():
|
for item in query.all():
|
||||||
results.append({
|
results.append({
|
||||||
'id': item.id,
|
'id': item.id,
|
||||||
|
'company_name': item.company_name, # [新增]
|
||||||
'name': item.name,
|
'name': item.name,
|
||||||
'spec': item.spec_model,
|
'spec': item.spec_model,
|
||||||
'category': item.category,
|
'category': item.category,
|
||||||
@ -57,13 +60,12 @@ class ProductInboundService:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 1.5 [新增] BOM 搜索逻辑
|
# 1.5 BOM 搜索逻辑
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def search_bom_options(keyword):
|
def search_bom_options(keyword):
|
||||||
from app.models.bom import BomTable
|
from app.models.bom import BomTable
|
||||||
try:
|
try:
|
||||||
# 关联查询:BOM表 + 父件基础信息表
|
|
||||||
query = db.session.query(
|
query = db.session.query(
|
||||||
BomTable.bom_no,
|
BomTable.bom_no,
|
||||||
BomTable.version,
|
BomTable.version,
|
||||||
@ -71,13 +73,11 @@ class ProductInboundService:
|
|||||||
MaterialBase.spec_model.label('parent_spec')
|
MaterialBase.spec_model.label('parent_spec')
|
||||||
).join(MaterialBase, BomTable.parent_id == MaterialBase.id)
|
).join(MaterialBase, BomTable.parent_id == MaterialBase.id)
|
||||||
|
|
||||||
# 只查询启用的BOM
|
|
||||||
if hasattr(BomTable, 'is_enabled'):
|
if hasattr(BomTable, 'is_enabled'):
|
||||||
query = query.filter(BomTable.is_enabled == True)
|
query = query.filter(BomTable.is_enabled == True)
|
||||||
|
|
||||||
if keyword:
|
if keyword:
|
||||||
kw = f'%{keyword}%'
|
kw = f'%{keyword}%'
|
||||||
# 支持搜索:BOM编号、父件名称、父件规格
|
|
||||||
query = query.filter(
|
query = query.filter(
|
||||||
or_(
|
or_(
|
||||||
BomTable.bom_no.ilike(kw),
|
BomTable.bom_no.ilike(kw),
|
||||||
@ -86,7 +86,6 @@ class ProductInboundService:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 去重并限制数量
|
|
||||||
results = query.distinct().limit(20).all()
|
results = query.distinct().limit(20).all()
|
||||||
|
|
||||||
return [{
|
return [{
|
||||||
@ -283,23 +282,30 @@ class ProductInboundService:
|
|||||||
# 6. 获取列表
|
# 6. 获取列表
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_list(page, limit, keyword=None, statuses=None, category=None, material_type=None):
|
def get_list(page, limit, keyword=None, statuses=None, category=None, material_type=None, company=None):
|
||||||
from app.models.inbound.product import StockProduct
|
from app.models.inbound.product import StockProduct
|
||||||
try:
|
try:
|
||||||
query = db.session.query(StockProduct).outerjoin(MaterialBase, StockProduct.base_id == MaterialBase.id)
|
query = db.session.query(StockProduct).outerjoin(MaterialBase, StockProduct.base_id == MaterialBase.id)
|
||||||
if keyword:
|
if keyword:
|
||||||
|
kw = f'%{keyword}%'
|
||||||
query = query.filter(or_(
|
query = query.filter(or_(
|
||||||
MaterialBase.name.ilike(f'%{keyword}%'),
|
MaterialBase.name.ilike(kw),
|
||||||
MaterialBase.spec_model.ilike(f'%{keyword}%'),
|
MaterialBase.spec_model.ilike(kw),
|
||||||
StockProduct.serial_number.ilike(f'%{keyword}%'),
|
MaterialBase.company_name.ilike(kw), # [新增]
|
||||||
StockProduct.work_order_code.ilike(f'%{keyword}%'),
|
StockProduct.serial_number.ilike(kw),
|
||||||
StockProduct.order_id.ilike(f'%{keyword}%'),
|
StockProduct.work_order_code.ilike(kw),
|
||||||
StockProduct.sku.ilike(f'%{keyword}%')
|
StockProduct.order_id.ilike(kw),
|
||||||
|
StockProduct.sku.ilike(kw)
|
||||||
))
|
))
|
||||||
if category and category.strip():
|
if category and category.strip():
|
||||||
query = query.filter(MaterialBase.category == category.strip())
|
query = query.filter(MaterialBase.category == category.strip())
|
||||||
if material_type and material_type.strip():
|
if material_type and material_type.strip():
|
||||||
query = query.filter(MaterialBase.material_type == material_type.strip())
|
query = query.filter(MaterialBase.material_type == material_type.strip())
|
||||||
|
|
||||||
|
# [新增]
|
||||||
|
if company and company.strip():
|
||||||
|
query = query.filter(MaterialBase.company_name == company.strip())
|
||||||
|
|
||||||
if not statuses:
|
if not statuses:
|
||||||
statuses = ['在库', '借库']
|
statuses = ['在库', '借库']
|
||||||
if '已出库' in statuses:
|
if '已出库' in statuses:
|
||||||
@ -324,23 +330,7 @@ class ProductInboundService:
|
|||||||
|
|
||||||
items = []
|
items = []
|
||||||
for item in current_items:
|
for item in current_items:
|
||||||
d = item.to_dict()
|
items.append(item.to_dict()) # 使用 Model to_dict
|
||||||
date_display = ''
|
|
||||||
if item.production_date:
|
|
||||||
try:
|
|
||||||
date_display = item.production_date.strftime('%Y-%m-%d')
|
|
||||||
except:
|
|
||||||
date_display = str(item.production_date)[:10]
|
|
||||||
d['inbound_date'] = date_display
|
|
||||||
d['qty_stock'] = float(item.stock_quantity or 0)
|
|
||||||
d['qty_available'] = float(item.available_quantity or 0)
|
|
||||||
d['sum_stock'] = d['qty_stock']
|
|
||||||
d['sum_available'] = d['qty_available']
|
|
||||||
d['product_photo'] = parse_img(item.product_photo)
|
|
||||||
d['quality_report_link'] = parse_img(item.quality_report_link)
|
|
||||||
d['inspection_report_link'] = parse_img(item.inspection_report_link)
|
|
||||||
d['global_print_id'] = item.global_print_id
|
|
||||||
items.append(d)
|
|
||||||
return {"total": pagination.total, "items": items}
|
return {"total": pagination.total, "items": items}
|
||||||
except:
|
except:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
@ -368,21 +358,37 @@ class ProductInboundService:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 7. 获取筛选项
|
||||||
|
# ============================================================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_filter_options():
|
def get_filter_options():
|
||||||
try:
|
try:
|
||||||
from app.models.base import MaterialBase
|
from app.models.base import MaterialBase
|
||||||
|
# 类别
|
||||||
categories = db.session.query(MaterialBase.category) \
|
categories = db.session.query(MaterialBase.category) \
|
||||||
.filter(MaterialBase.category != None, MaterialBase.category != '') \
|
.filter(MaterialBase.category != None, MaterialBase.category != '') \
|
||||||
.distinct().all()
|
.distinct().all()
|
||||||
|
sorted_categories = sorted([r[0] for r in categories])
|
||||||
|
|
||||||
|
# 类型
|
||||||
types = db.session.query(MaterialBase.material_type) \
|
types = db.session.query(MaterialBase.material_type) \
|
||||||
.filter(MaterialBase.material_type != None, MaterialBase.material_type != '') \
|
.filter(MaterialBase.material_type != None, MaterialBase.material_type != '') \
|
||||||
.distinct().all()
|
.distinct().all()
|
||||||
|
sorted_types = sorted([r[0] for r in types])
|
||||||
|
|
||||||
|
# [新增] 公司
|
||||||
|
companies = db.session.query(MaterialBase.company_name) \
|
||||||
|
.filter(MaterialBase.company_name != None, MaterialBase.company_name != '') \
|
||||||
|
.distinct().all()
|
||||||
|
sorted_companies = sorted([r[0] for r in companies])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"categories": [r[0] for r in categories],
|
"categories": sorted_categories,
|
||||||
"types": [r[0] for r in types]
|
"types": sorted_types,
|
||||||
|
"companies": sorted_companies
|
||||||
}
|
}
|
||||||
except Exception:
|
except Exception:
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return {"categories": [], "types": []}
|
return {"categories": [], "types": [], "companies": []}
|
||||||
@ -43,10 +43,12 @@ class SemiInboundService:
|
|||||||
try:
|
try:
|
||||||
query = MaterialBase.query.filter(MaterialBase.is_enabled == True)
|
query = MaterialBase.query.filter(MaterialBase.is_enabled == True)
|
||||||
if keyword:
|
if keyword:
|
||||||
|
kw = f'%{keyword}%'
|
||||||
query = query.filter(
|
query = query.filter(
|
||||||
or_(
|
or_(
|
||||||
MaterialBase.name.ilike(f'%{keyword}%'),
|
MaterialBase.name.ilike(kw),
|
||||||
MaterialBase.spec_model.ilike(f'%{keyword}%')
|
MaterialBase.spec_model.ilike(kw),
|
||||||
|
MaterialBase.company_name.ilike(kw) # [新增] 支持搜公司
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
query = query.order_by(MaterialBase.id.desc()).limit(20)
|
query = query.order_by(MaterialBase.id.desc()).limit(20)
|
||||||
@ -54,6 +56,7 @@ class SemiInboundService:
|
|||||||
for item in query.all():
|
for item in query.all():
|
||||||
results.append({
|
results.append({
|
||||||
'id': item.id,
|
'id': item.id,
|
||||||
|
'company_name': item.company_name, # [新增]
|
||||||
'name': item.name,
|
'name': item.name,
|
||||||
'spec': item.spec_model,
|
'spec': item.spec_model,
|
||||||
'category': item.category,
|
'category': item.category,
|
||||||
@ -67,13 +70,12 @@ class SemiInboundService:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 1.5 [新增] BOM 搜索逻辑
|
# 1.5 BOM 搜索逻辑
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def search_bom_options(keyword):
|
def search_bom_options(keyword):
|
||||||
from app.models.bom import BomTable
|
from app.models.bom import BomTable
|
||||||
try:
|
try:
|
||||||
# 关联查询:BOM表 + 父件基础信息表
|
|
||||||
query = db.session.query(
|
query = db.session.query(
|
||||||
BomTable.bom_no,
|
BomTable.bom_no,
|
||||||
BomTable.version,
|
BomTable.version,
|
||||||
@ -81,13 +83,11 @@ class SemiInboundService:
|
|||||||
MaterialBase.spec_model.label('parent_spec')
|
MaterialBase.spec_model.label('parent_spec')
|
||||||
).join(MaterialBase, BomTable.parent_id == MaterialBase.id)
|
).join(MaterialBase, BomTable.parent_id == MaterialBase.id)
|
||||||
|
|
||||||
# 只查询启用的BOM
|
|
||||||
if hasattr(BomTable, 'is_enabled'):
|
if hasattr(BomTable, 'is_enabled'):
|
||||||
query = query.filter(BomTable.is_enabled == True)
|
query = query.filter(BomTable.is_enabled == True)
|
||||||
|
|
||||||
if keyword:
|
if keyword:
|
||||||
kw = f'%{keyword}%'
|
kw = f'%{keyword}%'
|
||||||
# 支持搜索:BOM编号、父件名称、父件规格
|
|
||||||
query = query.filter(
|
query = query.filter(
|
||||||
or_(
|
or_(
|
||||||
BomTable.bom_no.ilike(kw),
|
BomTable.bom_no.ilike(kw),
|
||||||
@ -96,7 +96,6 @@ class SemiInboundService:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 去重并限制数量
|
|
||||||
results = query.distinct().limit(20).all()
|
results = query.distinct().limit(20).all()
|
||||||
|
|
||||||
return [{
|
return [{
|
||||||
@ -367,7 +366,7 @@ class SemiInboundService:
|
|||||||
# 6. 获取列表
|
# 6. 获取列表
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_list(page, limit, keyword=None, statuses=None, category=None, material_type=None):
|
def get_list(page, limit, keyword=None, statuses=None, category=None, material_type=None, company=None):
|
||||||
from app.models.inbound.semi import StockSemi
|
from app.models.inbound.semi import StockSemi
|
||||||
try:
|
try:
|
||||||
query = db.session.query(StockSemi).outerjoin(MaterialBase, StockSemi.base_id == MaterialBase.id)
|
query = db.session.query(StockSemi).outerjoin(MaterialBase, StockSemi.base_id == MaterialBase.id)
|
||||||
@ -377,6 +376,7 @@ class SemiInboundService:
|
|||||||
or_(
|
or_(
|
||||||
MaterialBase.name.ilike(kw),
|
MaterialBase.name.ilike(kw),
|
||||||
MaterialBase.spec_model.ilike(kw),
|
MaterialBase.spec_model.ilike(kw),
|
||||||
|
MaterialBase.company_name.ilike(kw), # [新增]
|
||||||
StockSemi.batch_number.ilike(kw),
|
StockSemi.batch_number.ilike(kw),
|
||||||
StockSemi.serial_number.ilike(kw),
|
StockSemi.serial_number.ilike(kw),
|
||||||
StockSemi.sku.ilike(kw),
|
StockSemi.sku.ilike(kw),
|
||||||
@ -388,6 +388,11 @@ class SemiInboundService:
|
|||||||
query = query.filter(MaterialBase.category == category.strip())
|
query = query.filter(MaterialBase.category == category.strip())
|
||||||
if material_type and material_type.strip():
|
if material_type and material_type.strip():
|
||||||
query = query.filter(MaterialBase.material_type == material_type.strip())
|
query = query.filter(MaterialBase.material_type == material_type.strip())
|
||||||
|
|
||||||
|
# [新增] 公司筛选
|
||||||
|
if company and company.strip():
|
||||||
|
query = query.filter(MaterialBase.company_name == company.strip())
|
||||||
|
|
||||||
if not statuses:
|
if not statuses:
|
||||||
statuses = ['在库', '借库']
|
statuses = ['在库', '借库']
|
||||||
if '已出库' in statuses:
|
if '已出库' in statuses:
|
||||||
@ -412,22 +417,7 @@ class SemiInboundService:
|
|||||||
|
|
||||||
items = []
|
items = []
|
||||||
for item in current_items:
|
for item in current_items:
|
||||||
d = item.to_dict()
|
items.append(item.to_dict()) # 直接使用 Model 的 to_dict (已包含 company_name)
|
||||||
date_display = ''
|
|
||||||
if item.production_date:
|
|
||||||
try:
|
|
||||||
date_display = item.production_date.strftime('%Y-%m-%d')
|
|
||||||
except:
|
|
||||||
date_display = str(item.production_date)[:10]
|
|
||||||
d['inbound_date'] = date_display
|
|
||||||
d['qty_stock'] = float(item.stock_quantity or 0)
|
|
||||||
d['qty_available'] = float(item.available_quantity or 0)
|
|
||||||
d['sum_stock'] = d['qty_stock']
|
|
||||||
d['sum_available'] = d['qty_available']
|
|
||||||
d['arrival_photo'] = parse_img(item.arrival_photo)
|
|
||||||
d['quality_report_link'] = parse_img(item.quality_report_link)
|
|
||||||
d['global_print_id'] = item.global_print_id
|
|
||||||
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"List Error: {e}")
|
print(f"List Error: {e}")
|
||||||
@ -456,21 +446,37 @@ class SemiInboundService:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 7. 获取筛选项 (排序)
|
||||||
|
# ============================================================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_filter_options():
|
def get_filter_options():
|
||||||
try:
|
try:
|
||||||
from app.models.base import MaterialBase
|
from app.models.base import MaterialBase
|
||||||
|
# 类别
|
||||||
categories = db.session.query(MaterialBase.category) \
|
categories = db.session.query(MaterialBase.category) \
|
||||||
.filter(MaterialBase.category != None, MaterialBase.category != '') \
|
.filter(MaterialBase.category != None, MaterialBase.category != '') \
|
||||||
.distinct().all()
|
.distinct().all()
|
||||||
|
sorted_categories = sorted([r[0] for r in categories])
|
||||||
|
|
||||||
|
# 类型
|
||||||
types = db.session.query(MaterialBase.material_type) \
|
types = db.session.query(MaterialBase.material_type) \
|
||||||
.filter(MaterialBase.material_type != None, MaterialBase.material_type != '') \
|
.filter(MaterialBase.material_type != None, MaterialBase.material_type != '') \
|
||||||
.distinct().all()
|
.distinct().all()
|
||||||
|
sorted_types = sorted([r[0] for r in types])
|
||||||
|
|
||||||
|
# [新增] 公司
|
||||||
|
companies = db.session.query(MaterialBase.company_name) \
|
||||||
|
.filter(MaterialBase.company_name != None, MaterialBase.company_name != '') \
|
||||||
|
.distinct().all()
|
||||||
|
sorted_companies = sorted([r[0] for r in companies])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"categories": [r[0] for r in categories],
|
"categories": sorted_categories,
|
||||||
"types": [r[0] for r in types]
|
"types": sorted_types,
|
||||||
|
"companies": sorted_companies
|
||||||
}
|
}
|
||||||
except Exception:
|
except Exception:
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return {"categories": [], "types": []}
|
return {"categories": [], "types": [], "companies": []}
|
||||||
@ -105,11 +105,6 @@
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #default="scope" v-else-if="col.prop === 'company_name'">
|
|
||||||
<el-tag v-if="scope.row.company_name" type="info" effect="plain" size="small" style="font-weight: bold;">{{ scope.row.company_name }}</el-tag>
|
|
||||||
<span v-else>-</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #default="scope" v-else-if="col.prop === 'sn_bn'">
|
<template #default="scope" v-else-if="col.prop === 'sn_bn'">
|
||||||
<div v-if="scope.row.serial_number" class="id-cell">
|
<div v-if="scope.row.serial_number" class="id-cell">
|
||||||
<span class="prefix-tag sn">SN</span>
|
<span class="prefix-tag sn">SN</span>
|
||||||
|
|||||||
@ -2,16 +2,28 @@
|
|||||||
<div class="product-module">
|
<div class="product-module">
|
||||||
<div class="header-tools">
|
<div class="header-tools">
|
||||||
<div class="left-tools">
|
<div class="left-tools">
|
||||||
|
<el-select
|
||||||
|
v-model="queryParams.company"
|
||||||
|
placeholder="所属公司"
|
||||||
|
class="filter-item-select"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
@change="fetchData"
|
||||||
|
style="width: 160px;"
|
||||||
|
>
|
||||||
|
<el-option v-for="item in companyOptions" :key="item" :label="item" :value="item" />
|
||||||
|
</el-select>
|
||||||
|
|
||||||
<el-input
|
<el-input
|
||||||
v-model="queryParams.keyword"
|
v-model="queryParams.keyword"
|
||||||
placeholder="🔍 搜索物料 / SN / 工单 / 订单号..."
|
placeholder="🔍 搜索物料 / SN / 工单..."
|
||||||
class="search-input"
|
class="filter-item-input"
|
||||||
clearable
|
clearable
|
||||||
@clear="fetchData"
|
@clear="fetchData"
|
||||||
@keyup.enter="fetchData"
|
@keyup.enter="fetchData"
|
||||||
style="width: 300px; margin-right: 10px;"
|
style="width: 260px;"
|
||||||
>
|
>
|
||||||
<template #append><el-button :icon="Search" @click="fetchData" /></template>
|
<template #prefix><el-icon><Search /></el-icon></template>
|
||||||
</el-input>
|
</el-input>
|
||||||
|
|
||||||
<el-select
|
<el-select
|
||||||
@ -66,6 +78,11 @@
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #default="scope" v-else-if="col.prop === 'company_name'">
|
||||||
|
<el-tag v-if="scope.row.company_name" type="info" effect="plain" size="small" style="font-weight: bold;">{{ scope.row.company_name }}</el-tag>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #default="scope" v-else-if="['serial_number'].includes(col.prop)">
|
<template #default="scope" v-else-if="['serial_number'].includes(col.prop)">
|
||||||
<div v-if="scope.row.serial_number" class="id-cell">
|
<div v-if="scope.row.serial_number" class="id-cell">
|
||||||
<span class="prefix-tag sn">SN</span>
|
<span class="prefix-tag sn">SN</span>
|
||||||
@ -133,21 +150,27 @@
|
|||||||
|
|
||||||
<el-pagination class="pagination-bar" v-model:current-page="queryParams.page" v-model:page-size="queryParams.pageSize" :total="total" layout="total, sizes, prev, pager, next" background @change="fetchData" />
|
<el-pagination class="pagination-bar" v-model:current-page="queryParams.page" v-model:page-size="queryParams.pageSize" :total="total" layout="total, sizes, prev, pager, next" background @change="fetchData" />
|
||||||
|
|
||||||
<el-dialog v-model="visible" :title="dialogStatus === 'create' ? '成品入库' : '编辑成品'" width="1100px" top="5vh" :close-on-click-modal="false" class="stylish-dialog">
|
<el-dialog v-model="visible" :title="dialogStatus === 'create' ? '成品入库' : '编辑成品'" width="1100px" top="5vh" :close-on-click-modal="false" class="stylish-dialog compact-layout">
|
||||||
<div class="dialog-scroll-container">
|
<div class="dialog-scroll-container">
|
||||||
<el-form :model="form" label-width="110px" ref="formRef" :rules="rules" size="default" class="stylish-form">
|
<el-form :model="form" label-width="110px" ref="formRef" :rules="rules" size="default" class="stylish-form">
|
||||||
|
|
||||||
<div class="form-card basic-card">
|
<div class="form-card basic-card">
|
||||||
<div class="card-title"><el-icon class="icon"><Box /></el-icon><span>1. 基础信息</span></div>
|
<div class="card-title">
|
||||||
|
<div style="display: flex; align-items: center;">
|
||||||
|
<el-icon class="icon"><Box /></el-icon>
|
||||||
|
<span>1. 基础信息</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<el-row :gutter="24" v-if="dialogStatus === 'create'" style="margin-bottom: 20px;">
|
<el-row :gutter="24" v-if="dialogStatus === 'create'" style="margin-bottom: 20px;">
|
||||||
<el-col :span="10">
|
<el-col :span="10">
|
||||||
<el-form-item label="物料搜索" prop="base_id">
|
<el-form-item label="物料搜索" prop="base_id" class="highlight-label">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="form.base_id"
|
v-model="form.base_id"
|
||||||
filterable
|
filterable
|
||||||
remote
|
remote
|
||||||
reserve-keyword
|
reserve-keyword
|
||||||
|
clearable
|
||||||
placeholder="搜名称/规格..."
|
placeholder="搜名称/规格..."
|
||||||
:remote-method="handleSearchMaterial"
|
:remote-method="handleSearchMaterial"
|
||||||
@visible-change="handleMaterialDropdownVisible"
|
@visible-change="handleMaterialDropdownVisible"
|
||||||
@ -155,15 +178,28 @@
|
|||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
@change="onMaterialSelected"
|
@change="onMaterialSelected"
|
||||||
default-first-option
|
default-first-option
|
||||||
|
v-loadmore="loadMoreMaterials"
|
||||||
|
popper-class="long-dropdown"
|
||||||
>
|
>
|
||||||
|
<template #prefix><el-icon><Search /></el-icon></template>
|
||||||
<el-option v-for="item in materialOptions" :key="item.id" :label="item.name" :value="item.id">
|
<el-option v-for="item in materialOptions" :key="item.id" :label="item.name" :value="item.id">
|
||||||
<div class="option-item">
|
<div class="option-item">
|
||||||
<span class="opt-name">{{ item.name }}</span>
|
<div class="opt-main">
|
||||||
<span class="opt-spec">{{ item.spec }}</span>
|
<span class="opt-name" :title="item.name">{{ item.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="opt-meta">
|
||||||
|
<span class="opt-spec" :title="item.spec">{{ item.spec || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="opt-tags">
|
||||||
|
<el-tag size="small" type="info" effect="light" class="company-tag">{{ item.company_name }}</el-tag>
|
||||||
<el-tag v-if="item.isHistory" size="small" type="info" effect="plain">历史</el-tag>
|
<el-tag v-if="item.isHistory" size="small" type="info" effect="plain">历史</el-tag>
|
||||||
<el-tag v-else size="small" type="success" effect="plain">系统</el-tag>
|
<el-tag v-else size="small" type="success" effect="plain">系统</el-tag>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</el-option>
|
</el-option>
|
||||||
|
<div v-if="loadingMore" style="text-align: center; color: #999; font-size: 12px; padding: 8px; background: #f9f9f9;">
|
||||||
|
<el-icon class="is-loading"><Refresh /></el-icon> 加载更多中...
|
||||||
|
</div>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
@ -175,11 +211,12 @@
|
|||||||
</el-row>
|
</el-row>
|
||||||
<div class="read-only-grid">
|
<div class="read-only-grid">
|
||||||
<el-row :gutter="24">
|
<el-row :gutter="24">
|
||||||
<el-col :span="8"><el-form-item label="名称"><el-input v-model="form.material_name" disabled class="is-text-view" /></el-form-item></el-col>
|
<el-col :span="8"><el-form-item label="所属公司"><el-input v-model="form.company_name" readonly class="is-text-view" /></el-form-item></el-col>
|
||||||
<el-col :span="8"><el-form-item label="规格"><el-input v-model="form.spec_model" disabled class="is-text-view" /></el-form-item></el-col>
|
<el-col :span="8"><el-form-item label="名称"><el-input v-model="form.material_name" readonly class="is-text-view" /></el-form-item></el-col>
|
||||||
<el-col :span="8"><el-form-item label="单位"><el-input v-model="form.unit" disabled class="is-text-view" /></el-form-item></el-col>
|
<el-col :span="8"><el-form-item label="规格"><el-input v-model="form.spec_model" readonly class="is-text-view" /></el-form-item></el-col>
|
||||||
<el-col :span="8"><el-form-item label="类型"><el-input v-model="form.material_type" disabled class="is-text-view" /></el-form-item></el-col>
|
<el-col :span="8"><el-form-item label="单位"><el-input v-model="form.unit" readonly class="is-text-view" /></el-form-item></el-col>
|
||||||
<el-col :span="8"><el-form-item label="类别"><el-input v-model="form.category" disabled class="is-text-view" /></el-form-item></el-col>
|
<el-col :span="8"><el-form-item label="类型"><el-input v-model="form.material_type" readonly class="is-text-view" /></el-form-item></el-col>
|
||||||
|
<el-col :span="8"><el-form-item label="类别"><el-input v-model="form.category" readonly class="is-text-view" /></el-form-item></el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -190,8 +227,8 @@
|
|||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<el-row :gutter="24">
|
<el-row :gutter="24">
|
||||||
<el-col :span="6"><el-form-item label="SKU" prop="sku"><el-input v-model="form.sku" placeholder="自动生成" disabled /></el-form-item></el-col>
|
<el-col :span="6"><el-form-item label="SKU" prop="sku"><el-input v-model="form.sku" placeholder="自动生成" disabled /></el-form-item></el-col>
|
||||||
<el-col :span="6"><el-form-item label="条码" prop="barcode"><el-input v-model="form.barcode" placeholder="自动生成" /></el-form-item></el-col>
|
<el-col :span="6"><el-form-item label="条码" prop="barcode"><el-input v-model="form.barcode" placeholder="自动生成" clearable /></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-col :span="6"><el-form-item label="库位" prop="warehouse_location"><el-input v-model="form.warehouse_location" placeholder="例如: B-01-01" clearable /></el-form-item></el-col>
|
||||||
<el-col :span="6"><el-form-item label="入库日期"><el-date-picker v-model="form.in_date" type="date" value-format="YYYY-MM-DD" style="width:100%" disabled /></el-form-item></el-col>
|
<el-col :span="6"><el-form-item label="入库日期"><el-date-picker v-model="form.in_date" type="date" value-format="YYYY-MM-DD" style="width:100%" disabled /></el-form-item></el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
@ -389,22 +426,53 @@ import {
|
|||||||
updateProductInbound,
|
updateProductInbound,
|
||||||
deleteProductInbound,
|
deleteProductInbound,
|
||||||
searchMaterialBase,
|
searchMaterialBase,
|
||||||
searchBom // [新增]
|
searchBom,
|
||||||
|
getFilterOptions // [新增]
|
||||||
} from '@/api/inbound/product'
|
} from '@/api/inbound/product'
|
||||||
import { uploadFile, deleteFile } from '@/api/inbound/buy'
|
import { uploadFile, deleteFile } from '@/api/inbound/buy'
|
||||||
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
|
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
|
||||||
import { getLabelPreview, executePrint } from '@/api/common/print'
|
import { getLabelPreview, executePrint } from '@/api/common/print'
|
||||||
|
|
||||||
|
// ------------------------------------
|
||||||
|
// v-loadmore
|
||||||
|
// ------------------------------------
|
||||||
|
const vLoadmore = {
|
||||||
|
mounted(el: any, binding: any) {
|
||||||
|
const checkAndBind = () => {
|
||||||
|
const dropDownWrap = document.querySelector('.long-dropdown .el-select-dropdown__wrap')
|
||||||
|
if (dropDownWrap && !dropDownWrap.getAttribute('data-loadmore-bound')) {
|
||||||
|
dropDownWrap.setAttribute('data-loadmore-bound', 'true')
|
||||||
|
dropDownWrap.addEventListener('scroll', function (this: any) {
|
||||||
|
const condition = this.scrollHeight - this.scrollTop <= this.clientHeight + 1
|
||||||
|
if (condition) {
|
||||||
|
binding.value()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTimeout(checkAndBind, 500)
|
||||||
|
el.addEventListener('click', () => setTimeout(checkAndBind, 300))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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 searchLoading = ref(false)
|
||||||
|
const loadingMore = 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: 100, keyword: '', statuses: ['在库', '借库'] })
|
const queryParams = reactive({ page: 1, pageSize: 100, keyword: '', statuses: ['在库', '借库'], company: '' })
|
||||||
|
const categoryOptions = ref<string[]>([])
|
||||||
|
const typeOptions = ref<string[]>([])
|
||||||
|
const companyOptions = ref<string[]>([]) // [新增]
|
||||||
const materialOptions = ref<any[]>([])
|
const materialOptions = ref<any[]>([])
|
||||||
|
const searchPage = ref(1)
|
||||||
|
const searchKeyword = ref('')
|
||||||
|
const hasNextPage = ref(true)
|
||||||
|
let searchTimer: any = null
|
||||||
|
|
||||||
// BOM 搜索相关
|
// BOM 搜索相关
|
||||||
const bomSearchLoading = ref(false)
|
const bomSearchLoading = ref(false)
|
||||||
@ -432,6 +500,7 @@ const inspection_url = ref('')
|
|||||||
|
|
||||||
// [核心优化] 所有列定义
|
// [核心优化] 所有列定义
|
||||||
const allColumns = [
|
const allColumns = [
|
||||||
|
{ prop: 'company_name', label: '所属公司', minWidth: '100' }, // [新增]
|
||||||
{ prop: 'material_name', label: '名称', minWidth: '140' },
|
{ prop: 'material_name', label: '名称', minWidth: '140' },
|
||||||
{ prop: 'sku', label: 'SKU', minWidth: '110' },
|
{ prop: 'sku', label: 'SKU', minWidth: '110' },
|
||||||
{ prop: 'serial_number', label: '序列号', minWidth: '130' },
|
{ prop: 'serial_number', label: '序列号', minWidth: '130' },
|
||||||
@ -454,11 +523,13 @@ const allColumns = [
|
|||||||
{ prop: 'detail_link', label: '详情', minWidth: '100' }
|
{ prop: 'detail_link', label: '详情', minWidth: '100' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const defaultVisibleCols = ['material_name', 'sku', 'serial_number', 'qty_stock', 'status', 'quality_status', 'product_photo', 'sale_price', 'order_id']
|
const defaultVisibleCols = ['company_name', 'material_name', 'sku', 'serial_number', 'qty_stock', 'status', 'quality_status', 'product_photo', 'sale_price', 'order_id']
|
||||||
const visibleColumnProps = ref(defaultVisibleCols)
|
const visibleColumnProps = ref(defaultVisibleCols)
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
id: undefined, base_id: undefined, material_name: '', spec_model: '', material_type: '', category: '', unit: '',
|
id: undefined, base_id: undefined,
|
||||||
|
company_name: '', // [新增]
|
||||||
|
material_name: '', spec_model: '', material_type: '', category: '', unit: '',
|
||||||
sku: '', barcode: '', serial_number: '', in_date: '',
|
sku: '', barcode: '', serial_number: '', in_date: '',
|
||||||
in_quantity: 1, stock_quantity: 1, available_quantity: 1,
|
in_quantity: 1, stock_quantity: 1, available_quantity: 1,
|
||||||
warehouse_location: '', status: '在库', quality_status: '合格',
|
warehouse_location: '', status: '在库', quality_status: '合格',
|
||||||
@ -513,18 +584,53 @@ const rules = {
|
|||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
// Material Search & Population Logic
|
// Material Search & Population Logic
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
const handleMaterialDropdownVisible = (visible: boolean) => { if (visible && materialOptions.value.length === 0) handleSearchMaterial('') }
|
const handleMaterialDropdownVisible = (visible: boolean) => { if (visible && materialOptions.value.length === 0) handleSearchMaterialDebounced('') }
|
||||||
|
|
||||||
|
const handleSearchMaterialDebounced = (query: string) => {
|
||||||
|
if (searchTimer) clearTimeout(searchTimer)
|
||||||
|
searchTimer = setTimeout(() => {
|
||||||
|
handleSearchMaterial(query)
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
const handleSearchMaterial = async (query: string) => {
|
const handleSearchMaterial = async (query: string) => {
|
||||||
searchLoading.value = true
|
searchLoading.value = true
|
||||||
|
searchKeyword.value = query
|
||||||
|
searchPage.value = 1
|
||||||
|
materialOptions.value = []
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res: any = await searchMaterialBase(query)
|
const res: any = await searchMaterialBase(query, 1)
|
||||||
const apiResults = (res.data || []).map((i: any) => ({ ...i, isHistory: false }))
|
const apiResults = (res.data || []).map((i: any) => ({ ...i, isHistory: false }))
|
||||||
materialOptions.value = apiResults
|
materialOptions.value = apiResults
|
||||||
|
hasNextPage.value = res.has_next
|
||||||
} finally { searchLoading.value = false }
|
} finally { searchLoading.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadMoreMaterials = async () => {
|
||||||
|
if (searchLoading.value || loadingMore.value || !hasNextPage.value) return
|
||||||
|
loadingMore.value = true
|
||||||
|
searchPage.value += 1
|
||||||
|
try {
|
||||||
|
const res: any = await searchMaterialBase(searchKeyword.value, searchPage.value)
|
||||||
|
if (res.data && res.data.length > 0) {
|
||||||
|
const newItems = res.data.map((i: any) => ({...i, isHistory: false}))
|
||||||
|
materialOptions.value.push(...newItems)
|
||||||
|
hasNextPage.value = res.has_next
|
||||||
|
} else {
|
||||||
|
hasNextPage.value = false
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
searchPage.value -= 1
|
||||||
|
} finally {
|
||||||
|
loadingMore.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const onMaterialSelected = (val: number) => {
|
const onMaterialSelected = (val: number) => {
|
||||||
const item = materialOptions.value.find(i => i.id === val)
|
const item = materialOptions.value.find(i => i.id === val)
|
||||||
if (item) {
|
if (item) {
|
||||||
|
form.company_name = item.company_name // [新增]
|
||||||
form.material_name = item.name
|
form.material_name = item.name
|
||||||
form.spec_model = item.spec
|
form.spec_model = item.spec
|
||||||
form.material_type = item.type
|
form.material_type = item.type
|
||||||
@ -552,6 +658,28 @@ const fetchData = async () => {
|
|||||||
} finally { loading.value = false }
|
} finally { loading.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchOptions = async () => {
|
||||||
|
try {
|
||||||
|
const res: any = await getFilterOptions()
|
||||||
|
if (res.code === 200) {
|
||||||
|
categoryOptions.value = res.data.categories
|
||||||
|
typeOptions.value = res.data.types
|
||||||
|
companyOptions.value = res.data.companies // [新增]
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Fetch options failed", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryParams.keyword = ''
|
||||||
|
queryParams.category = ''
|
||||||
|
queryParams.material_type = ''
|
||||||
|
queryParams.company = ''
|
||||||
|
queryParams.page = 1
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
const handleCreate = () => {
|
const handleCreate = () => {
|
||||||
dialogStatus.value = 'create'
|
dialogStatus.value = 'create'
|
||||||
resetForm()
|
resetForm()
|
||||||
@ -582,7 +710,7 @@ const handleUpdate = (row: any) => {
|
|||||||
inspectionFileList.value = iReports.filter(r => !isExternalLink(r)).map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }))
|
inspectionFileList.value = iReports.filter(r => !isExternalLink(r)).map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }))
|
||||||
const iLinks = iReports.filter(r => isExternalLink(r))
|
const iLinks = iReports.filter(r => isExternalLink(r))
|
||||||
inspection_url.value = iLinks.length > 0 ? iLinks[0] : ''
|
inspection_url.value = iLinks.length > 0 ? iLinks[0] : ''
|
||||||
materialOptions.value = [{ id: row.base_id, name: row.material_name, spec: row.spec_model, category: row.category, isHistory: false }]
|
materialOptions.value = [{ id: row.base_id, name: row.material_name, spec: row.spec_model, category: row.category, company_name: row.company_name, isHistory: false }]
|
||||||
// 回显BOM
|
// 回显BOM
|
||||||
if (form.bom_code) {
|
if (form.bom_code) {
|
||||||
bomOptions.value = [{ bom_no: form.bom_code, version: form.bom_version }]
|
bomOptions.value = [{ bom_no: form.bom_code, version: form.bom_version }]
|
||||||
@ -705,7 +833,10 @@ const resetForm = () => {
|
|||||||
const getStatusType = (s:string) => ({'在库':'success','出库':'info','借库':'warning','损耗':'danger'}[s]||'warning')
|
const getStatusType = (s:string) => ({'在库':'success','出库':'info','借库':'warning','损耗':'danger'}[s]||'warning')
|
||||||
const getQualityType = (s:string) => ({'合格':'success','不合格':'danger','待检':'info'}[s]||'info')
|
const getQualityType = (s:string) => ({'合格':'success','不合格':'danger','待检':'info'}[s]||'info')
|
||||||
const formatMoney = (val:any) => isNaN(Number(val)) ? '-' : `¥ ${Number(val).toFixed(2)}`
|
const formatMoney = (val:any) => isNaN(Number(val)) ? '-' : `¥ ${Number(val).toFixed(2)}`
|
||||||
onMounted(() => fetchData())
|
onMounted(() => {
|
||||||
|
fetchData()
|
||||||
|
fetchOptions()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -747,4 +878,31 @@ onMounted(() => fetchData())
|
|||||||
.camera-card:hover { border-color: #409EFF; color: #409EFF; }
|
.camera-card:hover { border-color: #409EFF; color: #409EFF; }
|
||||||
.camera-card .text { font-size: 12px; margin-top: 5px; }
|
.camera-card .text { font-size: 12px; margin-top: 5px; }
|
||||||
.camera-card .el-icon { font-size: 24px; }
|
.camera-card .el-icon { font-size: 24px; }
|
||||||
|
|
||||||
|
/* [重点] 下拉框 Flex 布局 */
|
||||||
|
.option-item { display: flex; align-items: center; padding: 8px 0; width: 100%; }
|
||||||
|
.opt-main { flex: 1; min-width: 0; margin-right: 10px; }
|
||||||
|
.opt-name { font-weight: 600; font-size: 14px; color: #333; display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.opt-meta { width: 100px; text-align: right; flex-shrink: 0; margin-right: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.opt-spec { color: #999; font-size: 12px; }
|
||||||
|
.opt-tags { display: flex; gap: 5px; flex-shrink: 0; }
|
||||||
|
.company-tag { font-weight: bold; }
|
||||||
|
|
||||||
|
/* [新增] 修复 filter-item-select/input 样式 */
|
||||||
|
.filter-item-select { /* 宽度已在行内样式控制 */ }
|
||||||
|
.filter-item-input { /* 宽度已在行内样式控制 */ }
|
||||||
|
.action-btn { font-weight: 500; }
|
||||||
|
|
||||||
|
/* [新增] 修复弹窗最小高度 */
|
||||||
|
.dialog-scroll-container { min-height: 450px; }
|
||||||
|
|
||||||
|
/* [新增] 纯文本样式 */
|
||||||
|
.is-text-view :deep(.el-input__wrapper) { box-shadow: none !important; background-color: transparent !important; border-bottom: 1px dashed #dcdfe6; border-radius: 0; padding-left: 0; }
|
||||||
|
.is-text-view :deep(.el-input__inner) { color: #303133; font-weight: 600; font-size: 14px; cursor: text; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.long-dropdown { width: 580px !important; }
|
||||||
|
.long-dropdown .el-select-dropdown__wrap { max-height: 320px !important; }
|
||||||
|
.long-dropdown .el-input__suffix { z-index: 10; }
|
||||||
</style>
|
</style>
|
||||||
@ -1,17 +1,36 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="semi-module">
|
<div class="semi-module">
|
||||||
<div class="header-tools">
|
<div class="header-tools">
|
||||||
<div class="left-tools" style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
|
<div class="left-tools">
|
||||||
|
|
||||||
|
<el-select
|
||||||
|
v-model="queryParams.company"
|
||||||
|
placeholder="所属公司"
|
||||||
|
class="filter-item-select"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
@change="fetchData"
|
||||||
|
style="width: 160px;"
|
||||||
|
>
|
||||||
|
<el-option v-for="item in companyOptions" :key="item" :label="item" :value="item" />
|
||||||
|
</el-select>
|
||||||
|
|
||||||
<el-input
|
<el-input
|
||||||
v-model="queryParams.keyword"
|
v-model="queryParams.keyword"
|
||||||
placeholder="请输入名称或规格"
|
placeholder="请输入名称或规格"
|
||||||
|
class="filter-item-input"
|
||||||
clearable
|
clearable
|
||||||
|
@clear="fetchData"
|
||||||
@keyup.enter="fetchData"
|
@keyup.enter="fetchData"
|
||||||
style="width: 240px;"
|
style="width: 240px;"
|
||||||
/>
|
>
|
||||||
|
<template #prefix><el-icon><Search /></el-icon></template>
|
||||||
|
</el-input>
|
||||||
|
|
||||||
<el-select
|
<el-select
|
||||||
v-model="queryParams.category"
|
v-model="queryParams.category"
|
||||||
placeholder="类别"
|
placeholder="类别"
|
||||||
|
class="filter-item-select"
|
||||||
clearable
|
clearable
|
||||||
filterable
|
filterable
|
||||||
@change="fetchData"
|
@change="fetchData"
|
||||||
@ -19,9 +38,11 @@
|
|||||||
>
|
>
|
||||||
<el-option v-for="item in categoryOptions" :key="item" :label="item" :value="item" />
|
<el-option v-for="item in categoryOptions" :key="item" :label="item" :value="item" />
|
||||||
</el-select>
|
</el-select>
|
||||||
|
|
||||||
<el-select
|
<el-select
|
||||||
v-model="queryParams.material_type"
|
v-model="queryParams.material_type"
|
||||||
placeholder="类型"
|
placeholder="类型"
|
||||||
|
class="filter-item-select"
|
||||||
clearable
|
clearable
|
||||||
filterable
|
filterable
|
||||||
@change="fetchData"
|
@change="fetchData"
|
||||||
@ -29,14 +50,16 @@
|
|||||||
>
|
>
|
||||||
<el-option v-for="item in typeOptions" :key="item" :label="item" :value="item" />
|
<el-option v-for="item in typeOptions" :key="item" :label="item" :value="item" />
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-button type="primary" plain @click="fetchData">搜索</el-button>
|
|
||||||
<el-button @click="resetQuery">重置</el-button>
|
<el-button type="primary" plain class="search-btn" @click="fetchData">搜索</el-button>
|
||||||
|
<el-button class="reset-btn" @click="resetQuery">重置</el-button>
|
||||||
|
|
||||||
<el-select
|
<el-select
|
||||||
v-model="queryParams.statuses"
|
v-model="queryParams.statuses"
|
||||||
multiple
|
multiple
|
||||||
collapse-tags
|
collapse-tags
|
||||||
placeholder="状态筛选"
|
placeholder="状态筛选"
|
||||||
style="width: 220px;"
|
style="width: 200px; margin-left: 10px;"
|
||||||
@change="fetchData"
|
@change="fetchData"
|
||||||
>
|
>
|
||||||
<el-option label="在库" value="在库" />
|
<el-option label="在库" value="在库" />
|
||||||
@ -46,12 +69,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="right-tools">
|
<div class="right-tools">
|
||||||
<el-button type="primary" :icon="Plus" @click="handleCreate" class="action-btn">半成品入库登记</el-button>
|
<el-button type="primary" :icon="Plus" @click="handleCreate" class="add-btn">半成品入库</el-button>
|
||||||
<el-button :icon="Refresh" @click="fetchData" class="action-btn">刷新</el-button>
|
<el-button :icon="Refresh" circle @click="fetchData" class="circle-btn" />
|
||||||
|
|
||||||
<el-popover placement="bottom-end" title="列配置" :width="500" trigger="click">
|
<el-popover placement="bottom-end" title="列配置" :width="500" trigger="click">
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<el-button :icon="Setting" class="action-btn">表头</el-button>
|
<el-button :icon="Setting" circle class="circle-btn" />
|
||||||
</template>
|
</template>
|
||||||
<el-checkbox-group v-model="visibleColumnProps" class="column-selector">
|
<el-checkbox-group v-model="visibleColumnProps" class="column-selector">
|
||||||
<div class="col-group-title">基础信息</div>
|
<div class="col-group-title">基础信息</div>
|
||||||
@ -95,6 +118,11 @@
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #default="scope" v-else-if="col.prop === 'company_name'">
|
||||||
|
<el-tag v-if="scope.row.company_name" type="info" effect="plain" size="small" style="font-weight: bold;">{{ scope.row.company_name }}</el-tag>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #default="scope" v-else-if="col.prop === 'sn_bn'">
|
<template #default="scope" v-else-if="col.prop === 'sn_bn'">
|
||||||
<div v-if="scope.row.serial_number" class="id-cell">
|
<div v-if="scope.row.serial_number" class="id-cell">
|
||||||
<span class="prefix-tag sn">SN</span>
|
<span class="prefix-tag sn">SN</span>
|
||||||
@ -195,35 +223,41 @@
|
|||||||
top="5vh"
|
top="5vh"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
class="stylish-dialog"
|
class="stylish-dialog compact-layout"
|
||||||
>
|
>
|
||||||
<div class="dialog-scroll-container">
|
<div class="dialog-scroll-container">
|
||||||
<el-form :model="form" label-width="100px" ref="formRef" :rules="rules" size="default" class="stylish-form">
|
<el-form :model="form" label-width="100px" ref="formRef" :rules="rules" size="default" class="stylish-form">
|
||||||
|
|
||||||
<div class="form-card basic-card">
|
<div class="form-card basic-card">
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
|
<div style="display: flex; align-items: center;">
|
||||||
<el-icon class="icon"><Box/></el-icon>
|
<el-icon class="icon"><Box/></el-icon>
|
||||||
<span>1. 基础信息</span>
|
<span>1. 基础信息</span>
|
||||||
|
</div>
|
||||||
<span class="sub-title" v-if="dialogStatus === 'create'"> (请先搜索选择半成品物料)</span>
|
<span class="sub-title" v-if="dialogStatus === 'create'"> (请先搜索选择半成品物料)</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<el-row :gutter="24" v-if="dialogStatus === 'create'" style="margin-bottom: 20px;">
|
<el-row :gutter="24" v-if="dialogStatus === 'create'" style="margin-bottom: 20px;">
|
||||||
<el-col :span="10">
|
<el-col :span="12">
|
||||||
<el-form-item label="物料搜索" prop="base_id">
|
<el-form-item label="物料搜索" prop="base_id" class="highlight-label">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="form.base_id"
|
v-model="form.base_id"
|
||||||
filterable
|
filterable
|
||||||
remote
|
remote
|
||||||
reserve-keyword
|
reserve-keyword
|
||||||
placeholder="输入名称或规格..."
|
clearable
|
||||||
|
placeholder="请输入名称或规格进行检索..."
|
||||||
:remote-method="handleSearchMaterial"
|
:remote-method="handleSearchMaterial"
|
||||||
@visible-change="handleMaterialDropdownVisible"
|
@visible-change="handleMaterialDropdownVisible"
|
||||||
:loading="searchLoading"
|
:loading="searchLoading"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
@change="onMaterialSelected"
|
@change="onMaterialSelected"
|
||||||
default-first-option
|
default-first-option
|
||||||
|
v-loadmore="loadMoreMaterials"
|
||||||
|
popper-class="long-dropdown"
|
||||||
>
|
>
|
||||||
|
<template #prefix><el-icon><Search /></el-icon></template>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in materialOptions"
|
v-for="item in materialOptions"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
@ -231,29 +265,40 @@
|
|||||||
:value="item.id"
|
:value="item.id"
|
||||||
>
|
>
|
||||||
<div class="option-item">
|
<div class="option-item">
|
||||||
<span class="opt-name">{{ item.name }}</span>
|
<div class="opt-main">
|
||||||
<span class="opt-spec">{{ item.spec }}</span>
|
<span class="opt-name" :title="item.name">{{ item.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="opt-meta">
|
||||||
|
<span class="opt-spec" :title="item.spec">{{ item.spec || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="opt-tags">
|
||||||
|
<el-tag size="small" type="info" effect="light" class="company-tag">{{ item.company_name }}</el-tag>
|
||||||
<el-tag v-if="item.isHistory" size="small" type="info" effect="plain">历史</el-tag>
|
<el-tag v-if="item.isHistory" size="small" type="info" effect="plain">历史</el-tag>
|
||||||
<el-tag v-else size="small" type="success" effect="plain">系统</el-tag>
|
<el-tag v-else size="small" type="success" effect="plain">系统</el-tag>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</el-option>
|
</el-option>
|
||||||
|
<div v-if="loadingMore" style="text-align: center; color: #999; font-size: 12px; padding: 8px; background: #f9f9f9;">
|
||||||
|
<el-icon class="is-loading"><Refresh /></el-icon> 加载更多中...
|
||||||
|
</div>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="14" style="display: flex; align-items: center;">
|
<el-col :span="12" style="display: flex; align-items: center;">
|
||||||
<span class="search-tip">
|
<span class="search-tip">
|
||||||
<el-icon><InfoFilled/></el-icon> 未输入时展示最新物料;输入关键词进行精确搜索。
|
<el-icon><InfoFilled/></el-icon> 支持名称、规格型号、公司名称模糊搜索
|
||||||
</span>
|
</span>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<div class="read-only-grid">
|
<div class="read-only-grid">
|
||||||
<el-row :gutter="24">
|
<el-row :gutter="24">
|
||||||
<el-col :span="8"><el-form-item label="名称"><el-input v-model="form.material_name" disabled class="is-text-view"/></el-form-item></el-col>
|
<el-col :span="8"><el-form-item label="所属公司"><el-input v-model="form.company_name" readonly class="is-text-view"/></el-form-item></el-col>
|
||||||
<el-col :span="8"><el-form-item label="规格型号"><el-input v-model="form.spec_model" disabled class="is-text-view"/></el-form-item></el-col>
|
<el-col :span="8"><el-form-item label="名称"><el-input v-model="form.material_name" readonly class="is-text-view"/></el-form-item></el-col>
|
||||||
<el-col :span="8"><el-form-item label="单位"><el-input v-model="form.unit" disabled class="is-text-view"/></el-form-item></el-col>
|
<el-col :span="8"><el-form-item label="规格型号"><el-input v-model="form.spec_model" readonly class="is-text-view"/></el-form-item></el-col>
|
||||||
<el-col :span="8"><el-form-item label="类别"><el-input v-model="form.category" disabled class="is-text-view"/></el-form-item></el-col>
|
<el-col :span="8"><el-form-item label="单位"><el-input v-model="form.unit" readonly class="is-text-view"/></el-form-item></el-col>
|
||||||
<el-col :span="8"><el-form-item label="类型"><el-input v-model="form.material_type" disabled class="is-text-view"/></el-form-item></el-col>
|
<el-col :span="8"><el-form-item label="类别"><el-input v-model="form.category" readonly class="is-text-view"/></el-form-item></el-col>
|
||||||
|
<el-col :span="8"><el-form-item label="类型"><el-input v-model="form.material_type" readonly class="is-text-view"/></el-form-item></el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -269,8 +314,8 @@
|
|||||||
<el-row :gutter="24">
|
<el-row :gutter="24">
|
||||||
<el-col :span="6"><el-form-item label="编码/SKU" prop="sku"><el-input v-model="form.sku" placeholder="系统自动生成" disabled/></el-form-item></el-col>
|
<el-col :span="6"><el-form-item label="编码/SKU" prop="sku"><el-input v-model="form.sku" placeholder="系统自动生成" disabled/></el-form-item></el-col>
|
||||||
<el-col :span="6"><el-form-item label="入库日期" prop="in_date"><el-date-picker v-model="form.in_date" type="date" value-format="YYYY-MM-DD" style="width:100%" disabled/></el-form-item></el-col>
|
<el-col :span="6"><el-form-item label="入库日期" prop="in_date"><el-date-picker v-model="form.in_date" type="date" value-format="YYYY-MM-DD" style="width:100%" disabled/></el-form-item></el-col>
|
||||||
<el-col :span="6"><el-form-item label="条码" prop="barcode"><el-input v-model="form.barcode" placeholder="扫描条码"/></el-form-item></el-col>
|
<el-col :span="6"><el-form-item label="条码" prop="barcode"><el-input v-model="form.barcode" placeholder="扫描条码" clearable/></el-form-item></el-col>
|
||||||
<el-col :span="6"><el-form-item label="库位" prop="warehouse_location"><el-input v-model="form.warehouse_location" placeholder="例如: B-01-01"/></el-form-item></el-col>
|
<el-col :span="6"><el-form-item label="库位" prop="warehouse_location"><el-input v-model="form.warehouse_location" placeholder="例如: B-01-01" clearable/></el-form-item></el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<div class="identity-panel">
|
<div class="identity-panel">
|
||||||
@ -366,13 +411,15 @@
|
|||||||
|
|
||||||
<div class="form-card production-card">
|
<div class="form-card production-card">
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
|
<div style="display: flex; align-items: center;">
|
||||||
<el-icon class="icon"><Setting/></el-icon>
|
<el-icon class="icon"><Setting/></el-icon>
|
||||||
<span>3. 生产与成本信息</span>
|
<span>3. 生产与成本信息</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="divider-text">生产任务信息</div>
|
<div class="divider-text">生产任务信息</div>
|
||||||
<el-row :gutter="24">
|
<el-row :gutter="24">
|
||||||
<el-col :span="8"><el-form-item label="工单号"><el-input v-model="form.work_order_code" placeholder="WO-xxx"/></el-form-item></el-col>
|
<el-col :span="8"><el-form-item label="工单号"><el-input v-model="form.work_order_code" placeholder="WO-xxx" clearable/></el-form-item></el-col>
|
||||||
|
|
||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
<el-form-item label="BOM编号">
|
<el-form-item label="BOM编号">
|
||||||
@ -440,7 +487,7 @@
|
|||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<el-button @click="visible = false" size="large">取消</el-button>
|
<el-button @click="visible = false" size="large">取消</el-button>
|
||||||
<el-button type="primary" :loading="submitting" @click="submitForm" size="large" class="confirm-btn">
|
<el-button type="primary" :loading="submitting" @click="submitForm" size="large" class="confirm-btn">
|
||||||
{{ dialogStatus === 'create' ? '确认入库并打印' : '保存修改' }}
|
{{ dialogStatus === 'create' ? '提交并打印' : '保存修改' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -480,13 +527,35 @@ import {
|
|||||||
updateSemiInbound,
|
updateSemiInbound,
|
||||||
deleteSemiInbound,
|
deleteSemiInbound,
|
||||||
searchMaterialBase,
|
searchMaterialBase,
|
||||||
searchBom, // [新增]
|
searchBom,
|
||||||
getFilterOptions
|
getFilterOptions
|
||||||
} from '@/api/inbound/semi'
|
} from '@/api/inbound/semi'
|
||||||
import { uploadFile, deleteFile } from '@/api/inbound/buy'
|
import { uploadFile, deleteFile } from '@/api/inbound/buy'
|
||||||
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
|
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
|
||||||
import {getLabelPreview, executePrint} from '@/api/common/print'
|
import {getLabelPreview, executePrint} from '@/api/common/print'
|
||||||
|
|
||||||
|
// ------------------------------------
|
||||||
|
// 自定义指令:v-loadmore (适配 Teleport 到 Body 的下拉框)
|
||||||
|
// ------------------------------------
|
||||||
|
const vLoadmore = {
|
||||||
|
mounted(el: any, binding: any) {
|
||||||
|
const checkAndBind = () => {
|
||||||
|
const dropDownWrap = document.querySelector('.long-dropdown .el-select-dropdown__wrap')
|
||||||
|
if (dropDownWrap && !dropDownWrap.getAttribute('data-loadmore-bound')) {
|
||||||
|
dropDownWrap.setAttribute('data-loadmore-bound', 'true')
|
||||||
|
dropDownWrap.addEventListener('scroll', function (this: any) {
|
||||||
|
const condition = this.scrollHeight - this.scrollTop <= this.clientHeight + 1
|
||||||
|
if (condition) {
|
||||||
|
binding.value()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTimeout(checkAndBind, 500)
|
||||||
|
el.addEventListener('click', () => setTimeout(checkAndBind, 300))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
// 状态与变量
|
// 状态与变量
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
@ -494,14 +563,20 @@ 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 searchLoading = ref(false)
|
||||||
|
const loadingMore = 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: 100, keyword: '', category: '', material_type: '', statuses: ['在库', '借库'] })
|
const queryParams = reactive({ page: 1, pageSize: 100, keyword: '', category: '', material_type: '', statuses: ['在库', '借库'], company: '' })
|
||||||
const categoryOptions = ref<string[]>([])
|
const categoryOptions = ref<string[]>([])
|
||||||
const typeOptions = ref<string[]>([])
|
const typeOptions = ref<string[]>([])
|
||||||
|
const companyOptions = ref<string[]>([]) // [新增]
|
||||||
const materialOptions = ref<any[]>([])
|
const materialOptions = ref<any[]>([])
|
||||||
|
const searchPage = ref(1)
|
||||||
|
const searchKeyword = ref('')
|
||||||
|
const hasNextPage = ref(true)
|
||||||
|
let searchTimer: any = null
|
||||||
|
|
||||||
// BOM 搜索相关
|
// BOM 搜索相关
|
||||||
const bomSearchLoading = ref(false)
|
const bomSearchLoading = ref(false)
|
||||||
@ -529,6 +604,7 @@ const modeLocked = ref(false)
|
|||||||
|
|
||||||
// 列定义
|
// 列定义
|
||||||
const baseColumns = [
|
const baseColumns = [
|
||||||
|
{prop: 'company_name', label: '所属公司'}, // [新增]
|
||||||
{prop: 'material_name', label: '名称'},
|
{prop: 'material_name', label: '名称'},
|
||||||
{prop: 'category', label: '类别'},
|
{prop: 'category', label: '类别'},
|
||||||
{prop: 'material_type', label: '类型'},
|
{prop: 'material_type', label: '类型'},
|
||||||
@ -564,15 +640,13 @@ const stockColumns = [
|
|||||||
]
|
]
|
||||||
const allColumns = [...baseColumns, ...stockColumns]
|
const allColumns = [...baseColumns, ...stockColumns]
|
||||||
|
|
||||||
const defaultColumns = ['material_name', 'spec_model', 'unit', 'inbound_date', 'sn_bn', 'status', 'quality_status', 'bom_code', 'work_order_code', 'qty_stock', 'qty_available', 'unit_total_cost', 'arrival_photo', 'quality_report_link']
|
const defaultColumns = ['company_name', 'material_name', 'spec_model', 'unit', 'inbound_date', 'sn_bn', 'status', 'quality_status', 'bom_code', 'work_order_code', 'qty_stock', 'qty_available', 'unit_total_cost', 'arrival_photo', 'quality_report_link']
|
||||||
const visibleColumnProps = ref(defaultColumns)
|
const visibleColumnProps = ref(defaultColumns)
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
id: undefined, base_id: undefined as number | undefined, material_name: '', spec_model: '', category: '', unit: '', material_type: '',
|
id: undefined, base_id: undefined as number | undefined,
|
||||||
sku: '', barcode: '', in_date: '', serial_number: '', batch_number: '', status: '在库', quality_status: '合格',
|
company_name: '', // [新增]
|
||||||
in_quantity: 1, stock_quantity: 1, available_quantity: 1, warehouse_location: '',
|
material_name: '', spec_model: '', category: '', unit: '', material_type: '', sku: '', barcode: '', in_date: '', serial_number: '', batch_number: '', status: '在库', quality_status: '合格', in_quantity: 1, stock_quantity: 1, available_quantity: 1, warehouse_location: '', bom_code: '', bom_version: '', work_order_code: '', raw_material_cost: 0, manual_cost: 0, unit_total_cost: 0, production_manager: '', production_time_range: [] as string[], arrival_photo: [] as string[], quality_report_link: [] as string[], detail_link: ''
|
||||||
bom_code: '', bom_version: '', work_order_code: '', raw_material_cost: 0, manual_cost: 0, unit_total_cost: 0,
|
|
||||||
production_manager: '', production_time_range: [] as string[], arrival_photo: [] as string[], quality_report_link: [] as string[], detail_link: ''
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
@ -609,18 +683,53 @@ const handleManagerSelect = (item: any) => {
|
|||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
// Material Search (Matches Buy.vue)
|
// Material Search (Matches Buy.vue)
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
const handleMaterialDropdownVisible = (visible: boolean) => { if (visible && materialOptions.value.length === 0) handleSearchMaterial('') }
|
const handleMaterialDropdownVisible = (visible: boolean) => { if (visible && materialOptions.value.length === 0) handleSearchMaterialDebounced('') }
|
||||||
|
|
||||||
|
const handleSearchMaterialDebounced = (query: string) => {
|
||||||
|
if (searchTimer) clearTimeout(searchTimer)
|
||||||
|
searchTimer = setTimeout(() => {
|
||||||
|
handleSearchMaterial(query)
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
const handleSearchMaterial = async (query: string) => {
|
const handleSearchMaterial = async (query: string) => {
|
||||||
searchLoading.value = true
|
searchLoading.value = true
|
||||||
|
searchKeyword.value = query
|
||||||
|
searchPage.value = 1
|
||||||
|
materialOptions.value = []
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res: any = await searchMaterialBase(query)
|
const res: any = await searchMaterialBase(query, 1)
|
||||||
const apiResults = (res.data || []).map((i: any) => ({...i, isHistory: false}))
|
const apiResults = (res.data || []).map((i: any) => ({...i, isHistory: false}))
|
||||||
materialOptions.value = apiResults
|
materialOptions.value = apiResults
|
||||||
|
hasNextPage.value = res.has_next
|
||||||
} finally { searchLoading.value = false }
|
} finally { searchLoading.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadMoreMaterials = async () => {
|
||||||
|
if (searchLoading.value || loadingMore.value || !hasNextPage.value) return
|
||||||
|
loadingMore.value = true
|
||||||
|
searchPage.value += 1
|
||||||
|
try {
|
||||||
|
const res: any = await searchMaterialBase(searchKeyword.value, searchPage.value)
|
||||||
|
if (res.data && res.data.length > 0) {
|
||||||
|
const newItems = res.data.map((i: any) => ({...i, isHistory: false}))
|
||||||
|
materialOptions.value.push(...newItems)
|
||||||
|
hasNextPage.value = res.has_next
|
||||||
|
} else {
|
||||||
|
hasNextPage.value = false
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
searchPage.value -= 1
|
||||||
|
} finally {
|
||||||
|
loadingMore.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const onMaterialSelected = (val: number) => {
|
const onMaterialSelected = (val: number) => {
|
||||||
const item = materialOptions.value.find(i => i.id === val)
|
const item = materialOptions.value.find(i => i.id === val)
|
||||||
if (item) {
|
if (item) {
|
||||||
|
form.company_name = item.company_name // [新增]
|
||||||
form.material_name = item.name
|
form.material_name = item.name
|
||||||
form.spec_model = item.spec
|
form.spec_model = item.spec
|
||||||
form.category = item.category
|
form.category = item.category
|
||||||
@ -699,6 +808,7 @@ const fetchOptions = async () => {
|
|||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
categoryOptions.value = res.data.categories
|
categoryOptions.value = res.data.categories
|
||||||
typeOptions.value = res.data.types
|
typeOptions.value = res.data.types
|
||||||
|
companyOptions.value = res.data.companies // [新增]
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Fetch options failed', e)
|
console.error('Fetch options failed', e)
|
||||||
@ -709,6 +819,7 @@ const resetQuery = () => {
|
|||||||
queryParams.keyword = ''
|
queryParams.keyword = ''
|
||||||
queryParams.category = ''
|
queryParams.category = ''
|
||||||
queryParams.material_type = ''
|
queryParams.material_type = ''
|
||||||
|
queryParams.company = ''
|
||||||
queryParams.page = 1
|
queryParams.page = 1
|
||||||
fetchData()
|
fetchData()
|
||||||
}
|
}
|
||||||
@ -729,7 +840,9 @@ const handleUpdate = (row: any) => {
|
|||||||
resetForm()
|
resetForm()
|
||||||
modeLocked.value = true
|
modeLocked.value = true
|
||||||
Object.assign(form, {
|
Object.assign(form, {
|
||||||
id: row.id, base_id: row.base_id, material_name: row.material_name, spec_model: row.spec_model, category: row.category,
|
id: row.id, base_id: row.base_id,
|
||||||
|
company_name: row.company_name, // [新增]
|
||||||
|
material_name: row.material_name, spec_model: row.spec_model, category: row.category,
|
||||||
unit: row.unit, material_type: row.material_type, sku: row.sku, barcode: row.barcode, in_date: row.inbound_date,
|
unit: row.unit, material_type: row.material_type, sku: row.sku, barcode: row.barcode, in_date: row.inbound_date,
|
||||||
warehouse_location: row.warehouse_loc, status: row.status, quality_status: row.quality_status,
|
warehouse_location: row.warehouse_loc, status: row.status, quality_status: row.quality_status,
|
||||||
in_quantity: Number(row.qty_inbound), stock_quantity: Number(row.qty_stock), available_quantity: Number(row.qty_available),
|
in_quantity: Number(row.qty_inbound), stock_quantity: Number(row.qty_stock), available_quantity: Number(row.qty_available),
|
||||||
@ -748,7 +861,7 @@ const handleUpdate = (row: any) => {
|
|||||||
quality_report_url.value = reportLinks.length > 0 ? reportLinks[0] : ''
|
quality_report_url.value = reportLinks.length > 0 ? reportLinks[0] : ''
|
||||||
if (row.serial_number) { entryMode.value = 'serial'; form.serial_number = row.serial_number; form.batch_number = '' }
|
if (row.serial_number) { entryMode.value = 'serial'; form.serial_number = row.serial_number; form.batch_number = '' }
|
||||||
else { entryMode.value = 'batch'; form.batch_number = row.batch_number; form.serial_number = '' }
|
else { entryMode.value = 'batch'; form.batch_number = row.batch_number; form.serial_number = '' }
|
||||||
materialOptions.value = [{ id: row.base_id, name: row.material_name, spec: row.spec_model, category: row.category, isHistory: false }]
|
materialOptions.value = [{ id: row.base_id, name: row.material_name, spec: row.spec_model, category: row.category, company_name: row.company_name, isHistory: false }]
|
||||||
// 回显BOM,如果存在
|
// 回显BOM,如果存在
|
||||||
if (form.bom_code) {
|
if (form.bom_code) {
|
||||||
bomOptions.value = [{ bom_no: form.bom_code, version: form.bom_version }]
|
bomOptions.value = [{ bom_no: form.bom_code, version: form.bom_version }]
|
||||||
@ -862,7 +975,10 @@ const handlePrint = async (row: any) => {
|
|||||||
const confirmPrint = async () => { printing.value = true; try { await executePrint(currentPrintData.value); ElMessage.success('指令已发送'); printVisible.value = false } catch (e: any) { ElMessage.error(e.msg || '打印失败') } finally { printing.value = false } }
|
const confirmPrint = async () => { printing.value = true; try { await executePrint(currentPrintData.value); ElMessage.success('指令已发送'); printVisible.value = false } catch (e: any) { ElMessage.error(e.msg || '打印失败') } finally { printing.value = false } }
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
materialOptions.value = []; bomOptions.value = []; arrivalFileList.value = []; reportFileList.value = []; quality_report_url.value = ''
|
materialOptions.value = []; bomOptions.value = []; arrivalFileList.value = []; reportFileList.value = []; quality_report_url.value = ''
|
||||||
Object.assign(form, { id: undefined, base_id: undefined, material_name: '', spec_model: '', category: '', unit: '', material_type: '', sku: '', barcode: '', in_date: '', serial_number: '', batch_number: '', status: '在库', quality_status: '合格', in_quantity: 1, stock_quantity: 1, available_quantity: 1, warehouse_location: '', bom_code: '', bom_version: '', work_order_code: '', raw_material_cost: 0, manual_cost: 0, unit_total_cost: 0, production_manager: '', production_time_range: [], arrival_photo: [], quality_report_link: [], detail_link: '' })
|
Object.assign(form, {
|
||||||
|
id: undefined, base_id: undefined,
|
||||||
|
company_name: '', // [新增]
|
||||||
|
material_name: '', spec_model: '', category: '', unit: '', material_type: '', sku: '', barcode: '', in_date: '', serial_number: '', batch_number: '', status: '在库', quality_status: '合格', in_quantity: 1, stock_quantity: 1, available_quantity: 1, warehouse_location: '', bom_code: '', bom_version: '', work_order_code: '', raw_material_cost: 0, manual_cost: 0, unit_total_cost: 0, production_manager: '', production_time_range: [], arrival_photo: [], quality_report_link: [], detail_link: '' })
|
||||||
}
|
}
|
||||||
const getStatusType = (status: string) => { const map: any = { '在库': 'success', '出库': 'info', '借库': 'warning', '损耗': 'danger' }; return map[status] || 'warning' }
|
const getStatusType = (status: string) => { const map: any = { '在库': 'success', '出库': 'info', '借库': 'warning', '损耗': 'danger' }; return map[status] || 'warning' }
|
||||||
const getQualityType = (status: string) => { const map: any = { '合格': 'success', '不合格': 'danger', '待检': 'info', '返修中': 'warning' }; return map[status] || 'info' }
|
const getQualityType = (status: string) => { const map: any = { '合格': 'success', '不合格': 'danger', '待检': 'info', '返修中': 'warning' }; return map[status] || 'info' }
|
||||||
@ -889,7 +1005,10 @@ onMounted(() => {
|
|||||||
.avail-num { font-weight: bold; color: #67C23A; font-size: 15px; }
|
.avail-num { font-weight: bold; color: #67C23A; font-size: 15px; }
|
||||||
.id-cell { display: flex; align-items: center; }
|
.id-cell { display: flex; align-items: center; }
|
||||||
.id-text { font-family: monospace; color: #606266; }
|
.id-text { font-family: monospace; color: #606266; }
|
||||||
.stylish-form .form-card { background: #fff; border-radius: 8px; border: 1px solid #e4e7ed; margin-bottom: 20px; overflow: hidden; }
|
/* [修改] 增加 min-height */
|
||||||
|
.dialog-scroll-container { padding: 20px; max-height: 70vh; overflow-y: auto; overflow-x: hidden; min-height: 450px; }
|
||||||
|
|
||||||
|
.stylish-form .form-card { background: #fff; border-radius: 8px; border: 1px solid #e4e7ed; margin-bottom: 20px; }
|
||||||
.card-title { background: #fcfcfc; padding: 12px 20px; border-bottom: 1px solid #ebeef5; font-weight: 600; font-size: 15px; color: #303133; display: flex; align-items: center; }
|
.card-title { background: #fcfcfc; padding: 12px 20px; border-bottom: 1px solid #ebeef5; font-weight: 600; font-size: 15px; color: #303133; display: flex; align-items: center; }
|
||||||
.card-title .icon { margin-right: 8px; font-size: 18px; color: #409EFF; }
|
.card-title .icon { margin-right: 8px; font-size: 18px; color: #409EFF; }
|
||||||
.card-title .sub-title { font-size: 12px; color: #909399; font-weight: normal; margin-left: 10px; }
|
.card-title .sub-title { font-size: 12px; color: #909399; font-weight: normal; margin-left: 10px; }
|
||||||
@ -908,13 +1027,19 @@ onMounted(() => {
|
|||||||
.divider-text::before { margin-right: 15px; }
|
.divider-text::before { margin-right: 15px; }
|
||||||
.divider-text::after { margin-left: 15px; }
|
.divider-text::after { margin-left: 15px; }
|
||||||
.dialog-footer { display: flex; justify-content: flex-end; gap: 15px; margin-top: 20px; padding: 20px; border-top: 1px solid #ebeef5; }
|
.dialog-footer { display: flex; justify-content: flex-end; gap: 15px; margin-top: 20px; padding: 20px; border-top: 1px solid #ebeef5; }
|
||||||
.option-item { display: flex; justify-content: space-between; width: 100%; align-items: center; }
|
|
||||||
.opt-name { font-weight: bold; }
|
.filter-item-select { /* 宽度已在行内样式控制 */ }
|
||||||
.opt-spec { color: #8492a6; font-size: 13px; margin-right: 10px; }
|
.filter-item-input { /* 宽度已在行内样式控制 */ }
|
||||||
.is-text-view :deep(.el-input__wrapper) { box-shadow: none !important; background-color: #f5f7fa; border-bottom: 1px solid #dcdfe6; border-radius: 0; padding-left: 0; }
|
.search-btn { background-color: #E6F1FC; border-color: #A3D0FD; color: #409EFF; }
|
||||||
.is-text-view :deep(.el-input__inner) { color: #606266; font-weight: 500; }
|
.search-btn:hover { background-color: #409EFF; border-color: #409EFF; color: #fff; }
|
||||||
|
.reset-btn { background-color: #fff; border: 1px solid #dcdfe6; }
|
||||||
|
.reset-btn:hover { border-color: #c0c4cc; color: #606266; }
|
||||||
|
|
||||||
|
/* [优化] 纯文本样式 */
|
||||||
|
.is-text-view :deep(.el-input__wrapper) { box-shadow: none !important; background-color: transparent !important; border-bottom: 1px dashed #dcdfe6; border-radius: 0; padding-left: 0; }
|
||||||
|
.is-text-view :deep(.el-input__inner) { color: #303133; font-weight: 600; font-size: 14px; cursor: text; }
|
||||||
|
|
||||||
.search-tip { color: #909399; font-size: 12px; margin-left: 10px; display: flex; align-items: center; gap: 4px; }
|
.search-tip { color: #909399; font-size: 12px; margin-left: 10px; display: flex; align-items: center; gap: 4px; }
|
||||||
.dialog-scroll-container { padding: 20px; max-height: 70vh; overflow-y: auto; }
|
|
||||||
.upload-container { display: flex; flex-wrap: wrap; gap: 8px; }
|
.upload-container { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||||
:deep(.el-upload--picture-card) { width: 100px; height: 100px; line-height: 100px; }
|
:deep(.el-upload--picture-card) { width: 100px; height: 100px; line-height: 100px; }
|
||||||
:deep(.el-upload-list--picture-card .el-upload-list__item) { width: 100px; height: 100px; }
|
:deep(.el-upload-list--picture-card .el-upload-list__item) { width: 100px; height: 100px; }
|
||||||
@ -922,4 +1047,19 @@ onMounted(() => {
|
|||||||
.camera-card:hover { border-color: #409EFF; color: #409EFF; }
|
.camera-card:hover { border-color: #409EFF; color: #409EFF; }
|
||||||
.camera-card .text { font-size: 12px; margin-top: 5px; }
|
.camera-card .text { font-size: 12px; margin-top: 5px; }
|
||||||
.camera-card .el-icon { font-size: 24px; }
|
.camera-card .el-icon { font-size: 24px; }
|
||||||
|
|
||||||
|
/* [重点] 下拉框 Flex 布局 */
|
||||||
|
.option-item { display: flex; align-items: center; padding: 8px 0; width: 100%; }
|
||||||
|
.opt-main { flex: 1; min-width: 0; margin-right: 10px; }
|
||||||
|
.opt-name { font-weight: 600; font-size: 14px; color: #333; display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.opt-meta { width: 100px; text-align: right; flex-shrink: 0; margin-right: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.opt-spec { color: #999; font-size: 12px; }
|
||||||
|
.opt-tags { display: flex; gap: 5px; flex-shrink: 0; }
|
||||||
|
.company-tag { font-weight: bold; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.long-dropdown { width: 580px !important; }
|
||||||
|
.long-dropdown .el-select-dropdown__wrap { max-height: 320px !important; }
|
||||||
|
.long-dropdown .el-input__suffix { z-index: 10; }
|
||||||
</style>
|
</style>
|
||||||
Reference in New Issue
Block a user