修正新增入库时3个组件的名称筛选逻辑
This commit is contained in:
@ -7,19 +7,27 @@ inbound_product_bp = Blueprint('inbound_product', __name__)
|
|||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# 0. 基础物料搜索
|
# 0. 基础物料搜索 (关键接口:配合 Service 实现自动回填)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_product_bp.route('/search-base', methods=['GET'])
|
@inbound_product_bp.route('/search-base', methods=['GET'])
|
||||||
def search_base():
|
def search_base():
|
||||||
|
"""
|
||||||
|
对应前端 API: /inbound/product/search-base
|
||||||
|
功能: 模糊搜索基础物料,返回 spec, unit, category, type 等详细信息
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
data = ProductInboundService.search_base_material(request.args.get('keyword', ''))
|
keyword = request.args.get('keyword', '')
|
||||||
|
# 调用 Service 层已修复的 search_base_material 方法
|
||||||
|
data = ProductInboundService.search_base_material(keyword)
|
||||||
return jsonify({"code": 200, "msg": "success", "data": data})
|
return jsonify({"code": 200, "msg": "success", "data": data})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
# 捕获异常并打印堆栈,方便调试
|
||||||
|
traceback.print_exc()
|
||||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# 1. 获取列表 (修改:接收 status 参数)
|
# 1. 获取列表 (支持 status 多选筛选)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_product_bp.route('/list', methods=['GET'])
|
@inbound_product_bp.route('/list', methods=['GET'])
|
||||||
def get_list():
|
def get_list():
|
||||||
@ -27,13 +35,15 @@ def get_list():
|
|||||||
page = request.args.get('page', 1, type=int)
|
page = request.args.get('page', 1, type=int)
|
||||||
limit = request.args.get('pageSize', 15, type=int)
|
limit = request.args.get('pageSize', 15, type=int)
|
||||||
keyword = request.args.get('keyword', '')
|
keyword = request.args.get('keyword', '')
|
||||||
# 接收状态参数
|
|
||||||
|
# 接收状态参数 (逗号分隔字符串 -> 列表)
|
||||||
statuses_str = request.args.get('statuses', '')
|
statuses_str = request.args.get('statuses', '')
|
||||||
statuses = statuses_str.split(',') if statuses_str else []
|
statuses = statuses_str.split(',') if statuses_str else []
|
||||||
|
|
||||||
result = ProductInboundService.get_list(page, limit, keyword, statuses)
|
result = ProductInboundService.get_list(page, limit, keyword, statuses)
|
||||||
return jsonify({"code": 200, "msg": "success", "data": result})
|
return jsonify({"code": 200, "msg": "success", "data": result})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
@ -46,7 +56,7 @@ def submit():
|
|||||||
# 调用 Service 处理入库,获取新创建的对象
|
# 调用 Service 处理入库,获取新创建的对象
|
||||||
new_stock = ProductInboundService.handle_inbound(request.get_json())
|
new_stock = ProductInboundService.handle_inbound(request.get_json())
|
||||||
|
|
||||||
# 返回成功信息以及新创建的数据(包含生成的ID和SKU),供前端打印使用
|
# 返回成功信息以及新创建的数据(包含生成的ID和SKU),供前端自动打印使用
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"code": 200,
|
"code": 200,
|
||||||
"msg": "入库成功",
|
"msg": "入库成功",
|
||||||
@ -66,6 +76,7 @@ def update(id):
|
|||||||
ProductInboundService.update_inbound(id, request.get_json())
|
ProductInboundService.update_inbound(id, request.get_json())
|
||||||
return jsonify({"code": 200, "msg": "更新成功"})
|
return jsonify({"code": 200, "msg": "更新成功"})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
@ -78,11 +89,12 @@ def delete(id):
|
|||||||
ProductInboundService.delete_inbound(id)
|
ProductInboundService.delete_inbound(id)
|
||||||
return jsonify({"code": 200, "msg": "删除成功"})
|
return jsonify({"code": 200, "msg": "删除成功"})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# 5. [新增] 获取出库历史
|
# 5. 获取出库历史
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_product_bp.route('/<int:id>/history', methods=['GET'])
|
@inbound_product_bp.route('/<int:id>/history', methods=['GET'])
|
||||||
def get_history(id):
|
def get_history(id):
|
||||||
@ -90,4 +102,5 @@ def get_history(id):
|
|||||||
data = ProductInboundService.get_outbound_history(id)
|
data = ProductInboundService.get_outbound_history(id)
|
||||||
return jsonify({"code": 200, "msg": "success", "data": data})
|
return jsonify({"code": 200, "msg": "success", "data": data})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||||
@ -2,30 +2,46 @@
|
|||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models.base import MaterialBase
|
from app.models.base import MaterialBase
|
||||||
from app.models.outbound import TransOutbound
|
from app.models.outbound import TransOutbound
|
||||||
from datetime import datetime, timedelta, timezone # [修改]
|
from datetime import datetime, timedelta, timezone
|
||||||
from sqlalchemy import or_, func, text, and_
|
from sqlalchemy import or_, func, text, and_
|
||||||
import traceback
|
import traceback
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
||||||
class ProductInboundService:
|
class ProductInboundService:
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 1. 基础物料搜索 (已修正:完全对齐 Buy/Semi 的逻辑)
|
||||||
|
# ============================================================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def search_base_material(keyword):
|
def search_base_material(keyword):
|
||||||
try:
|
try:
|
||||||
if not keyword:
|
# 1. 基础查询:必须是已启用的物料
|
||||||
query = MaterialBase.query.filter(MaterialBase.is_enabled == True).order_by(
|
query = MaterialBase.query.filter(MaterialBase.is_enabled == True)
|
||||||
MaterialBase.id.desc()).limit(20)
|
|
||||||
else:
|
|
||||||
query = MaterialBase.query.filter(
|
|
||||||
MaterialBase.is_enabled == True,
|
|
||||||
or_(MaterialBase.name.ilike(f'%{keyword}%'), MaterialBase.spec_model.ilike(f'%{keyword}%'))
|
|
||||||
).limit(20)
|
|
||||||
|
|
||||||
|
# 2. 动态条件:如果传入了关键词,则增加模糊匹配条件
|
||||||
|
if keyword:
|
||||||
|
query = query.filter(
|
||||||
|
or_(
|
||||||
|
MaterialBase.name.ilike(f'%{keyword}%'),
|
||||||
|
MaterialBase.spec_model.ilike(f'%{keyword}%')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. 排序与限制:按ID倒序,取最新20条
|
||||||
|
query = query.order_by(MaterialBase.id.desc()).limit(20)
|
||||||
|
|
||||||
|
# 4. 结果封装:确保字段名与前端 Vue 的 handleSelect 方法一致
|
||||||
results = []
|
results = []
|
||||||
for item in query.all():
|
for item in query.all():
|
||||||
results.append({
|
results.append({
|
||||||
'id': item.id, 'name': item.name, 'spec': item.spec_model,
|
'id': item.id,
|
||||||
'category': item.category, 'unit': item.unit, 'type': item.material_type
|
'name': item.name,
|
||||||
|
'spec': item.spec_model, # 对应前端: item.spec
|
||||||
|
'category': item.category, # 对应前端: item.category
|
||||||
|
'unit': item.unit, # 对应前端: item.unit
|
||||||
|
'type': item.material_type, # 对应前端: item.type
|
||||||
|
'status': '启用'
|
||||||
})
|
})
|
||||||
return results
|
return results
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -129,6 +145,9 @@ class ProductInboundService:
|
|||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 3. 更新逻辑
|
||||||
|
# ============================================================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_inbound(stock_id, data):
|
def update_inbound(stock_id, data):
|
||||||
from app.models.inbound.product import StockProduct
|
from app.models.inbound.product import StockProduct
|
||||||
@ -184,6 +203,9 @@ class ProductInboundService:
|
|||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 4. 删除逻辑
|
||||||
|
# ============================================================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def delete_inbound(stock_id):
|
def delete_inbound(stock_id):
|
||||||
from app.models.inbound.product import StockProduct
|
from app.models.inbound.product import StockProduct
|
||||||
@ -197,6 +219,9 @@ class ProductInboundService:
|
|||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 5. 出库历史
|
||||||
|
# ============================================================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_outbound_history(stock_id):
|
def get_outbound_history(stock_id):
|
||||||
"""获取出库历史"""
|
"""获取出库历史"""
|
||||||
@ -209,7 +234,7 @@ class ProductInboundService:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 获取列表 (修改:按时间倒序排序 + 展示只显示日期)
|
# 6. 获取列表
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_list(page, limit, keyword=None, statuses=None):
|
def get_list(page, limit, keyword=None, statuses=None):
|
||||||
@ -240,7 +265,7 @@ class ProductInboundService:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# [核心修改] 按照 production_date (入库日期) 倒序排序
|
# 按照 production_date (入库日期) 倒序排序
|
||||||
pagination = query.order_by(StockProduct.production_date.desc()).paginate(page=page, per_page=limit,
|
pagination = query.order_by(StockProduct.production_date.desc()).paginate(page=page, per_page=limit,
|
||||||
error_out=False)
|
error_out=False)
|
||||||
|
|
||||||
@ -257,7 +282,7 @@ class ProductInboundService:
|
|||||||
for item in current_items:
|
for item in current_items:
|
||||||
d = item.to_dict()
|
d = item.to_dict()
|
||||||
|
|
||||||
# [核心修改] 格式化日期
|
# 格式化日期
|
||||||
date_display = ''
|
date_display = ''
|
||||||
if item.production_date:
|
if item.production_date:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -2,36 +2,44 @@
|
|||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models.base import MaterialBase
|
from app.models.base import MaterialBase
|
||||||
from app.models.outbound import TransOutbound
|
from app.models.outbound import TransOutbound
|
||||||
from datetime import datetime, timedelta, timezone # [修改]
|
from datetime import datetime, timedelta, timezone
|
||||||
from sqlalchemy import or_, func, text, and_
|
from sqlalchemy import or_, func, text, and_
|
||||||
import traceback
|
import traceback
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
||||||
class SemiInboundService:
|
class SemiInboundService:
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 1. 基础物料搜索 (已修复:支持空关键词返回最新数据)
|
||||||
|
# ============================================================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def search_base_material(keyword):
|
def search_base_material(keyword):
|
||||||
try:
|
try:
|
||||||
if not keyword:
|
# 基础查询:必须是已启用的物料
|
||||||
return []
|
query = MaterialBase.query.filter(MaterialBase.is_enabled == True)
|
||||||
|
|
||||||
query = MaterialBase.query.filter(
|
# 如果有关键词,进行模糊匹配
|
||||||
MaterialBase.is_enabled == True,
|
if keyword:
|
||||||
or_(
|
query = query.filter(
|
||||||
MaterialBase.name.ilike(f'%{keyword}%'),
|
or_(
|
||||||
MaterialBase.spec_model.ilike(f'%{keyword}%')
|
MaterialBase.name.ilike(f'%{keyword}%'),
|
||||||
|
MaterialBase.spec_model.ilike(f'%{keyword}%')
|
||||||
|
)
|
||||||
)
|
)
|
||||||
).limit(20)
|
|
||||||
|
# 统一逻辑:按ID倒序,限制20条
|
||||||
|
query = query.order_by(MaterialBase.id.desc()).limit(20)
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
for item in query.all():
|
for item in query.all():
|
||||||
results.append({
|
results.append({
|
||||||
'id': item.id,
|
'id': item.id,
|
||||||
'name': item.name,
|
'name': item.name,
|
||||||
'spec': item.spec_model,
|
'spec': item.spec_model, # 对应前端 item.spec
|
||||||
'category': item.category,
|
'category': item.category,
|
||||||
'unit': item.unit,
|
'unit': item.unit,
|
||||||
'type': item.material_type,
|
'type': item.material_type, # 对应前端 item.type
|
||||||
'status': '启用'
|
'status': '启用'
|
||||||
})
|
})
|
||||||
return results
|
return results
|
||||||
@ -39,6 +47,9 @@ class SemiInboundService:
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 2. 新增入库逻辑
|
||||||
|
# ============================================================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def handle_inbound(data):
|
def handle_inbound(data):
|
||||||
from app.models.inbound.semi import StockSemi
|
from app.models.inbound.semi import StockSemi
|
||||||
@ -171,6 +182,9 @@ class SemiInboundService:
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 3. 更新逻辑
|
||||||
|
# ============================================================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_inbound(stock_id, data):
|
def update_inbound(stock_id, data):
|
||||||
from app.models.inbound.semi import StockSemi
|
from app.models.inbound.semi import StockSemi
|
||||||
@ -268,6 +282,9 @@ class SemiInboundService:
|
|||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 4. 删除逻辑
|
||||||
|
# ============================================================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def delete_inbound(stock_id):
|
def delete_inbound(stock_id):
|
||||||
from app.models.inbound.semi import StockSemi
|
from app.models.inbound.semi import StockSemi
|
||||||
@ -282,6 +299,9 @@ class SemiInboundService:
|
|||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 5. 出库历史
|
||||||
|
# ============================================================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_outbound_history(stock_id):
|
def get_outbound_history(stock_id):
|
||||||
"""获取出库历史"""
|
"""获取出库历史"""
|
||||||
@ -294,7 +314,7 @@ class SemiInboundService:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 6. 获取列表 (修改:按时间倒序排序 + 展示只显示日期)
|
# 6. 获取列表
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_list(page, limit, keyword=None, statuses=None):
|
def get_list(page, limit, keyword=None, statuses=None):
|
||||||
@ -329,7 +349,7 @@ class SemiInboundService:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# [核心修改] 按照 production_date (入库日期) 倒序排序
|
# 按照 production_date (入库日期) 倒序排序
|
||||||
pagination = query.order_by(StockSemi.production_date.desc()).paginate(page=page, per_page=limit,
|
pagination = query.order_by(StockSemi.production_date.desc()).paginate(page=page, per_page=limit,
|
||||||
error_out=False)
|
error_out=False)
|
||||||
|
|
||||||
@ -346,7 +366,7 @@ class SemiInboundService:
|
|||||||
for item in current_items:
|
for item in current_items:
|
||||||
d = item.to_dict()
|
d = item.to_dict()
|
||||||
|
|
||||||
# [核心修改] 格式化展示日期,覆盖 to_dict 的默认行为
|
# 格式化展示日期
|
||||||
date_display = ''
|
date_display = ''
|
||||||
if item.production_date:
|
if item.production_date:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -35,7 +35,7 @@ export function deleteProductInbound(id: number) {
|
|||||||
|
|
||||||
export function searchMaterialBase(keyword: string) {
|
export function searchMaterialBase(keyword: string) {
|
||||||
return request({
|
return request({
|
||||||
url: '/inbound/base/search',
|
url: '/inbound/product/search-base',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params: { keyword }
|
params: { keyword }
|
||||||
})
|
})
|
||||||
|
|||||||
@ -177,7 +177,9 @@
|
|||||||
<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.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.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.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.unit" 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" 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" disabled 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-row>
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -230,21 +232,10 @@
|
|||||||
<el-col :span="dialogStatus === 'update' ? 12 : 18">
|
<el-col :span="dialogStatus === 'update' ? 12 : 18">
|
||||||
<el-form-item label="成品实拍" prop="product_photo">
|
<el-form-item label="成品实拍" prop="product_photo">
|
||||||
<div class="upload-container">
|
<div class="upload-container">
|
||||||
<el-upload
|
<el-upload v-model:file-list="productPhotoList" action="#" list-type="picture-card" multiple :http-request="(opts) => customUpload(opts, 'product_photo')" :on-preview="handlePreviewPicture" :on-remove="(file) => handleRemoveImage(file, 'product_photo')" :before-upload="beforeAvatarUpload">
|
||||||
v-model:file-list="productPhotoList"
|
|
||||||
action="#"
|
|
||||||
list-type="picture-card"
|
|
||||||
multiple
|
|
||||||
:http-request="(opts) => customUpload(opts, 'product_photo')"
|
|
||||||
:on-preview="handlePreviewPicture"
|
|
||||||
:on-remove="(file) => handleRemoveImage(file, 'product_photo')"
|
|
||||||
:before-upload="beforeAvatarUpload"
|
|
||||||
>
|
|
||||||
<el-icon><Plus /></el-icon>
|
<el-icon><Plus /></el-icon>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
<div class="camera-card" @click="triggerCamera('product_photo')">
|
<div class="camera-card" @click="triggerCamera('product_photo')"><el-icon><Camera /></el-icon><span class="text">拍照</span></div>
|
||||||
<el-icon><Camera /></el-icon><span class="text">拍照</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<el-input v-model="form.product_photo" style="display:none" />
|
<el-input v-model="form.product_photo" style="display:none" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -255,21 +246,10 @@
|
|||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="质量报告" prop="quality_report_link">
|
<el-form-item label="质量报告" prop="quality_report_link">
|
||||||
<div class="upload-container">
|
<div class="upload-container">
|
||||||
<el-upload
|
<el-upload v-model:file-list="qualityFileList" action="#" list-type="picture-card" multiple :http-request="(opts) => customUpload(opts, 'quality_report_link')" :on-preview="handlePreviewPicture" :on-remove="(file) => handleRemoveImage(file, 'quality_report_link')" :before-upload="beforeAvatarUpload">
|
||||||
v-model:file-list="qualityFileList"
|
|
||||||
action="#"
|
|
||||||
list-type="picture-card"
|
|
||||||
multiple
|
|
||||||
:http-request="(opts) => customUpload(opts, 'quality_report_link')"
|
|
||||||
:on-preview="handlePreviewPicture"
|
|
||||||
:on-remove="(file) => handleRemoveImage(file, 'quality_report_link')"
|
|
||||||
:before-upload="beforeAvatarUpload"
|
|
||||||
>
|
|
||||||
<el-icon><Plus /></el-icon>
|
<el-icon><Plus /></el-icon>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
<div class="camera-card" @click="triggerCamera('quality_report_link')">
|
<div class="camera-card" @click="triggerCamera('quality_report_link')"><el-icon><Camera /></el-icon><span class="text">拍照</span></div>
|
||||||
<el-icon><Camera /></el-icon><span class="text">拍照</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<el-input v-model="quality_url" placeholder="外部链接..." style="margin-top:8px" clearable><template #prefix><el-icon><Link /></el-icon></template></el-input>
|
<el-input v-model="quality_url" placeholder="外部链接..." style="margin-top:8px" clearable><template #prefix><el-icon><Link /></el-icon></template></el-input>
|
||||||
<el-input v-model="form.quality_report_link" style="display:none" />
|
<el-input v-model="form.quality_report_link" style="display:none" />
|
||||||
@ -279,21 +259,10 @@
|
|||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="检测报告" prop="inspection_report_link">
|
<el-form-item label="检测报告" prop="inspection_report_link">
|
||||||
<div class="upload-container">
|
<div class="upload-container">
|
||||||
<el-upload
|
<el-upload v-model:file-list="inspectionFileList" action="#" list-type="picture-card" multiple :http-request="(opts) => customUpload(opts, 'inspection_report_link')" :on-preview="handlePreviewPicture" :on-remove="(file) => handleRemoveImage(file, 'inspection_report_link')" :before-upload="beforeAvatarUpload">
|
||||||
v-model:file-list="inspectionFileList"
|
|
||||||
action="#"
|
|
||||||
list-type="picture-card"
|
|
||||||
multiple
|
|
||||||
:http-request="(opts) => customUpload(opts, 'inspection_report_link')"
|
|
||||||
:on-preview="handlePreviewPicture"
|
|
||||||
:on-remove="(file) => handleRemoveImage(file, 'inspection_report_link')"
|
|
||||||
:before-upload="beforeAvatarUpload"
|
|
||||||
>
|
|
||||||
<el-icon><Plus /></el-icon>
|
<el-icon><Plus /></el-icon>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
<div class="camera-card" @click="triggerCamera('inspection_report_link')">
|
<div class="camera-card" @click="triggerCamera('inspection_report_link')"><el-icon><Camera /></el-icon><span class="text">拍照</span></div>
|
||||||
<el-icon><Camera /></el-icon><span class="text">拍照</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<el-input v-model="inspection_url" placeholder="外部链接..." style="margin-top:8px" clearable><template #prefix><el-icon><Link /></el-icon></template></el-input>
|
<el-input v-model="inspection_url" placeholder="外部链接..." style="margin-top:8px" clearable><template #prefix><el-icon><Link /></el-icon></template></el-input>
|
||||||
<el-input v-model="form.inspection_report_link" style="display:none" />
|
<el-input v-model="form.inspection_report_link" style="display:none" />
|
||||||
@ -315,15 +284,7 @@
|
|||||||
<el-col :span="8"><el-form-item label="订单号"><el-input v-model="form.order_id" placeholder="关联销售订单" /></el-form-item></el-col>
|
<el-col :span="8"><el-form-item label="订单号"><el-input v-model="form.order_id" placeholder="关联销售订单" /></el-form-item></el-col>
|
||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
<el-form-item label="负责人">
|
<el-form-item label="负责人">
|
||||||
<el-autocomplete
|
<el-autocomplete v-model="form.production_manager" :fetch-suggestions="querySearchManager" placeholder="输入或选择负责人" style="width: 100%" clearable :trigger-on-focus="true" @select="handleManagerSelect"/>
|
||||||
v-model="form.production_manager"
|
|
||||||
:fetch-suggestions="querySearchManager"
|
|
||||||
placeholder="输入或选择负责人"
|
|
||||||
style="width: 100%"
|
|
||||||
clearable
|
|
||||||
:trigger-on-focus="true"
|
|
||||||
@select="handleManagerSelect"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="8"><el-form-item label="产品定价"><el-input-number v-model="form.sale_price" :precision="2" style="width:100%"><template #prefix>¥</template></el-input-number></el-form-item></el-col>
|
<el-col :span="8"><el-form-item label="产品定价"><el-input-number v-model="form.sale_price" :precision="2" style="width:100%"><template #prefix>¥</template></el-input-number></el-form-item></el-col>
|
||||||
@ -368,10 +329,7 @@
|
|||||||
<img v-if="previewUrl" :src="previewUrl" alt="Label Preview" style="width: 100%; border: 1px solid #ccc;"/>
|
<img v-if="previewUrl" :src="previewUrl" alt="Label Preview" style="width: 100%; border: 1px solid #ccc;"/>
|
||||||
<div v-else class="empty-preview">正在生成预览...</div>
|
<div v-else class="empty-preview">正在生成预览...</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top: 20px; font-size: 14px; color: #666;">
|
<div style="margin-top: 20px; font-size: 14px; color: #666;"><p>打印机 IP: 192.168.9.205</p><p>尺寸: 40mm x 30mm</p></div>
|
||||||
<p>打印机 IP: 192.168.9.205</p>
|
|
||||||
<p>尺寸: 40mm x 30mm</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
@ -403,13 +361,7 @@ 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()
|
||||||
// [修改] 增加 statuses 参数
|
const queryParams = reactive({ page: 1, pageSize: 15, keyword: '', statuses: ['在库', '借库'] })
|
||||||
const queryParams = reactive({
|
|
||||||
page: 1,
|
|
||||||
pageSize: 15,
|
|
||||||
keyword: '',
|
|
||||||
statuses: ['在库', '借库']
|
|
||||||
})
|
|
||||||
const materialOptions = ref<any[]>([])
|
const materialOptions = ref<any[]>([])
|
||||||
|
|
||||||
// 打印相关变量
|
// 打印相关变量
|
||||||
@ -441,6 +393,7 @@ const allColumns = [
|
|||||||
{ prop: 'status', label: '状态', minWidth: '90' },
|
{ prop: 'status', label: '状态', minWidth: '90' },
|
||||||
{ prop: 'quality_status', label: '质量', minWidth: '90' },
|
{ prop: 'quality_status', label: '质量', minWidth: '90' },
|
||||||
{ prop: 'spec_model', label: '规格', minWidth: '120' },
|
{ prop: 'spec_model', label: '规格', minWidth: '120' },
|
||||||
|
{ prop: 'unit', label: '单位', minWidth: '80' },
|
||||||
{ prop: 'product_photo', label: '实拍图', minWidth: '100' },
|
{ prop: 'product_photo', label: '实拍图', minWidth: '100' },
|
||||||
{ prop: 'sale_price', label: '售价', minWidth: '100' },
|
{ prop: 'sale_price', label: '售价', minWidth: '100' },
|
||||||
{ prop: 'order_id', label: '订单号', minWidth: '120' },
|
{ prop: 'order_id', label: '订单号', minWidth: '120' },
|
||||||
@ -455,40 +408,21 @@ 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 = [
|
const STORAGE_KEY = 'stock_product_visible_columns_v2'
|
||||||
'material_name', 'sku', 'serial_number', 'qty_stock', 'status', 'quality_status',
|
const getSavedColumns = () => { try { const saved = localStorage.getItem(STORAGE_KEY); return saved ? JSON.parse(saved) : defaultVisibleCols } catch (e) { return defaultVisibleCols } }
|
||||||
'product_photo', 'sale_price', 'order_id'
|
|
||||||
]
|
|
||||||
|
|
||||||
// 表头持久化
|
|
||||||
const STORAGE_KEY = 'stock_product_visible_columns_v2' // Changed key
|
|
||||||
const getSavedColumns = () => {
|
|
||||||
try {
|
|
||||||
const saved = localStorage.getItem(STORAGE_KEY)
|
|
||||||
return saved ? JSON.parse(saved) : defaultVisibleCols
|
|
||||||
} catch (e) {
|
|
||||||
return defaultVisibleCols
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const visibleColumnProps = ref(getSavedColumns())
|
const visibleColumnProps = ref(getSavedColumns())
|
||||||
|
watch(visibleColumnProps, (newVal) => { localStorage.setItem(STORAGE_KEY, JSON.stringify(newVal)) }, { deep: true })
|
||||||
watch(visibleColumnProps, (newVal) => {
|
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(newVal))
|
|
||||||
}, { deep: true })
|
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
id: undefined, base_id: undefined, material_name: '', spec_model: '', material_type: '', category: '',
|
id: undefined, base_id: undefined, 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: '合格',
|
||||||
bom_code: '', bom_version: '', work_order_code: '', order_id: '',
|
bom_code: '', bom_version: '', work_order_code: '', order_id: '',
|
||||||
production_manager: '', production_time_range: [] as string[],
|
production_manager: '', production_time_range: [] as string[],
|
||||||
raw_material_cost: 0, manual_cost: 0, sale_price: 0,
|
raw_material_cost: 0, manual_cost: 0, sale_price: 0,
|
||||||
quality_report_link: [] as string[],
|
quality_report_link: [] as string[], inspection_report_link: [] as string[], product_photo: [] as string[], detail_link: ''
|
||||||
inspection_report_link: [] as string[],
|
|
||||||
product_photo: [] as string[],
|
|
||||||
detail_link: ''
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
@ -503,6 +437,39 @@ const getHistoryList = (key: string): any[] => { try { return (JSON.parse(localS
|
|||||||
const saveMaterialHistory = (item: any) => { if (!item || !item.id) return; const key = HISTORY_KEYS.MATERIAL; try { let list = JSON.parse(localStorage.getItem(key) || '[]'); list = list.filter((i: any) => i.id !== item.id); list.unshift({ ...item, isHistory: true }); if (list.length > 10) list = list.slice(0, 10); localStorage.setItem(key, JSON.stringify(list)) } catch (e) {} }
|
const saveMaterialHistory = (item: any) => { if (!item || !item.id) return; const key = HISTORY_KEYS.MATERIAL; try { let list = JSON.parse(localStorage.getItem(key) || '[]'); list = list.filter((i: any) => i.id !== item.id); list.unshift({ ...item, isHistory: true }); if (list.length > 10) list = list.slice(0, 10); localStorage.setItem(key, JSON.stringify(list)) } catch (e) {} }
|
||||||
const getMaterialHistory = () => { try { return JSON.parse(localStorage.getItem(HISTORY_KEYS.MATERIAL) || '[]') } catch (e) { return [] } }
|
const getMaterialHistory = () => { try { return JSON.parse(localStorage.getItem(HISTORY_KEYS.MATERIAL) || '[]') } catch (e) { return [] } }
|
||||||
|
|
||||||
|
// ------------------------------------
|
||||||
|
// Material Search & Population Logic
|
||||||
|
// ------------------------------------
|
||||||
|
const handleMaterialDropdownVisible = (visible: boolean) => { if (visible && materialOptions.value.length === 0) handleSearchMaterial('') }
|
||||||
|
const handleSearchMaterial = async (query: string) => {
|
||||||
|
searchLoading.value = true
|
||||||
|
try {
|
||||||
|
const res: any = await searchMaterialBase(query)
|
||||||
|
const apiResults = (res.data || []).map((i: any) => ({ ...i, isHistory: false }))
|
||||||
|
if (!query) {
|
||||||
|
const history = getMaterialHistory()
|
||||||
|
const historyIds = new Set(history.map((h: any) => h.id))
|
||||||
|
const filteredApi = apiResults.filter((apiItem: any) => !historyIds.has(apiItem.id))
|
||||||
|
materialOptions.value = [...history, ...filteredApi]
|
||||||
|
} else { materialOptions.value = apiResults }
|
||||||
|
} finally { searchLoading.value = false }
|
||||||
|
}
|
||||||
|
const onMaterialSelected = (val: number) => {
|
||||||
|
const item = materialOptions.value.find(i => i.id === val)
|
||||||
|
if (item) {
|
||||||
|
saveMaterialHistory(item)
|
||||||
|
// Auto-populate readonly fields
|
||||||
|
form.material_name = item.name
|
||||||
|
form.spec_model = item.spec
|
||||||
|
form.material_type = item.type
|
||||||
|
form.category = item.category
|
||||||
|
form.unit = item.unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------
|
||||||
|
// Autocomplete (Manager)
|
||||||
|
// ------------------------------------
|
||||||
const createFilter = (qs: string) => { return (item: any) => (item.value.toLowerCase().indexOf(qs.toLowerCase()) === 0) }
|
const createFilter = (qs: string) => { return (item: any) => (item.value.toLowerCase().indexOf(qs.toLowerCase()) === 0) }
|
||||||
const getTableDataUnique = (field: string) => { return Array.from(new Set(tableData.value.map((i: any) => i[field]).filter(Boolean))).map(i => ({ value: i })) }
|
const getTableDataUnique = (field: string) => { return Array.from(new Set(tableData.value.map((i: any) => i[field]).filter(Boolean))).map(i => ({ value: i })) }
|
||||||
const mixedSearch = (qs: string, tableField: string, storageKey: string, cb: any) => { const tableList = getTableDataUnique(tableField); const historyList = getHistoryList(storageKey); const map = new Map(); historyList.forEach(i => map.set(i.value, i)); tableList.forEach(i => map.set(i.value, i)); const allList = Array.from(map.values()); const results = qs ? allList.filter(createFilter(qs)) : allList; cb(results) }
|
const mixedSearch = (qs: string, tableField: string, storageKey: string, cb: any) => { const tableList = getTableDataUnique(tableField); const historyList = getHistoryList(storageKey); const map = new Map(); historyList.forEach(i => map.set(i.value, i)); tableList.forEach(i => map.set(i.value, i)); const allList = Array.from(map.values()); const results = qs ? allList.filter(createFilter(qs)) : allList; cb(results) }
|
||||||
@ -512,20 +479,13 @@ const handleManagerSelect = (item: any) => saveToHistory(HISTORY_KEYS.PRODUCTION
|
|||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
// [修改] 传递 statuses
|
|
||||||
const params = { ...queryParams, statuses: queryParams.statuses.join(',') }
|
const params = { ...queryParams, statuses: queryParams.statuses.join(',') }
|
||||||
const res: any = await getProductList(params);
|
const res: any = await getProductList(params);
|
||||||
tableData.value = res.data.items || [];
|
tableData.value = res.data.items || [];
|
||||||
total.value = res.data.total || 0
|
total.value = res.data.total || 0
|
||||||
} finally {
|
} finally { loading.value = false }
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMaterialDropdownVisible = (visible: boolean) => { if (visible && materialOptions.value.length === 0) handleSearchMaterial('') }
|
|
||||||
const handleSearchMaterial = async (query: string) => { searchLoading.value = true; try { const res: any = await searchMaterialBase(query); const apiResults = (res.data || []).map((i: any) => ({ ...i, isHistory: false })); if (!query) { const history = getMaterialHistory(); const historyIds = new Set(history.map((h: any) => h.id)); const filteredApi = apiResults.filter((apiItem: any) => !historyIds.has(apiItem.id)); materialOptions.value = [...history, ...filteredApi] } else { materialOptions.value = apiResults } } finally { searchLoading.value = false } }
|
|
||||||
const onMaterialSelected = (val: number) => { const item = materialOptions.value.find(i => i.id === val); if (item) { saveMaterialHistory(item); form.material_name = item.name; form.spec_model = item.spec; form.material_type = item.type; form.category = item.category } }
|
|
||||||
|
|
||||||
const handleCreate = () => {
|
const handleCreate = () => {
|
||||||
dialogStatus.value = 'create'
|
dialogStatus.value = 'create'
|
||||||
resetForm()
|
resetForm()
|
||||||
@ -534,9 +494,6 @@ const handleCreate = () => {
|
|||||||
materialOptions.value = []
|
materialOptions.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------
|
|
||||||
// 核心更新逻辑 (回显三个图片字段)
|
|
||||||
// ------------------------------------
|
|
||||||
const handleUpdate = (row: any) => {
|
const handleUpdate = (row: any) => {
|
||||||
dialogStatus.value = 'update'
|
dialogStatus.value = 'update'
|
||||||
Object.assign(form, {
|
Object.assign(form, {
|
||||||
@ -550,22 +507,15 @@ const handleUpdate = (row: any) => {
|
|||||||
sale_price: Number(row.sale_price)
|
sale_price: Number(row.sale_price)
|
||||||
})
|
})
|
||||||
if(row.production_start_time && row.production_end_time) { form.production_time_range = [row.production_start_time, row.production_end_time] } else { form.production_time_range = [] }
|
if(row.production_start_time && row.production_end_time) { form.production_time_range = [row.production_start_time, row.production_end_time] } else { form.production_time_range = [] }
|
||||||
|
|
||||||
// 1. 成品实拍
|
|
||||||
productPhotoList.value = form.product_photo.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }))
|
productPhotoList.value = form.product_photo.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }))
|
||||||
|
|
||||||
// 2. 质量报告
|
|
||||||
const qReports = form.quality_report_link || []
|
const qReports = form.quality_report_link || []
|
||||||
qualityFileList.value = qReports.filter(r => !isExternalLink(r)).map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }))
|
qualityFileList.value = qReports.filter(r => !isExternalLink(r)).map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }))
|
||||||
const qLinks = qReports.filter(r => isExternalLink(r))
|
const qLinks = qReports.filter(r => isExternalLink(r))
|
||||||
quality_url.value = qLinks.length > 0 ? qLinks[0] : ''
|
quality_url.value = qLinks.length > 0 ? qLinks[0] : ''
|
||||||
|
|
||||||
// 3. 检测报告
|
|
||||||
const iReports = form.inspection_report_link || []
|
const iReports = form.inspection_report_link || []
|
||||||
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, isHistory: false }]
|
||||||
visible.value = true
|
visible.value = true
|
||||||
}
|
}
|
||||||
@ -575,21 +525,16 @@ const isExternalLink = (str: string) => { return str && (str.startsWith('http://
|
|||||||
const getImagesOnly = (list: string[]) => { if (!list) return []; return list.filter(item => !isExternalLink(item)) }
|
const getImagesOnly = (list: string[]) => { if (!list) return []; return list.filter(item => !isExternalLink(item)) }
|
||||||
const hasExternalLink = (list: string[]) => { if (!list) return false; return list.some(item => isExternalLink(item)) }
|
const hasExternalLink = (list: string[]) => { if (!list) return false; return list.some(item => isExternalLink(item)) }
|
||||||
const beforeAvatarUpload = (rawFile: any) => { if (rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/png') { ElMessage.error('仅支持 JPG/PNG'); return false } if (rawFile.size / 1024 / 1024 > 5) { ElMessage.error('图片不能超过 5MB'); return false } return true }
|
const beforeAvatarUpload = (rawFile: any) => { if (rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/png') { ElMessage.error('仅支持 JPG/PNG'); return false } if (rawFile.size / 1024 / 1024 > 5) { ElMessage.error('图片不能超过 5MB'); return false } return true }
|
||||||
|
|
||||||
const customUpload = async (options: any, targetField: 'product_photo' | 'quality_report_link' | 'inspection_report_link') => {
|
const customUpload = async (options: any, targetField: 'product_photo' | 'quality_report_link' | 'inspection_report_link') => {
|
||||||
const { file, onSuccess, onError } = options
|
const { file, onSuccess, onError } = options
|
||||||
const formData = new FormData(); formData.append('file', file)
|
const formData = new FormData(); formData.append('file', file)
|
||||||
try {
|
try {
|
||||||
const res: any = await uploadFile(formData)
|
const res: any = await uploadFile(formData)
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
const newUrl = res.data.url
|
const newUrl = res.data.url; form[targetField].push(newUrl); ElMessage.success('上传成功'); onSuccess(res)
|
||||||
form[targetField].push(newUrl)
|
|
||||||
ElMessage.success('上传成功')
|
|
||||||
onSuccess(res)
|
|
||||||
} else { ElMessage.error(res.msg || '上传失败'); onError(new Error(res.msg)) }
|
} else { ElMessage.error(res.msg || '上传失败'); onError(new Error(res.msg)) }
|
||||||
} catch (e) { ElMessage.error('网络错误'); onError(e) }
|
} catch (e) { ElMessage.error('网络错误'); onError(e) }
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRemoveImage = async (uploadFile: any, targetField: 'product_photo' | 'quality_report_link' | 'inspection_report_link') => {
|
const handleRemoveImage = async (uploadFile: any, targetField: 'product_photo' | 'quality_report_link' | 'inspection_report_link') => {
|
||||||
try {
|
try {
|
||||||
const urlToRemove = form[targetField].find(u => getImageUrl(u) === uploadFile.url) || uploadFile.url
|
const urlToRemove = form[targetField].find(u => getImageUrl(u) === uploadFile.url) || uploadFile.url
|
||||||
@ -598,7 +543,6 @@ const handleRemoveImage = async (uploadFile: any, targetField: 'product_photo' |
|
|||||||
ElMessage.success('已删除')
|
ElMessage.success('已删除')
|
||||||
} catch (e) { console.error(e) }
|
} catch (e) { console.error(e) }
|
||||||
}
|
}
|
||||||
|
|
||||||
const triggerCamera = (field: any) => { currentCameraField.value = field; if (cameraInputRef.value) cameraInputRef.value.click() }
|
const triggerCamera = (field: any) => { currentCameraField.value = field; if (cameraInputRef.value) cameraInputRef.value.click() }
|
||||||
const handleCameraFile = async (event: Event) => {
|
const handleCameraFile = async (event: Event) => {
|
||||||
const input = event.target as HTMLInputElement; if (input.files && input.files[0]) {
|
const input = event.target as HTMLInputElement; if (input.files && input.files[0]) {
|
||||||
@ -618,44 +562,27 @@ const handleCameraFile = async (event: Event) => {
|
|||||||
}
|
}
|
||||||
const handlePreviewPicture = (uploadFile: any) => { dialogImageUrl.value = uploadFile.url!; dialogVisibleImage.value = true }
|
const handlePreviewPicture = (uploadFile: any) => { dialogImageUrl.value = uploadFile.url!; dialogVisibleImage.value = true }
|
||||||
|
|
||||||
// ------------------------------------
|
|
||||||
// 提交逻辑 (合并链接)
|
|
||||||
// ------------------------------------
|
|
||||||
const submitForm = async () => {
|
const submitForm = async () => {
|
||||||
await formRef.value.validate(async (valid: boolean) => {
|
await formRef.value.validate(async (valid: boolean) => {
|
||||||
if(valid) {
|
if(valid) {
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
|
|
||||||
// 合并 Quality 链接
|
|
||||||
const qList = [...form.quality_report_link]
|
const qList = [...form.quality_report_link]
|
||||||
const qImages = qList.filter(item => !isExternalLink(item))
|
const qImages = qList.filter(item => !isExternalLink(item))
|
||||||
if (quality_url.value && !qList.includes(quality_url.value)) qImages.push(quality_url.value)
|
if (quality_url.value && !qList.includes(quality_url.value)) qImages.push(quality_url.value)
|
||||||
else if (quality_url.value) qImages.push(quality_url.value) // 重新添加输入框内容
|
else if (quality_url.value) qImages.push(quality_url.value)
|
||||||
|
|
||||||
// 合并 Inspection 链接
|
|
||||||
const iList = [...form.inspection_report_link]
|
const iList = [...form.inspection_report_link]
|
||||||
const iImages = iList.filter(item => !isExternalLink(item))
|
const iImages = iList.filter(item => !isExternalLink(item))
|
||||||
if (inspection_url.value && !iList.includes(inspection_url.value)) iImages.push(inspection_url.value)
|
if (inspection_url.value && !iList.includes(inspection_url.value)) iImages.push(inspection_url.value)
|
||||||
else if (inspection_url.value) iImages.push(inspection_url.value)
|
else if (inspection_url.value) iImages.push(inspection_url.value)
|
||||||
|
const payload = { ...form, quality_report_link: qImages, inspection_report_link: iImages, production_start_time: form.production_time_range?.[0], production_end_time: form.production_time_range?.[1] }
|
||||||
const payload = { ...form,
|
|
||||||
quality_report_link: qImages,
|
|
||||||
inspection_report_link: iImages,
|
|
||||||
production_start_time: form.production_time_range?.[0],
|
|
||||||
production_end_time: form.production_time_range?.[1]
|
|
||||||
}
|
|
||||||
delete payload.production_time_range
|
delete payload.production_time_range
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if(dialogStatus.value === 'create') {
|
if(dialogStatus.value === 'create') {
|
||||||
const res: any = await createProductInbound(payload)
|
const res: any = await createProductInbound(payload)
|
||||||
ElMessage.success('入库成功')
|
ElMessage.success('入库成功')
|
||||||
const newItem = res.data
|
const newItem = res.data
|
||||||
if (newItem) { ElMessage.info('发送打印...'); try { await executePrint(newItem); ElMessage.success('指令已发送') } catch (e: any) { ElMessage.warning('打印失败') } }
|
if (newItem) { ElMessage.info('发送打印...'); try { await executePrint(newItem); ElMessage.success('指令已发送') } catch (e: any) { ElMessage.warning('打印失败') } }
|
||||||
} else {
|
} else { await updateProductInbound(form.id!, payload); ElMessage.success('更新成功') }
|
||||||
await updateProductInbound(form.id!, payload)
|
|
||||||
ElMessage.success('更新成功')
|
|
||||||
}
|
|
||||||
saveToHistory(HISTORY_KEYS.PRODUCTION_MANAGER, form.production_manager)
|
saveToHistory(HISTORY_KEYS.PRODUCTION_MANAGER, form.production_manager)
|
||||||
visible.value = false; fetchData()
|
visible.value = false; fetchData()
|
||||||
} catch(e:any) { ElMessage.error(e.msg || '失败') } finally { submitting.value = false }
|
} catch(e:any) { ElMessage.error(e.msg || '失败') } finally { submitting.value = false }
|
||||||
@ -664,28 +591,16 @@ const submitForm = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleDelete = async (row: any) => { try { await deleteProductInbound(row.id); ElMessage.success('删除成功'); fetchData() } catch(e) { ElMessage.error('删除失败') } }
|
const handleDelete = async (row: any) => { try { await deleteProductInbound(row.id); ElMessage.success('删除成功'); fetchData() } catch(e) { ElMessage.error('删除失败') } }
|
||||||
|
|
||||||
const handlePrint = async (row: any) => {
|
const handlePrint = async (row: any) => {
|
||||||
printVisible.value = true; printLoading.value = true; previewUrl.value = ''
|
printVisible.value = true; printLoading.value = true; previewUrl.value = ''
|
||||||
currentPrintData.value = { global_print_id: row.global_print_id, material_name: row.material_name, spec_model: row.spec_model, category: row.category, material_type: row.material_type, warehouse_loc: row.warehouse_loc, serial_number: row.serial_number, sku: row.sku }
|
currentPrintData.value = { global_print_id: row.global_print_id, material_name: row.material_name, spec_model: row.spec_model, category: row.category, material_type: row.material_type, warehouse_loc: row.warehouse_loc, serial_number: row.serial_number, sku: row.sku }
|
||||||
try { const res: any = await getLabelPreview(currentPrintData.value); previewUrl.value = res.data } catch (e) { ElMessage.error('预览失败') } finally { printLoading.value = false }
|
try { const res: any = await getLabelPreview(currentPrintData.value); previewUrl.value = res.data } catch (e) { ElMessage.error('预览失败') } finally { printLoading.value = false }
|
||||||
}
|
}
|
||||||
const confirmPrint = async () => { printing.value = true; try { await executePrint(currentPrintData.value); ElMessage.success('已发送'); printVisible.value = false } catch (e: any) { ElMessage.error('打印失败') } 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('打印失败') } finally { printing.value = false } }
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
materialOptions.value = []; productPhotoList.value = []; qualityFileList.value = []; inspectionFileList.value = []; quality_url.value = ''; inspection_url.value = ''
|
materialOptions.value = []; productPhotoList.value = []; qualityFileList.value = []; inspectionFileList.value = []; quality_url.value = ''; inspection_url.value = ''
|
||||||
Object.assign(form, {
|
Object.assign(form, { id: undefined, base_id: undefined, material_name: '', spec_model: '', material_type: '', category: '', unit: '', sku: '', barcode: '', serial_number: '', in_date: '', in_quantity: 1, stock_quantity: 1, available_quantity: 1, warehouse_location: '', status: '在库', quality_status: '合格', bom_code: '', bom_version: '', work_order_code: '', order_id: '', production_manager: '', production_time_range: [], raw_material_cost: 0, manual_cost: 0, sale_price: 0, quality_report_link: [], inspection_report_link: [], product_photo: [], detail_link: '' })
|
||||||
id: undefined, base_id: undefined, material_name: '', spec_model: '', material_type: '', category: '',
|
|
||||||
sku: '', barcode: '', serial_number: '', in_date: '',
|
|
||||||
in_quantity: 1, stock_quantity: 1, available_quantity: 1,
|
|
||||||
warehouse_location: '', status: '在库', quality_status: '合格',
|
|
||||||
bom_code: '', bom_version: '', work_order_code: '', order_id: '',
|
|
||||||
production_manager: '', production_time_range: [],
|
|
||||||
raw_material_cost: 0, manual_cost: 0, sale_price: 0,
|
|
||||||
quality_report_link: [], inspection_report_link: [], product_photo: [], detail_link: ''
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)}`
|
||||||
@ -701,7 +616,6 @@ onMounted(() => fetchData())
|
|||||||
:deep(.table-header-gray th) { background-color: #f8f9fb !important; color: #606266; }
|
:deep(.table-header-gray th) { background-color: #f8f9fb !important; color: #606266; }
|
||||||
.tag-sn { color: #409EFF; font-weight: bold; font-family: monospace; }
|
.tag-sn { color: #409EFF; font-weight: bold; font-family: monospace; }
|
||||||
.stock-num { font-weight: bold; font-size: 15px; }
|
.stock-num { font-weight: bold; font-size: 15px; }
|
||||||
.sum-tag { margin-left: 4px; transform: scale(0.9); }
|
|
||||||
.form-card { background: #fff; border-radius: 8px; margin-bottom: 20px; border: 1px solid #e4e7ed; overflow: hidden; }
|
.form-card { background: #fff; border-radius: 8px; margin-bottom: 20px; border: 1px solid #e4e7ed; overflow: hidden; }
|
||||||
.card-title { background: #fcfcfc; padding: 10px 20px; border-bottom: 1px solid #ebeef5; font-weight: 600; display: flex; align-items: center; gap: 8px; }
|
.card-title { background: #fcfcfc; padding: 10px 20px; border-bottom: 1px solid #ebeef5; font-weight: 600; display: flex; align-items: center; gap: 8px; }
|
||||||
.card-title .icon { font-size: 18px; }
|
.card-title .icon { font-size: 18px; }
|
||||||
@ -725,7 +639,6 @@ onMounted(() => fetchData())
|
|||||||
.clickable-text { color: #409EFF; cursor: pointer; font-weight: 500; text-decoration: underline; }
|
.clickable-text { color: #409EFF; cursor: pointer; font-weight: 500; text-decoration: underline; }
|
||||||
.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; }
|
||||||
|
|
||||||
.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; }
|
||||||
|
|||||||
@ -193,9 +193,7 @@
|
|||||||
|
|
||||||
<div class="form-card basic-card">
|
<div class="form-card basic-card">
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<el-icon class="icon">
|
<el-icon class="icon"><Box/></el-icon>
|
||||||
<Box/>
|
|
||||||
</el-icon>
|
|
||||||
<span>1. 基础信息</span>
|
<span>1. 基础信息</span>
|
||||||
<span class="sub-title" v-if="dialogStatus === 'create'"> (请先搜索选择半成品物料)</span>
|
<span class="sub-title" v-if="dialogStatus === 'create'"> (请先搜索选择半成品物料)</span>
|
||||||
</div>
|
</div>
|
||||||
@ -242,31 +240,11 @@
|
|||||||
|
|
||||||
<div class="read-only-grid">
|
<div class="read-only-grid">
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="8">
|
<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-form-item label="名称">
|
<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-input v-model="form.material_name" disabled class="is-text-view"/>
|
<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-form-item>
|
<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>
|
<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.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.unit" disabled 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" disabled class="is-text-view"/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -274,42 +252,22 @@
|
|||||||
|
|
||||||
<div class="form-card inbound-card">
|
<div class="form-card inbound-card">
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<el-icon class="icon">
|
<el-icon class="icon"><House/></el-icon>
|
||||||
<House/>
|
|
||||||
</el-icon>
|
|
||||||
<span>2. 入库详情</span>
|
<span>2. 入库详情</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="6">
|
<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-form-item label="编码/SKU" prop="sku">
|
<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-input v-model="form.sku" placeholder="系统自动生成" disabled/>
|
<el-col :span="6"><el-form-item label="条码" prop="barcode"><el-input v-model="form.barcode" placeholder="扫描条码"/></el-form-item></el-col>
|
||||||
</el-form-item>
|
<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>
|
|
||||||
<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="warehouse_location">
|
|
||||||
<el-input v-model="form.warehouse_location" placeholder="例如: B-01-01"/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<div class="identity-panel">
|
<div class="identity-panel">
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-col :span="24" style="margin-bottom: 8px;">
|
<el-col :span="24" style="margin-bottom: 8px;">
|
||||||
<el-radio-group v-model="entryMode" @change="handleEntryModeChange" :disabled="modeLocked"
|
<el-radio-group v-model="entryMode" @change="handleEntryModeChange" :disabled="modeLocked" size="small" class="custom-radio-group">
|
||||||
size="small" class="custom-radio-group">
|
|
||||||
<el-radio-button label="batch">按批号入库 (Batch)</el-radio-button>
|
<el-radio-button label="batch">按批号入库 (Batch)</el-radio-button>
|
||||||
<el-radio-button label="serial">按序列号入库 (SN)</el-radio-button>
|
<el-radio-button label="serial">按序列号入库 (SN)</el-radio-button>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
@ -320,24 +278,14 @@
|
|||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="批号" prop="batch_number">
|
<el-form-item label="批号" prop="batch_number">
|
||||||
<el-input
|
<el-input v-model="form.batch_number" :placeholder="entryMode === 'batch' ? '系统生成...' : '不可用'" :disabled="entryMode === 'serial'" clearable>
|
||||||
v-model="form.batch_number"
|
|
||||||
:placeholder="entryMode === 'batch' ? '系统生成...' : '不可用'"
|
|
||||||
:disabled="entryMode === 'serial'"
|
|
||||||
clearable
|
|
||||||
>
|
|
||||||
<template #prefix><span class="prefix-tag bn">BN</span></template>
|
<template #prefix><span class="prefix-tag bn">BN</span></template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="序列号" prop="serial_number">
|
<el-form-item label="序列号" prop="serial_number">
|
||||||
<el-input
|
<el-input v-model="form.serial_number" :placeholder="entryMode === 'serial' ? '请扫描 SN...' : '不可用'" :disabled="entryMode === 'batch'" clearable>
|
||||||
v-model="form.serial_number"
|
|
||||||
:placeholder="entryMode === 'serial' ? '请扫描 SN...' : '不可用'"
|
|
||||||
:disabled="entryMode === 'batch'"
|
|
||||||
clearable
|
|
||||||
>
|
|
||||||
<template #prefix><span class="prefix-tag sn">SN</span></template>
|
<template #prefix><span class="prefix-tag sn">SN</span></template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -348,22 +296,13 @@
|
|||||||
<el-row :gutter="20" style="margin-top: 10px;">
|
<el-row :gutter="20" style="margin-top: 10px;">
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<el-form-item label="入库数量" prop="in_quantity">
|
<el-form-item label="入库数量" prop="in_quantity">
|
||||||
<el-input-number v-model="form.in_quantity" :min="1" controls-position="right" style="width:100%"
|
<el-input-number v-model="form.in_quantity" :min="1" controls-position="right" style="width:100%" class="strong-input"/>
|
||||||
class="strong-input"/>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
|
||||||
<template v-if="dialogStatus === 'update'">
|
<template v-if="dialogStatus === 'update'">
|
||||||
<el-col :span="6">
|
<el-col :span="6"><el-form-item label="当前库存" prop="stock_quantity"><el-input-number v-model="form.stock_quantity" disabled style="width:100%" :controls="false"/></el-form-item></el-col>
|
||||||
<el-form-item label="当前库存" prop="stock_quantity">
|
<el-col :span="6"><el-form-item label="当前可用" prop="available_quantity"><el-input-number v-model="form.available_quantity" disabled style="width:100%" :controls="false"/></el-form-item></el-col>
|
||||||
<el-input-number v-model="form.stock_quantity" disabled style="width:100%" :controls="false"/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="6">
|
|
||||||
<el-form-item label="当前可用" prop="available_quantity">
|
|
||||||
<el-input-number v-model="form.available_quantity" disabled style="width:100%" :controls="false"/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<el-form-item label="库存状态" prop="status">
|
<el-form-item label="库存状态" prop="status">
|
||||||
<el-select v-model="form.status" style="width:100%">
|
<el-select v-model="form.status" style="width:100%">
|
||||||
@ -391,23 +330,10 @@
|
|||||||
<el-col :span="24">
|
<el-col :span="24">
|
||||||
<el-form-item label="到货图片" prop="arrival_photo">
|
<el-form-item label="到货图片" prop="arrival_photo">
|
||||||
<div class="upload-container">
|
<div class="upload-container">
|
||||||
<el-upload
|
<el-upload v-model:file-list="arrivalFileList" action="#" list-type="picture-card" multiple :http-request="(opts) => customUpload(opts, 'arrival_photo')" :on-preview="handlePreviewPicture" :on-remove="(file) => handleRemoveImage(file, 'arrival_photo')" :before-upload="beforeAvatarUpload">
|
||||||
v-model:file-list="arrivalFileList"
|
|
||||||
action="#"
|
|
||||||
list-type="picture-card"
|
|
||||||
multiple
|
|
||||||
:http-request="(opts) => customUpload(opts, 'arrival_photo')"
|
|
||||||
:on-preview="handlePreviewPicture"
|
|
||||||
:on-remove="(file) => handleRemoveImage(file, 'arrival_photo')"
|
|
||||||
:before-upload="beforeAvatarUpload"
|
|
||||||
>
|
|
||||||
<el-icon><Plus /></el-icon>
|
<el-icon><Plus /></el-icon>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
|
<div class="camera-card" @click="triggerCamera('arrival_photo')"><el-icon><Camera /></el-icon><span class="text">拍照</span></div>
|
||||||
<div class="camera-card" @click="triggerCamera('arrival_photo')">
|
|
||||||
<el-icon><Camera /></el-icon>
|
|
||||||
<span class="text">拍照</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<el-input v-model="form.arrival_photo" placeholder="图片列表" style="display:none;" />
|
<el-input v-model="form.arrival_photo" placeholder="图片列表" style="display:none;" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -416,33 +342,12 @@
|
|||||||
<el-col :span="24">
|
<el-col :span="24">
|
||||||
<el-form-item label="质量报告" prop="quality_report_link">
|
<el-form-item label="质量报告" prop="quality_report_link">
|
||||||
<div class="upload-container">
|
<div class="upload-container">
|
||||||
<el-upload
|
<el-upload v-model:file-list="reportFileList" action="#" list-type="picture-card" multiple :http-request="(opts) => customUpload(opts, 'quality_report_link')" :on-preview="handlePreviewPicture" :on-remove="(file) => handleRemoveImage(file, 'quality_report_link')" :before-upload="beforeAvatarUpload">
|
||||||
v-model:file-list="reportFileList"
|
|
||||||
action="#"
|
|
||||||
list-type="picture-card"
|
|
||||||
multiple
|
|
||||||
:http-request="(opts) => customUpload(opts, 'quality_report_link')"
|
|
||||||
:on-preview="handlePreviewPicture"
|
|
||||||
:on-remove="(file) => handleRemoveImage(file, 'quality_report_link')"
|
|
||||||
:before-upload="beforeAvatarUpload"
|
|
||||||
>
|
|
||||||
<el-icon><Plus /></el-icon>
|
<el-icon><Plus /></el-icon>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
|
<div class="camera-card" @click="triggerCamera('quality_report_link')"><el-icon><Camera /></el-icon><span class="text">拍照</span></div>
|
||||||
<div class="camera-card" @click="triggerCamera('quality_report_link')">
|
|
||||||
<el-icon><Camera /></el-icon>
|
|
||||||
<span class="text">拍照</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<el-input v-model="quality_report_url" placeholder="如有外部报告链接,请在此输入 (选填)" style="margin-top: 8px;" clearable><template #prefix><el-icon><Link /></el-icon></template></el-input>
|
||||||
<el-input
|
|
||||||
v-model="quality_report_url"
|
|
||||||
placeholder="如有外部报告链接,请在此输入 (选填)"
|
|
||||||
style="margin-top: 8px;"
|
|
||||||
clearable
|
|
||||||
>
|
|
||||||
<template #prefix><el-icon><Link /></el-icon></template>
|
|
||||||
</el-input>
|
|
||||||
<el-input v-model="form.quality_report_link" placeholder="列表" style="display:none;" />
|
<el-input v-model="form.quality_report_link" placeholder="列表" style="display:none;" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
@ -452,95 +357,41 @@
|
|||||||
|
|
||||||
<div class="form-card production-card">
|
<div class="form-card production-card">
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<el-icon class="icon">
|
<el-icon class="icon"><Setting/></el-icon>
|
||||||
<Setting/>
|
|
||||||
</el-icon>
|
|
||||||
<span>3. 生产与成本信息</span>
|
<span>3. 生产与成本信息</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="divider-text">生产任务信息</div>
|
<div class="divider-text">生产任务信息</div>
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="8">
|
<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-form-item label="工单号">
|
<el-col :span="8"><el-form-item label="BOM编号"><el-input v-model="form.bom_code" placeholder="BOM-xxx"/></el-form-item></el-col>
|
||||||
<el-input v-model="form.work_order_code" placeholder="WO-xxx"/>
|
<el-col :span="8"><el-form-item label="BOM版本"><el-input v-model="form.bom_version" placeholder="v1.0"/></el-form-item></el-col>
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="8">
|
|
||||||
<el-form-item label="BOM编号">
|
|
||||||
<el-input v-model="form.bom_code" placeholder="BOM-xxx"/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="8">
|
|
||||||
<el-form-item label="BOM版本">
|
|
||||||
<el-input v-model="form.bom_version" placeholder="v1.0"/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
<el-form-item label="生产负责人">
|
<el-form-item label="生产负责人">
|
||||||
<el-autocomplete
|
<el-autocomplete v-model="form.production_manager" :fetch-suggestions="querySearchManager" placeholder="输入或选择负责人" style="width: 100%" clearable :trigger-on-focus="true" @select="handleManagerSelect">
|
||||||
v-model="form.production_manager"
|
<template #default="{ item }"><span>{{ item.value }}</span></template>
|
||||||
:fetch-suggestions="querySearchManager"
|
|
||||||
placeholder="输入或选择负责人"
|
|
||||||
style="width: 100%"
|
|
||||||
clearable
|
|
||||||
:trigger-on-focus="true"
|
|
||||||
@select="handleManagerSelect"
|
|
||||||
>
|
|
||||||
<template #default="{ item }">
|
|
||||||
<span>{{ item.value }}</span>
|
|
||||||
</template>
|
|
||||||
</el-autocomplete>
|
</el-autocomplete>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="16">
|
<el-col :span="16">
|
||||||
<el-form-item label="生产时间">
|
<el-form-item label="生产时间">
|
||||||
<el-date-picker
|
<el-date-picker v-model="form.production_time_range" type="datetimerange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间" value-format="YYYY-MM-DD HH:mm:ss" style="width: 100%"/>
|
||||||
v-model="form.production_time_range"
|
|
||||||
type="datetimerange"
|
|
||||||
range-separator="至"
|
|
||||||
start-placeholder="开始时间"
|
|
||||||
end-placeholder="结束时间"
|
|
||||||
value-format="YYYY-MM-DD HH:mm:ss"
|
|
||||||
style="width: 100%"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<div class="divider-text">成本核算 (单件)</div>
|
<div class="divider-text">成本核算 (单件)</div>
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="8">
|
<el-col :span="8"><el-form-item label="原材料成本"><el-input-number v-model="form.raw_material_cost" :precision="2" controls-position="right" style="width:100%"/></el-form-item></el-col>
|
||||||
<el-form-item label="原材料成本">
|
<el-col :span="8"><el-form-item label="手动/工时"><el-input-number v-model="form.manual_cost" :precision="2" controls-position="right" style="width:100%"/></el-form-item></el-col>
|
||||||
<el-input-number v-model="form.raw_material_cost" :precision="2" controls-position="right"
|
<el-col :span="8"><el-form-item label="单件总成本"><el-input-number v-model="form.unit_total_cost" :precision="2" disabled :controls="false" style="width:100%" class="total-price-input"/></el-form-item></el-col>
|
||||||
style="width:100%">
|
|
||||||
</el-input-number>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="8">
|
|
||||||
<el-form-item label="手动/工时">
|
|
||||||
<el-input-number v-model="form.manual_cost" :precision="2" controls-position="right"
|
|
||||||
style="width:100%">
|
|
||||||
</el-input-number>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="8">
|
|
||||||
<el-form-item label="单件总成本">
|
|
||||||
<el-input-number v-model="form.unit_total_cost" :precision="2" disabled :controls="false"
|
|
||||||
style="width:100%" class="total-price-input">
|
|
||||||
</el-input-number>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-row :gutter="20" style="margin-top:10px">
|
<el-row :gutter="20" style="margin-top:10px">
|
||||||
<el-col :span="24">
|
<el-col :span="24"><el-form-item label="详情链接"><el-input v-model="form.detail_link" placeholder="外部生产系统详情页 http://"/></el-form-item></el-col>
|
||||||
<el-form-item label="详情链接">
|
|
||||||
<el-input v-model="form.detail_link" placeholder="外部生产系统详情页 http://"/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -558,49 +409,20 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<input
|
<input type="file" ref="cameraInputRef" accept="image/*" capture="environment" style="display: none" @change="handleCameraFile"/>
|
||||||
type="file"
|
<el-dialog v-model="dialogVisibleImage" append-to-body width="50%"><img style="width: 100%" :src="dialogImageUrl" alt="Preview Image" /></el-dialog>
|
||||||
ref="cameraInputRef"
|
<el-dialog v-model="printVisible" title="标签打印预览" width="400px" destroy-on-close append-to-body>
|
||||||
accept="image/*"
|
|
||||||
capture="environment"
|
|
||||||
style="display: none"
|
|
||||||
@change="handleCameraFile"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<el-dialog v-model="dialogVisibleImage" append-to-body width="50%">
|
|
||||||
<img style="width: 100%" :src="dialogImageUrl" alt="Preview Image" />
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<el-dialog
|
|
||||||
v-model="printVisible"
|
|
||||||
title="标签打印预览"
|
|
||||||
width="400px"
|
|
||||||
destroy-on-close
|
|
||||||
append-to-body
|
|
||||||
>
|
|
||||||
<div style="text-align: center;">
|
<div style="text-align: center;">
|
||||||
<div v-loading="printLoading" class="preview-box">
|
<div v-loading="printLoading" class="preview-box">
|
||||||
<img v-if="previewUrl" :src="previewUrl" alt="Label Preview" style="width: 100%; border: 1px solid #ccc;"/>
|
<img v-if="previewUrl" :src="previewUrl" alt="Label Preview" style="width: 100%; border: 1px solid #ccc;"/>
|
||||||
<div v-else class="empty-preview">正在生成预览...</div>
|
<div v-else class="empty-preview">正在生成预览...</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top: 20px; font-size: 14px; color: #666;">
|
<div style="margin-top: 20px; font-size: 14px; color: #666;"><p>打印机 IP: 192.168.9.205</p><p>尺寸: 40mm x 30mm</p></div>
|
||||||
<p>打印机 IP: 192.168.9.205</p>
|
|
||||||
<p>尺寸: 40mm x 30mm</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer"><el-button @click="printVisible = false">取消</el-button><el-button type="primary" :loading="printing" @click="confirmPrint"><el-icon><Printer/></el-icon>确认打印</el-button></div>
|
||||||
<el-button @click="printVisible = false">取消</el-button>
|
|
||||||
<el-button type="primary" :loading="printing" @click="confirmPrint">
|
|
||||||
<el-icon>
|
|
||||||
<Printer/>
|
|
||||||
</el-icon>
|
|
||||||
确认打印
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -616,7 +438,7 @@ import {
|
|||||||
deleteSemiInbound,
|
deleteSemiInbound,
|
||||||
searchMaterialBase
|
searchMaterialBase
|
||||||
} from '@/api/inbound/semi'
|
} from '@/api/inbound/semi'
|
||||||
import { uploadFile, deleteFile } from '@/api/inbound/buy' // 复用文件上传接口
|
import { uploadFile, deleteFile } from '@/api/inbound/buy'
|
||||||
import {getLabelPreview, executePrint} from '@/api/common/print'
|
import {getLabelPreview, executePrint} from '@/api/common/print'
|
||||||
|
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
@ -630,12 +452,7 @@ 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({
|
const queryParams = reactive({ page: 1, pageSize: 15, keyword: '', statuses: ['在库', '借库'] })
|
||||||
page: 1,
|
|
||||||
pageSize: 15,
|
|
||||||
keyword: '',
|
|
||||||
statuses: ['在库', '借库']
|
|
||||||
})
|
|
||||||
const materialOptions = ref<any[]>([])
|
const materialOptions = ref<any[]>([])
|
||||||
|
|
||||||
// 打印相关变量
|
// 打印相关变量
|
||||||
@ -652,7 +469,7 @@ const arrivalFileList = ref<any[]>([])
|
|||||||
const reportFileList = ref<any[]>([])
|
const reportFileList = ref<any[]>([])
|
||||||
const cameraInputRef = ref<HTMLInputElement | null>(null)
|
const cameraInputRef = ref<HTMLInputElement | null>(null)
|
||||||
const currentCameraField = ref<'arrival_photo' | 'quality_report_link'>('arrival_photo')
|
const currentCameraField = ref<'arrival_photo' | 'quality_report_link'>('arrival_photo')
|
||||||
const quality_report_url = ref('') // 外部链接输入
|
const quality_report_url = ref('')
|
||||||
|
|
||||||
const entryMode = ref('batch')
|
const entryMode = ref('batch')
|
||||||
const modeLocked = ref(false)
|
const modeLocked = ref(false)
|
||||||
@ -666,17 +483,13 @@ const baseColumns = [
|
|||||||
{prop: 'unit', label: '单位'},
|
{prop: 'unit', label: '单位'},
|
||||||
]
|
]
|
||||||
|
|
||||||
// 半成品特有列
|
|
||||||
const stockColumns = [
|
const stockColumns = [
|
||||||
{prop: 'id', label: 'ID', minWidth: '60'},
|
{prop: 'id', label: 'ID', minWidth: '60'},
|
||||||
{prop: 'base_id', label: 'BaseID', minWidth: '80'},
|
{prop: 'base_id', label: 'BaseID', minWidth: '80'},
|
||||||
{prop: 'sku', label: 'SKU', minWidth: '120'},
|
{prop: 'sku', label: 'SKU', minWidth: '120'},
|
||||||
{prop: 'inbound_date', label: '入库日期', minWidth: '120'},
|
{prop: 'inbound_date', label: '入库日期', minWidth: '120'},
|
||||||
{prop: 'barcode', label: '条码', minWidth: '120'},
|
{prop: 'barcode', label: '条码', minWidth: '120'},
|
||||||
|
|
||||||
// 修改:合并序列号和批号
|
|
||||||
{prop: 'sn_bn', label: '序列号/批号', minWidth: '160'},
|
{prop: 'sn_bn', label: '序列号/批号', minWidth: '160'},
|
||||||
|
|
||||||
{prop: 'status', label: '状态', minWidth: '100'},
|
{prop: 'status', label: '状态', minWidth: '100'},
|
||||||
{prop: 'quality_status', label: '质量状态', minWidth: '100'},
|
{prop: 'quality_status', label: '质量状态', minWidth: '100'},
|
||||||
{prop: 'qty_inbound', label: '入库量', minWidth: '100'},
|
{prop: 'qty_inbound', label: '入库量', minWidth: '100'},
|
||||||
@ -696,65 +509,26 @@ const stockColumns = [
|
|||||||
{prop: 'quality_report_link', label: '质量报告', minWidth: '100'},
|
{prop: 'quality_report_link', label: '质量报告', minWidth: '100'},
|
||||||
{prop: 'detail_link', label: '详情链接', minWidth: '100'},
|
{prop: 'detail_link', label: '详情链接', minWidth: '100'},
|
||||||
]
|
]
|
||||||
|
|
||||||
const allColumns = [...baseColumns, ...stockColumns]
|
const allColumns = [...baseColumns, ...stockColumns]
|
||||||
|
|
||||||
// 表头持久化
|
const STORAGE_KEY = 'stock_semi_visible_columns_v2'
|
||||||
const STORAGE_KEY = 'stock_semi_visible_columns_v2' // Update key to force refresh
|
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 = [
|
const getSavedColumns = () => { try { const saved = localStorage.getItem(STORAGE_KEY); return saved ? JSON.parse(saved) : defaultColumns } catch (e) { return 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 getSavedColumns = () => {
|
|
||||||
try {
|
|
||||||
const saved = localStorage.getItem(STORAGE_KEY)
|
|
||||||
return saved ? JSON.parse(saved) : defaultColumns
|
|
||||||
} catch (e) {
|
|
||||||
return defaultColumns
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const visibleColumnProps = ref(getSavedColumns())
|
const visibleColumnProps = ref(getSavedColumns())
|
||||||
|
watch(visibleColumnProps, (newVal) => { localStorage.setItem(STORAGE_KEY, JSON.stringify(newVal)) }, {deep: true})
|
||||||
watch(visibleColumnProps, (newVal) => {
|
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(newVal))
|
|
||||||
}, {deep: true})
|
|
||||||
|
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
id: undefined,
|
id: undefined, base_id: undefined as number | undefined, material_name: '', spec_model: '', category: '', unit: '', material_type: '',
|
||||||
base_id: undefined as number | undefined,
|
sku: '', barcode: '', in_date: '', serial_number: '', batch_number: '', status: '在库', quality_status: '合格',
|
||||||
material_name: '', spec_model: '', category: '', unit: '', material_type: '',
|
in_quantity: 1, stock_quantity: 1, available_quantity: 1, warehouse_location: '',
|
||||||
sku: '', barcode: '', in_date: '',
|
bom_code: '', bom_version: '', work_order_code: '', raw_material_cost: 0, manual_cost: 0, unit_total_cost: 0,
|
||||||
serial_number: '', batch_number: '',
|
production_manager: '', production_time_range: [] as string[], arrival_photo: [] as string[], quality_report_link: [] as string[], detail_link: ''
|
||||||
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: ''
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
// 历史记录管理器
|
// 历史记录管理器
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
const HISTORY_KEYS = {
|
const HISTORY_KEYS = { PRODUCTION_MANAGER: 'history_production_managers', MATERIAL: 'history_semi_materials' }
|
||||||
PRODUCTION_MANAGER: 'history_production_managers',
|
|
||||||
MATERIAL: 'history_semi_materials'
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveToHistory = (key: string, value: string) => {
|
const saveToHistory = (key: string, value: string) => {
|
||||||
if (!value) return
|
if (!value) return
|
||||||
try {
|
try {
|
||||||
@ -766,11 +540,7 @@ const saveToHistory = (key: string, value: string) => {
|
|||||||
localStorage.setItem(key, JSON.stringify(list))
|
localStorage.setItem(key, JSON.stringify(list))
|
||||||
} catch (e) { console.error('save history failed', e) }
|
} catch (e) { console.error('save history failed', e) }
|
||||||
}
|
}
|
||||||
|
const getHistoryList = (key: string): any[] => { try { return (JSON.parse(localStorage.getItem(key) || '[]')).map((v: string) => ({value: v})) } catch (e) { return [] } }
|
||||||
const getHistoryList = (key: string): any[] => {
|
|
||||||
try { return (JSON.parse(localStorage.getItem(key) || '[]')).map((v: string) => ({value: v})) } catch (e) { return [] }
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveMaterialHistory = (item: any) => {
|
const saveMaterialHistory = (item: any) => {
|
||||||
if (!item || !item.id) return
|
if (!item || !item.id) return
|
||||||
const key = HISTORY_KEYS.MATERIAL
|
const key = HISTORY_KEYS.MATERIAL
|
||||||
@ -782,17 +552,13 @@ const saveMaterialHistory = (item: any) => {
|
|||||||
localStorage.setItem(key, JSON.stringify(list))
|
localStorage.setItem(key, JSON.stringify(list))
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
const getMaterialHistory = () => { try { return JSON.parse(localStorage.getItem(HISTORY_KEYS.MATERIAL) || '[]') } catch (e) { return [] } }
|
||||||
const getMaterialHistory = () => {
|
|
||||||
try { return JSON.parse(localStorage.getItem(HISTORY_KEYS.MATERIAL) || '[]') } catch (e) { return [] }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
// Autocomplete Logic
|
// Autocomplete & Search Logic
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
const createFilter = (queryString: string) => { return (item: any) => (item.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0) }
|
const createFilter = (queryString: string) => { return (item: any) => (item.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0) }
|
||||||
const getTableDataUnique = (field: string) => { return Array.from(new Set(tableData.value.map((i: any) => i[field]).filter(Boolean))).map(i => ({value: i})) }
|
const getTableDataUnique = (field: string) => { return Array.from(new Set(tableData.value.map((i: any) => i[field]).filter(Boolean))).map(i => ({value: i})) }
|
||||||
|
|
||||||
const mixedSearch = (queryString: string, tableField: string, storageKey: string, cb: any) => {
|
const mixedSearch = (queryString: string, tableField: string, storageKey: string, cb: any) => {
|
||||||
const tableList = getTableDataUnique(tableField)
|
const tableList = getTableDataUnique(tableField)
|
||||||
const historyList = getHistoryList(storageKey)
|
const historyList = getHistoryList(storageKey)
|
||||||
@ -803,10 +569,39 @@ const mixedSearch = (queryString: string, tableField: string, storageKey: string
|
|||||||
const results = queryString ? allList.filter(createFilter(queryString)) : allList
|
const results = queryString ? allList.filter(createFilter(queryString)) : allList
|
||||||
cb(results)
|
cb(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
const querySearchManager = (qs: string, cb: any) => mixedSearch(qs, 'production_manager', HISTORY_KEYS.PRODUCTION_MANAGER, cb)
|
const querySearchManager = (qs: string, cb: any) => mixedSearch(qs, 'production_manager', HISTORY_KEYS.PRODUCTION_MANAGER, cb)
|
||||||
const handleManagerSelect = (item: any) => saveToHistory(HISTORY_KEYS.PRODUCTION_MANAGER, item.value)
|
const handleManagerSelect = (item: any) => saveToHistory(HISTORY_KEYS.PRODUCTION_MANAGER, item.value)
|
||||||
|
|
||||||
|
// ------------------------------------
|
||||||
|
// Material Search (Matches Buy.vue)
|
||||||
|
// ------------------------------------
|
||||||
|
const handleMaterialDropdownVisible = (visible: boolean) => { if (visible && materialOptions.value.length === 0) handleSearchMaterial('') }
|
||||||
|
const handleSearchMaterial = async (query: string) => {
|
||||||
|
searchLoading.value = true
|
||||||
|
try {
|
||||||
|
const res: any = await searchMaterialBase(query)
|
||||||
|
const apiResults = (res.data || []).map((i: any) => ({...i, isHistory: false}))
|
||||||
|
if (!query) {
|
||||||
|
const history = getMaterialHistory()
|
||||||
|
const historyIds = new Set(history.map((h: any) => h.id))
|
||||||
|
materialOptions.value = [...history, ...apiResults.filter((apiItem: any) => !historyIds.has(apiItem.id))]
|
||||||
|
} else { materialOptions.value = apiResults }
|
||||||
|
} finally { searchLoading.value = false }
|
||||||
|
}
|
||||||
|
const onMaterialSelected = (val: number) => {
|
||||||
|
const item = materialOptions.value.find(i => i.id === val)
|
||||||
|
if (item) {
|
||||||
|
saveMaterialHistory(item)
|
||||||
|
// Populate form fields
|
||||||
|
form.material_name = item.name
|
||||||
|
form.spec_model = item.spec
|
||||||
|
form.category = item.category
|
||||||
|
form.unit = item.unit
|
||||||
|
form.material_type = item.type
|
||||||
|
// Trigger batch/serial logic specific to Semi
|
||||||
|
checkHistoryAndSetMode(item.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
// Validation Logic
|
// Validation Logic
|
||||||
@ -822,13 +617,11 @@ const validateUnique = (rule: any, value: string, callback: any) => {
|
|||||||
if (isDuplicate) callback(new Error('编号重复'))
|
if (isDuplicate) callback(new Error('编号重复'))
|
||||||
else callback()
|
else callback()
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateIdentity = (rule: any, value: any, callback: any) => {
|
const validateIdentity = (rule: any, value: any, callback: any) => {
|
||||||
if (entryMode.value === 'serial' && !form.serial_number && rule.field === 'serial_number') callback(new Error('SN必填'))
|
if (entryMode.value === 'serial' && !form.serial_number && rule.field === 'serial_number') callback(new Error('SN必填'))
|
||||||
else if (entryMode.value === 'batch' && !form.batch_number && rule.field === 'batch_number') callback(new Error('批号必填'))
|
else if (entryMode.value === 'batch' && !form.batch_number && rule.field === 'batch_number') callback(new Error('批号必填'))
|
||||||
else callback()
|
else callback()
|
||||||
}
|
}
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
base_id: [{required: true, message: '请选择物料', trigger: 'change'}],
|
base_id: [{required: true, message: '请选择物料', trigger: 'change'}],
|
||||||
in_quantity: [{required: true, message: '请输入数量', trigger: 'blur'}],
|
in_quantity: [{required: true, message: '请输入数量', trigger: 'blur'}],
|
||||||
@ -846,64 +639,22 @@ const checkHistoryAndSetMode = async (baseId: number) => {
|
|||||||
if (historyItems.length > 0) {
|
if (historyItems.length > 0) {
|
||||||
modeLocked.value = true
|
modeLocked.value = true
|
||||||
const latest = historyItems.sort((a: any, b: any) => b.id - a.id)[0]
|
const latest = historyItems.sort((a: any, b: any) => b.id - a.id)[0]
|
||||||
if (latest.serial_number) {
|
if (latest.serial_number) { entryMode.value = 'serial'; form.serial_number = ''; form.batch_number = '' }
|
||||||
entryMode.value = 'serial'; form.serial_number = ''; form.batch_number = ''
|
else { entryMode.value = 'batch'; form.serial_number = ''; form.batch_number = incrementBatchNumber(latest.batch_number || '000000') }
|
||||||
} else {
|
} else { modeLocked.value = false; entryMode.value = 'batch'; form.batch_number = '000001' }
|
||||||
entryMode.value = 'batch'; form.serial_number = ''; form.batch_number = incrementBatchNumber(latest.batch_number || '000000')
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
modeLocked.value = false; entryMode.value = 'batch'; form.batch_number = '000001'
|
|
||||||
}
|
|
||||||
if(formRef.value) { formRef.value.clearValidate('serial_number'); formRef.value.clearValidate('batch_number') }
|
if(formRef.value) { formRef.value.clearValidate('serial_number'); formRef.value.clearValidate('batch_number') }
|
||||||
} catch (e) {
|
} catch (e) { modeLocked.value = false; entryMode.value = 'batch'; form.batch_number = '000001' }
|
||||||
modeLocked.value = false; entryMode.value = 'batch'; form.batch_number = '000001'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const incrementBatchNumber = (batchStr: string) => {
|
const incrementBatchNumber = (batchStr: string) => {
|
||||||
if (!batchStr || !/^\d+$/.test(batchStr)) return '000001'
|
if (!batchStr || !/^\d+$/.test(batchStr)) return '000001'
|
||||||
const num = parseInt(batchStr, 10)
|
const num = parseInt(batchStr, 10)
|
||||||
return (num + 1).toString().padStart(6, '0')
|
return (num + 1).toString().padStart(6, '0')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEntryModeChange = (val: string) => {
|
const handleEntryModeChange = (val: string) => {
|
||||||
if (val === 'batch') {
|
if (val === 'batch') { form.serial_number = ''; form.batch_number = '000001'; if(formRef.value) formRef.value.clearValidate('serial_number') }
|
||||||
form.serial_number = ''; form.batch_number = '000001'; if(formRef.value) formRef.value.clearValidate('serial_number')
|
else { form.batch_number = ''; if(formRef.value) formRef.value.clearValidate('batch_number') }
|
||||||
} else {
|
|
||||||
form.batch_number = ''; if(formRef.value) formRef.value.clearValidate('batch_number')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
watch(() => [form.raw_material_cost, form.manual_cost], () => { form.unit_total_cost = Number((form.raw_material_cost + form.manual_cost).toFixed(2)) })
|
||||||
const handleMaterialDropdownVisible = (visible: boolean) => { if (visible && materialOptions.value.length === 0) handleSearchMaterial('') }
|
|
||||||
const handleSearchMaterial = async (query: string) => {
|
|
||||||
searchLoading.value = true
|
|
||||||
try {
|
|
||||||
const res: any = await searchMaterialBase(query)
|
|
||||||
const apiResults = (res.data || []).map((i: any) => ({...i, isHistory: false}))
|
|
||||||
if (!query) {
|
|
||||||
const history = getMaterialHistory()
|
|
||||||
const historyIds = new Set(history.map((h: any) => h.id))
|
|
||||||
materialOptions.value = [...history, ...apiResults.filter((apiItem: any) => !historyIds.has(apiItem.id))]
|
|
||||||
} else { materialOptions.value = apiResults }
|
|
||||||
} finally { searchLoading.value = false }
|
|
||||||
}
|
|
||||||
|
|
||||||
const onMaterialSelected = (val: number) => {
|
|
||||||
const item = materialOptions.value.find(i => i.id === val)
|
|
||||||
if (item) {
|
|
||||||
saveMaterialHistory(item)
|
|
||||||
form.material_name = item.name
|
|
||||||
form.spec_model = item.spec
|
|
||||||
form.category = item.category
|
|
||||||
form.unit = item.unit
|
|
||||||
form.material_type = item.type
|
|
||||||
checkHistoryAndSetMode(item.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(() => [form.raw_material_cost, form.manual_cost], () => {
|
|
||||||
form.unit_total_cost = Number((form.raw_material_cost + form.manual_cost).toFixed(2))
|
|
||||||
})
|
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@ -930,8 +681,6 @@ const handleUpdate = (row: any) => {
|
|||||||
dialogStatus.value = 'update'
|
dialogStatus.value = 'update'
|
||||||
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, 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,
|
||||||
@ -942,159 +691,80 @@ const handleUpdate = (row: any) => {
|
|||||||
production_manager: row.production_manager,
|
production_manager: row.production_manager,
|
||||||
production_time_range: (row.production_start_time && row.production_end_time) ? [row.production_start_time, row.production_end_time] : [],
|
production_time_range: (row.production_start_time && row.production_end_time) ? [row.production_start_time, row.production_end_time] : [],
|
||||||
detail_link: row.detail_link,
|
detail_link: row.detail_link,
|
||||||
// 图片列表处理
|
arrival_photo: row.arrival_photo || [], quality_report_link: row.quality_report_link || []
|
||||||
arrival_photo: row.arrival_photo || [],
|
|
||||||
quality_report_link: row.quality_report_link || []
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 1. 同步到货图片
|
|
||||||
arrivalFileList.value = form.arrival_photo.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }))
|
arrivalFileList.value = form.arrival_photo.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }))
|
||||||
|
|
||||||
// 2. 分离质量报告中的图片和链接
|
|
||||||
const reports = form.quality_report_link || []
|
const reports = form.quality_report_link || []
|
||||||
const reportImgs = reports.filter(r => !isExternalLink(r))
|
const reportImgs = reports.filter(r => !isExternalLink(r))
|
||||||
const reportLinks = reports.filter(r => isExternalLink(r))
|
const reportLinks = reports.filter(r => isExternalLink(r))
|
||||||
|
|
||||||
reportFileList.value = reportImgs.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }))
|
reportFileList.value = reportImgs.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }))
|
||||||
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 = '' }
|
||||||
// 模式设定
|
else { entryMode.value = 'batch'; form.batch_number = row.batch_number; form.serial_number = '' }
|
||||||
if (row.serial_number) {
|
materialOptions.value = [{ id: row.base_id, name: row.material_name, spec: row.spec_model, category: row.category, isHistory: false }]
|
||||||
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 = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
materialOptions.value = [{ id: row.base_id, name: row.material_name, spec: row.spec_model, category: row.category }]
|
|
||||||
visible.value = true
|
visible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------
|
const getImageUrl = (url: string) => { if (!url) return ''; return url.startsWith('http') ? url : url }
|
||||||
// 图片上传、拍照、删除逻辑
|
const isExternalLink = (str: string) => { return str && (str.startsWith('http://') || str.startsWith('https://')) && !str.includes('/api/v1/common/files') }
|
||||||
// ------------------------------------
|
const getImagesOnly = (list: string[]) => { return !list ? [] : list.filter(item => !isExternalLink(item)) }
|
||||||
const getImageUrl = (url: string) => {
|
const hasExternalLink = (list: string[]) => { return !list ? false : list.some(item => isExternalLink(item)) }
|
||||||
if (!url) return ''
|
|
||||||
if (url.startsWith('http')) return url
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
|
|
||||||
const isExternalLink = (str: string) => {
|
|
||||||
return str && (str.startsWith('http://') || str.startsWith('https://')) && !str.includes('/api/v1/common/files')
|
|
||||||
}
|
|
||||||
|
|
||||||
const getImagesOnly = (list: string[]) => {
|
|
||||||
if (!list) return []
|
|
||||||
return list.filter(item => !isExternalLink(item))
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasExternalLink = (list: string[]) => {
|
|
||||||
if (!list) return false
|
|
||||||
return list.some(item => isExternalLink(item))
|
|
||||||
}
|
|
||||||
|
|
||||||
const beforeAvatarUpload = (rawFile: any) => {
|
const beforeAvatarUpload = (rawFile: any) => {
|
||||||
if (rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/png') { ElMessage.error('仅支持 JPG/PNG'); return false }
|
if (rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/png') { ElMessage.error('仅支持 JPG/PNG'); return false }
|
||||||
if (rawFile.size / 1024 / 1024 > 5) { ElMessage.error('图片不能超过 5MB'); return false }
|
if (rawFile.size / 1024 / 1024 > 5) { ElMessage.error('图片不能超过 5MB'); return false }
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const customUpload = async (options: any, targetField: 'arrival_photo' | 'quality_report_link') => {
|
const customUpload = async (options: any, targetField: 'arrival_photo' | 'quality_report_link') => {
|
||||||
const { file, onSuccess, onError } = options
|
const { file, onSuccess, onError } = options
|
||||||
const formData = new FormData()
|
const formData = new FormData(); formData.append('file', file)
|
||||||
formData.append('file', file)
|
|
||||||
try {
|
try {
|
||||||
const res: any = await uploadFile(formData)
|
const res: any = await uploadFile(formData)
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
const newUrl = res.data.url
|
const newUrl = res.data.url; form[targetField].push(newUrl); ElMessage.success('上传成功'); onSuccess(res)
|
||||||
form[targetField].push(newUrl)
|
} else { ElMessage.error(res.msg || '上传失败'); onError(new Error(res.msg)) }
|
||||||
ElMessage.success('上传成功')
|
|
||||||
onSuccess(res)
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.msg || '上传失败')
|
|
||||||
onError(new Error(res.msg))
|
|
||||||
}
|
|
||||||
} catch (e) { ElMessage.error('网络错误'); onError(e) }
|
} catch (e) { ElMessage.error('网络错误'); onError(e) }
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRemoveImage = async (uploadFile: any, targetField: 'arrival_photo' | 'quality_report_link') => {
|
const handleRemoveImage = async (uploadFile: any, targetField: 'arrival_photo' | 'quality_report_link') => {
|
||||||
try {
|
try {
|
||||||
const urlToRemove = form[targetField].find(u => getImageUrl(u) === uploadFile.url) || uploadFile.url
|
const urlToRemove = form[targetField].find(u => getImageUrl(u) === uploadFile.url) || uploadFile.url
|
||||||
form[targetField] = form[targetField].filter(u => u !== urlToRemove)
|
form[targetField] = form[targetField].filter(u => u !== urlToRemove)
|
||||||
if (!isExternalLink(urlToRemove)) {
|
if (!isExternalLink(urlToRemove)) { const filename = urlToRemove.split('/').pop(); if (filename) await deleteFile(filename) }
|
||||||
const filename = urlToRemove.split('/').pop()
|
|
||||||
if (filename) await deleteFile(filename)
|
|
||||||
}
|
|
||||||
ElMessage.success('已删除')
|
ElMessage.success('已删除')
|
||||||
} catch (e) { console.error(e) }
|
} catch (e) { console.error(e) }
|
||||||
}
|
}
|
||||||
|
const handlePreviewPicture = (uploadFile: any) => { dialogImageUrl.value = uploadFile.url!; dialogVisibleImage.value = true }
|
||||||
const handlePreviewPicture = (uploadFile: any) => {
|
const triggerCamera = (field: 'arrival_photo' | 'quality_report_link') => { currentCameraField.value = field; if (cameraInputRef.value) cameraInputRef.value.click() }
|
||||||
dialogImageUrl.value = uploadFile.url!
|
|
||||||
dialogVisibleImage.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const triggerCamera = (field: 'arrival_photo' | 'quality_report_link') => {
|
|
||||||
currentCameraField.value = field
|
|
||||||
if (cameraInputRef.value) cameraInputRef.value.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCameraFile = async (event: Event) => {
|
const handleCameraFile = async (event: Event) => {
|
||||||
const input = event.target as HTMLInputElement
|
const input = event.target as HTMLInputElement
|
||||||
if (input.files && input.files[0]) {
|
if (input.files && input.files[0]) {
|
||||||
const file = input.files[0]
|
const file = input.files[0]
|
||||||
if (!beforeAvatarUpload(file)) { input.value = ''; return }
|
if (!beforeAvatarUpload(file)) { input.value = ''; return }
|
||||||
const formData = new FormData()
|
const formData = new FormData(); formData.append('file', file)
|
||||||
formData.append('file', file)
|
|
||||||
const loadingMsg = ElMessage.loading({ message: '照片上传中...', duration: 0 })
|
const loadingMsg = ElMessage.loading({ message: '照片上传中...', duration: 0 })
|
||||||
try {
|
try {
|
||||||
const res: any = await uploadFile(formData)
|
const res: any = await uploadFile(formData)
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
const newUrl = res.data.url
|
const newUrl = res.data.url; const field = currentCameraField.value
|
||||||
const field = currentCameraField.value
|
|
||||||
form[field].push(newUrl)
|
form[field].push(newUrl)
|
||||||
if (field === 'arrival_photo') {
|
if (field === 'arrival_photo') arrivalFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) })
|
||||||
arrivalFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) })
|
else reportFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) })
|
||||||
} else {
|
|
||||||
reportFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) })
|
|
||||||
}
|
|
||||||
ElMessage.success('拍照上传成功')
|
ElMessage.success('拍照上传成功')
|
||||||
} else { ElMessage.error(res.msg || '上传失败') }
|
} else { ElMessage.error(res.msg || '上传失败') }
|
||||||
} catch (e) { ElMessage.error('网络错误,上传失败') }
|
} catch (e) { ElMessage.error('网络错误,上传失败') } finally { loadingMsg.close(); input.value = '' }
|
||||||
finally { loadingMsg.close(); input.value = '' }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------
|
|
||||||
// Submit Logic
|
|
||||||
// ------------------------------------
|
|
||||||
const submitForm = async () => {
|
const submitForm = async () => {
|
||||||
if (!formRef.value) return
|
if (!formRef.value) return
|
||||||
await formRef.value.validate(async (valid: boolean) => {
|
await formRef.value.validate(async (valid: boolean) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
|
|
||||||
// 合并外部链接到 quality_report_link 数组
|
|
||||||
const finalReportList = [...form.quality_report_link]
|
const finalReportList = [...form.quality_report_link]
|
||||||
if (quality_report_url.value && !finalReportList.includes(quality_report_url.value)) {
|
if (quality_report_url.value && !finalReportList.includes(quality_report_url.value)) finalReportList.push(quality_report_url.value)
|
||||||
finalReportList.push(quality_report_url.value)
|
|
||||||
}
|
|
||||||
// 确保如果是修改模式下清空了输入框,只保留图片(重新构建只包含图片+当前输入链接的列表)
|
|
||||||
const onlyImages = finalReportList.filter(item => !isExternalLink(item))
|
const onlyImages = finalReportList.filter(item => !isExternalLink(item))
|
||||||
if (quality_report_url.value) {
|
if (quality_report_url.value) onlyImages.push(quality_report_url.value)
|
||||||
onlyImages.push(quality_report_url.value)
|
const payload: any = { ...form, quality_report_link: onlyImages, in_quantity: Number(form.in_quantity), raw_material_cost: Number(form.raw_material_cost), manual_cost: Number(form.manual_cost), production_start_time: form.production_time_range?.[0] || null, production_end_time: form.production_time_range?.[1] || null }
|
||||||
}
|
|
||||||
|
|
||||||
const payload: any = {
|
|
||||||
...form,
|
|
||||||
quality_report_link: onlyImages,
|
|
||||||
in_quantity: Number(form.in_quantity),
|
|
||||||
raw_material_cost: Number(form.raw_material_cost),
|
|
||||||
manual_cost: Number(form.manual_cost),
|
|
||||||
production_start_time: form.production_time_range?.[0] || null,
|
|
||||||
production_end_time: form.production_time_range?.[1] || null
|
|
||||||
}
|
|
||||||
delete payload.production_time_range
|
delete payload.production_time_range
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (dialogStatus.value === 'create') {
|
if (dialogStatus.value === 'create') {
|
||||||
const res: any = await createSemiInbound(payload)
|
const res: any = await createSemiInbound(payload)
|
||||||
@ -1105,68 +775,25 @@ const submitForm = async () => {
|
|||||||
try { await executePrint(newItem); ElMessage.success('打印指令已发送') }
|
try { await executePrint(newItem); ElMessage.success('打印指令已发送') }
|
||||||
catch (printErr: any) { ElMessage.warning('入库成功,但自动打印失败:' + (printErr.msg || '未知错误')) }
|
catch (printErr: any) { ElMessage.warning('入库成功,但自动打印失败:' + (printErr.msg || '未知错误')) }
|
||||||
}
|
}
|
||||||
} else {
|
} else { await updateSemiInbound(form.id!, payload); ElMessage.success('更新成功') }
|
||||||
await updateSemiInbound(form.id!, payload)
|
|
||||||
ElMessage.success('更新成功')
|
|
||||||
}
|
|
||||||
saveToHistory(HISTORY_KEYS.PRODUCTION_MANAGER, form.production_manager)
|
saveToHistory(HISTORY_KEYS.PRODUCTION_MANAGER, form.production_manager)
|
||||||
await fetchData()
|
await fetchData(); visible.value = false
|
||||||
visible.value = false
|
} catch (e: any) { ElMessage.error(e.msg || '操作失败') } finally { submitting.value = false }
|
||||||
} catch (e: any) { ElMessage.error(e.msg || '操作失败') }
|
|
||||||
finally { submitting.value = false }
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDelete = async (row: any) => {
|
const handleDelete = async (row: any) => { try { await deleteSemiInbound(row.id); ElMessage.success('删除成功'); fetchData() } catch (e) { ElMessage.error('删除失败') } }
|
||||||
try { await deleteSemiInbound(row.id); ElMessage.success('删除成功'); fetchData() }
|
|
||||||
catch (e) { ElMessage.error('删除失败') }
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePrint = async (row: any) => {
|
const handlePrint = async (row: any) => {
|
||||||
printVisible.value = true; printLoading.value = true; previewUrl.value = ''
|
printVisible.value = true; printLoading.value = true; previewUrl.value = ''
|
||||||
const printData = {
|
currentPrintData.value = { global_print_id: row.global_print_id, material_name: row.material_name, spec_model: row.spec_model, category: row.category, material_type: row.material_type, warehouse_loc: row.warehouse_loc, serial_number: row.serial_number, batch_number: row.batch_number, sku: row.sku }
|
||||||
global_print_id: row.global_print_id,
|
try { const res: any = await getLabelPreview(currentPrintData.value); previewUrl.value = res.data } catch (e) { ElMessage.error('预览生成失败') } finally { printLoading.value = false }
|
||||||
material_name: row.material_name,
|
|
||||||
spec_model: row.spec_model,
|
|
||||||
category: row.category,
|
|
||||||
material_type: row.material_type,
|
|
||||||
warehouse_loc: row.warehouse_loc,
|
|
||||||
serial_number: row.serial_number,
|
|
||||||
batch_number: row.batch_number,
|
|
||||||
sku: row.sku
|
|
||||||
}
|
|
||||||
currentPrintData.value = printData
|
|
||||||
try { const res: any = await getLabelPreview(printData); previewUrl.value = res.data }
|
|
||||||
catch (e) { ElMessage.error('预览生成失败') } finally { printLoading.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 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 = []
|
materialOptions.value = []; arrivalFileList.value = []; reportFileList.value = []; quality_report_url.value = ''
|
||||||
arrivalFileList.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: '' })
|
||||||
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: ''
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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' }
|
||||||
const formatMoney = (val: any) => { const num = Number(val); return isNaN(num) ? '-' : `¥ ${num.toFixed(2)}` }
|
const formatMoney = (val: any) => { const num = Number(val); return isNaN(num) ? '-' : `¥ ${num.toFixed(2)}` }
|
||||||
@ -1175,16 +802,11 @@ onMounted(() => fetchData())
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 全局布局 */
|
|
||||||
.semi-module { background: #f5f7fa; padding: 20px; min-height: 100vh; }
|
.semi-module { background: #f5f7fa; padding: 20px; min-height: 100vh; }
|
||||||
|
|
||||||
/* 顶部工具栏 */
|
|
||||||
.header-tools { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; background: #fff; padding: 15px 20px; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05); }
|
.header-tools { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; background: #fff; padding: 15px 20px; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05); }
|
||||||
.left-tools { display: flex; gap: 10px; align-items: center; flex: 1; }
|
.left-tools { display: flex; gap: 10px; align-items: center; flex: 1; }
|
||||||
.right-tools { display: flex; gap: 10px; align-items: center; }
|
.right-tools { display: flex; gap: 10px; align-items: center; }
|
||||||
.action-btn { font-weight: 500; }
|
.action-btn { font-weight: 500; }
|
||||||
|
|
||||||
/* 表格美化 */
|
|
||||||
.modern-table { border-radius: 8px; overflow: hidden; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05); }
|
.modern-table { border-radius: 8px; overflow: hidden; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05); }
|
||||||
:deep(.table-header-gray th) { background-color: #f8f9fb !important; color: #606266; font-weight: 600; height: 50px; }
|
:deep(.table-header-gray th) { background-color: #f8f9fb !important; color: #606266; font-weight: 600; height: 50px; }
|
||||||
.tag-sn { color: #409EFF; font-weight: bold; font-family: monospace; }
|
.tag-sn { color: #409EFF; font-weight: bold; font-family: monospace; }
|
||||||
@ -1192,38 +814,30 @@ onMounted(() => fetchData())
|
|||||||
.money-text { font-family: 'Consolas', monospace; color: #303133; }
|
.money-text { font-family: 'Consolas', monospace; color: #303133; }
|
||||||
.stock-num { font-weight: bold; color: #333; font-size: 15px; }
|
.stock-num { font-weight: bold; color: #333; font-size: 15px; }
|
||||||
.avail-num { font-weight: bold; color: #67C23A; font-size: 15px; }
|
.avail-num { font-weight: bold; color: #67C23A; font-size: 15px; }
|
||||||
.sum-tag { margin-left: 4px; transform: scale(0.9); }
|
|
||||||
.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; }
|
.stylish-form .form-card { background: #fff; border-radius: 8px; border: 1px solid #e4e7ed; margin-bottom: 20px; overflow: hidden; }
|
||||||
.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; }
|
||||||
.card-content { padding: 20px; }
|
.card-content { padding: 20px; }
|
||||||
|
|
||||||
.basic-card { border-left: 4px solid #409EFF; }
|
.basic-card { border-left: 4px solid #409EFF; }
|
||||||
.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; }
|
||||||
.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; }
|
.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; }
|
||||||
.is-text-view :deep(.el-input__inner) { color: #606266; font-weight: 500; }
|
.is-text-view :deep(.el-input__inner) { color: #606266; font-weight: 500; }
|
||||||
|
|
||||||
.inbound-card { border-left: 4px solid #67C23A; }
|
.inbound-card { border-left: 4px solid #67C23A; }
|
||||||
.production-card { border-left: 4px solid #E6A23C; }
|
.production-card { border-left: 4px solid #E6A23C; }
|
||||||
.production-card .card-title .icon { color: #E6A23C; }
|
.production-card .card-title .icon { color: #E6A23C; }
|
||||||
|
|
||||||
.identity-panel { background: #fffbf0; border: 1px dashed #e6a23c; border-radius: 6px; padding: 15px; margin-bottom: 20px; }
|
.identity-panel { background: #fffbf0; border: 1px dashed #e6a23c; border-radius: 6px; padding: 15px; margin-bottom: 20px; }
|
||||||
.custom-radio-group { margin-bottom: 10px; }
|
.custom-radio-group { margin-bottom: 10px; }
|
||||||
.locked-msg { font-size: 12px; color: #e6a23c; margin-left: 15px; }
|
.locked-msg { font-size: 12px; color: #e6a23c; margin-left: 15px; }
|
||||||
.prefix-tag { font-weight: bold; font-size: 12px; padding: 0 5px; border-radius: 4px; }
|
.prefix-tag { font-weight: bold; font-size: 12px; padding: 0 5px; border-radius: 4px; }
|
||||||
.prefix-tag.bn { color: #67C23A; background: #f0f9eb; }
|
.prefix-tag.bn { color: #67C23A; background: #f0f9eb; }
|
||||||
.prefix-tag.sn { color: #409EFF; background: #ecf5ff; }
|
.prefix-tag.sn { color: #409EFF; background: #ecf5ff; }
|
||||||
|
|
||||||
.divider-text { display: flex; align-items: center; text-align: center; margin: 30px 0 20px; color: #909399; font-size: 14px; font-weight: 500; }
|
.divider-text { display: flex; align-items: center; text-align: center; margin: 30px 0 20px; color: #909399; font-size: 14px; font-weight: 500; }
|
||||||
.divider-text::before, .divider-text::after { content: ''; flex: 1; border-bottom: 1px solid #ebeef5; }
|
.divider-text::before, .divider-text::after { content: ''; flex: 1; border-bottom: 1px solid #ebeef5; }
|
||||||
.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; }
|
.dialog-footer { display: flex; justify-content: flex-end; gap: 15px; margin-top: 20px; }
|
||||||
.option-item { display: flex; justify-content: space-between; width: 100%; align-items: center;}
|
.option-item { display: flex; justify-content: space-between; width: 100%; align-items: center;}
|
||||||
.opt-name { font-weight: bold; }
|
.opt-name { font-weight: bold; }
|
||||||
@ -1234,19 +848,10 @@ onMounted(() => fetchData())
|
|||||||
.more-images-badge { margin-left: 5px; background: #909399; color: #fff; border-radius: 10px; padding: 0 6px; font-size: 12px; }
|
.more-images-badge { margin-left: 5px; background: #909399; color: #fff; border-radius: 10px; padding: 0 6px; font-size: 12px; }
|
||||||
.clickable-text { color: #409EFF; cursor: pointer; font-weight: 500; text-decoration: underline; }
|
.clickable-text { color: #409EFF; cursor: pointer; font-weight: 500; text-decoration: underline; }
|
||||||
.clickable-text:hover { color: #66b1ff; }
|
.clickable-text:hover { color: #66b1ff; }
|
||||||
|
|
||||||
/* 容器:让上传组件和拍照按钮横向排列 */
|
|
||||||
.upload-container { display: flex; flex-wrap: wrap; gap: 8px; }
|
.upload-container { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||||
/* 隐藏 el-upload 的 input 样式,只展示图片卡片 */
|
|
||||||
: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; }
|
||||||
|
.camera-card { width: 100px; height: 100px; background-color: #fbfdff; border: 1px dashed #c0ccda; border-radius: 6px; box-sizing: border-box; display: flex; flex-direction: column; justify-content: center; align-items: center; cursor: pointer; transition: all 0.3s; color: #8c939d; }
|
||||||
.camera-card {
|
|
||||||
width: 100px; height: 100px;
|
|
||||||
background-color: #fbfdff; border: 1px dashed #c0ccda; border-radius: 6px;
|
|
||||||
box-sizing: border-box; display: flex; flex-direction: column; justify-content: center; align-items: center;
|
|
||||||
cursor: pointer; transition: all 0.3s; color: #8c939d;
|
|
||||||
}
|
|
||||||
.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; }
|
||||||
|
|||||||
Reference in New Issue
Block a user