将半成品成品同样进行新增所属公司以及内容修改

This commit is contained in:
dxc
2026-02-24 16:16:17 +08:00
parent 42171ed612
commit 31ddb1aafd
7 changed files with 464 additions and 155 deletions

View File

@ -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),

View File

@ -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),

View File

@ -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": []}

View File

@ -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": []}

View File

@ -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>

View File

@ -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>

View File

@ -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>