Compare commits

4 Commits

12 changed files with 421 additions and 67 deletions

View File

@ -200,6 +200,19 @@ def create_app():
except ImportError as e: except ImportError as e:
print(f"❌ 错误: Warehouse 模块导入失败: {e}") print(f"❌ 错误: Warehouse 模块导入失败: {e}")
# -----------------------------------------------------
# 2.12 注册通用聚合搜索模块 (Common - Global Search)
# -----------------------------------------------------
try:
from app.api.v1.common import common_bp
# 标准: /api/v1/common/global-search
app.register_blueprint(common_bp, url_prefix='/api/v1/common')
# 兼容: /api/common/global-search
app.register_blueprint(common_bp, url_prefix='/api/common', name='common_legacy')
print("✅ Common 模块注册成功")
except ImportError as e:
print(f"❌ 错误: Common 模块导入失败: {e}")
# ========================================================= # =========================================================
# 3. 预加载数据模型 # 3. 预加载数据模型
# ========================================================= # =========================================================

View File

@ -1,7 +1,9 @@
from flask import Blueprint from flask import Blueprint
from .inbound import inbound_bp from .inbound import inbound_bp
from .bom import bom_bp from .bom import bom_bp
from .common import common_bp
v1_bp = Blueprint('v1', __name__) v1_bp = Blueprint('v1', __name__)
v1_bp.register_blueprint(inbound_bp, url_prefix='/inbound') v1_bp.register_blueprint(inbound_bp, url_prefix='/inbound')
v1_bp.register_blueprint(bom_bp, url_prefix='/bom') v1_bp.register_blueprint(bom_bp, url_prefix='/bom')
v1_bp.register_blueprint(common_bp, url_prefix='/common')

View File

@ -0,0 +1,7 @@
# inventory-backend/app/api/v1/common/__init__.py
from flask import Blueprint
common_bp = Blueprint('common', __name__)
# 导入子模块,使其路由装饰器注册到 common_bp
from . import search

View File

@ -0,0 +1,99 @@
# inventory-backend/app/api/v1/common/search.py
from flask import jsonify, request
from . import common_bp
from app.models import MaterialBase
from app.models.inbound.buy import StockBuy
from app.models.bom import BomTable
from app.extensions import db
@common_bp.route('/global-search', methods=['GET'])
def global_search():
"""
全局聚合搜索接口(多词 AND 模式,无数量限制)
入参: keyword (字符串,支持空格分词,多词必须同时匹配)
搜索范围: 基础物料、采购库、BOM配方
"""
keyword = request.args.get('keyword', request.args.get('q', '')).strip()
keywords = keyword.split()
if not keywords:
return jsonify({"code": 200, "data": []})
merged_list = []
# ── 1. 基础物料 (MaterialBase) ──────────────────────────
# 真实字段: name, common_name, spec_model, category
material_conditions = []
for kw in keywords:
kw_term = f'%{kw}%'
material_conditions.append(
db.or_(
MaterialBase.name.ilike(kw_term),
MaterialBase.common_name.ilike(kw_term),
MaterialBase.spec_model.ilike(kw_term),
MaterialBase.category.ilike(kw_term)
)
)
bases = MaterialBase.query.filter(db.and_(*material_conditions)).all()
for b in bases:
merged_list.append({
"id": b.id,
"type": "material",
"title": b.name,
"subtitle": b.spec_model or b.common_name or '无规格型号',
"badge": "基础物料",
"extra": {"category": b.category or ''}
})
# ── 2. 采购库 (StockBuy) ─────────────────────────────────
# 真实字段: barcode, sku (通过 join 搜索关联的 MaterialBase.name)
stock_conditions = []
for kw in keywords:
kw_term = f'%{kw}%'
stock_conditions.append(
db.or_(
MaterialBase.name.ilike(kw_term),
StockBuy.barcode.ilike(kw_term),
StockBuy.sku.ilike(kw_term)
)
)
stocks = StockBuy.query.join(MaterialBase, StockBuy.base_id == MaterialBase.id).filter(
db.and_(*stock_conditions)
).all()
for s in stocks:
merged_list.append({
"id": s.base_id,
"stock_id": s.id,
"type": "stock_buy",
"title": s.base.name if s.base else '未知物料',
"subtitle": f"条码: {s.barcode or ''} | 库存: {s.stock_quantity}",
"badge": "采购库",
"extra": {"barcode": s.barcode or '', "status": s.status or ''}
})
# ── 3. BOM 配方 (BomTable) ──────────────────────────────
# 真实字段: bom_no, version
bom_conditions = []
for kw in keywords:
kw_term = f'%{kw}%'
bom_conditions.append(
db.or_(
BomTable.bom_no.ilike(kw_term),
BomTable.version.ilike(kw_term)
)
)
boms = BomTable.query.filter(db.and_(*bom_conditions)).all()
for bom in boms:
parent_name = bom.parent.name if bom.parent else ''
merged_list.append({
"id": bom.id,
"bom_no": bom.bom_no,
"type": "bom",
"title": f"{bom.bom_no} ({bom.version})",
"subtitle": f"父件: {parent_name}" if parent_name else f"版本: {bom.version}",
"badge": "配方BOM",
"extra": {"version": bom.version, "parent_id": bom.parent_id}
})
return jsonify({"code": 200, "data": merged_list})

View File

@ -22,14 +22,18 @@ except ImportError:
SysUser = None SysUser = None
# 尝试导入半成品和成品 # 尝试导入半成品和成品
import logging
try: try:
from app.models.inbound.semi import StockSemi from app.models.inbound.semi import StockSemi
except ImportError: except Exception as e:
logging.error(f"❌ 致命错误StockSemi 模型导入失败: {e}")
StockSemi = None StockSemi = None
try: try:
from app.models.inbound.product import StockProduct from app.models.inbound.product import StockProduct
except ImportError: except Exception as e:
logging.error(f"❌ 致命错误StockProduct 模型导入失败: {e}")
StockProduct = None StockProduct = None
@ -79,28 +83,50 @@ def get_stock_info(uuid_or_barcode):
根据 uuid 或 barcode 查询库存信息 根据 uuid 或 barcode 查询库存信息
返回: (item, source_table, stock_id) 返回: (item, source_table, stock_id)
""" """
# 清洗输入:去掉前后空格和换行符
uuid_or_barcode = str(uuid_or_barcode).strip()
# 1. 成品 # 1. 成品
if StockProduct: if StockProduct:
print(f"🔍 [QUERY DEBUG] 正在成品表搜关键词: {uuid_or_barcode}")
item = StockProduct.query.filter( item = StockProduct.query.filter(
db.or_(StockProduct.barcode == uuid_or_barcode, StockProduct.sku == uuid_or_barcode) db.or_(
StockProduct.barcode.ilike(f"%{uuid_or_barcode}%"),
StockProduct.sku.ilike(f"%{uuid_or_barcode}%"),
StockProduct.serial_number.ilike(f"%{uuid_or_barcode}%")
)
).first() ).first()
if item: if item:
print(f"✅ [QUERY DEBUG] 命中成品! ID={item.id}, SKU={item.sku}")
return (item, 'stock_product', item.id) return (item, 'stock_product', item.id)
else:
print(f"❌ [QUERY DEBUG] 成品表查询结束,无匹配项")
# 2. 半成品 # 2. 半成品
if StockSemi: if StockSemi:
print(f"🔍 [QUERY DEBUG] 正在半成品表搜关键词: {uuid_or_barcode}")
item = StockSemi.query.filter( item = StockSemi.query.filter(
db.or_(StockSemi.barcode == uuid_or_barcode, StockSemi.sku == uuid_or_barcode) db.or_(
StockSemi.barcode.ilike(f"%{uuid_or_barcode}%"),
StockSemi.sku.ilike(f"%{uuid_or_barcode}%"),
StockSemi.serial_number.ilike(f"%{uuid_or_barcode}%")
)
).first() ).first()
if item: if item:
print(f"✅ [QUERY DEBUG] 命中半成品! ID={item.id}, SKU={item.sku}")
return (item, 'stock_semi', item.id) return (item, 'stock_semi', item.id)
# 3. 采购件 # 3. 采购件
if StockBuy: if StockBuy:
print(f"🔍 [QUERY DEBUG] 正在采购件表搜关键词: {uuid_or_barcode}")
item = StockBuy.query.filter( item = StockBuy.query.filter(
db.or_(StockBuy.barcode == uuid_or_barcode, StockBuy.sku == uuid_or_barcode) db.or_(
StockBuy.barcode.ilike(f"%{uuid_or_barcode}%"),
StockBuy.sku.ilike(f"%{uuid_or_barcode}%")
)
).first() ).first()
if item: if item:
print(f"✅ [QUERY DEBUG] 命中采购件! ID={item.id}, SKU={item.sku}")
return (item, 'stock_buy', item.id) return (item, 'stock_buy', item.id)
return (None, None, None) return (None, None, None)
@ -216,32 +242,32 @@ def get_stock_list():
except Exception: except Exception:
pass pass
# 3. 成品 # 3. 成品
if StockProduct: if StockProduct:
try: q = StockProduct.query.filter(StockProduct.stock_quantity > 0)
q = StockProduct.query.filter(StockProduct.stock_quantity > 0) if keyword:
if keyword: q = q.filter(
q = q.filter( db.or_(
db.or_( StockProduct.base.has(MaterialBase.name.ilike(f'%{keyword}%')),
StockProduct.product_name.ilike(f'%{keyword}%'), StockProduct.base.has(MaterialBase.spec_model.ilike(f'%{keyword}%')),
StockProduct.spec_model.ilike(f'%{keyword}%'), StockProduct.sku.ilike(f'%{keyword}%'),
StockProduct.sku.ilike(f'%{keyword}%') StockProduct.barcode.ilike(f'%{keyword}%'),
) StockProduct.serial_number.ilike(f'%{keyword}%')
) )
rows = q.all() )
for item in rows: rows = q.all()
d = item.to_dict() for item in rows:
d['stock_type'] = 'product' d = item.to_dict()
d['type'] = 'product' d['stock_type'] = 'product'
d['typeLabel'] = '成品' d['type'] = 'product'
d['name'] = d.get('product_name', d.get('name', '')) d['typeLabel'] = '成品'
d['standard'] = d.get('spec_model', d.get('standard', '')) d['name'] = d.get('material_name', d.get('name', ''))
d['available_quantity'] = d.get('qty_available', d.get('available_quantity', 0)) d['standard'] = d.get('spec_model', d.get('standard', ''))
all_items.append(d) d['available_quantity'] = d.get('qty_available', d.get('available_quantity', 0))
except Exception: all_items.append(d)
pass
total = len(all_items) total = len(all_items)
start = (page - 1) * pageSize start = (page - 1) * pageSize
end = start + pageSize end = start + pageSize
paged = all_items[start:end] paged = all_items[start:end]
@ -361,6 +387,7 @@ def add_draft():
data = request.json data = request.json
user_id = _normalize_user_id() user_id = _normalize_user_id()
uuid = data.get('uuid') uuid = data.get('uuid')
print(f"🚀 [SCAN DEBUG] 后端实际接收到的 UUID 原文: |{uuid}| (长度: {len(str(uuid)) if uuid else 0})")
quantity = float(data.get('quantity', 1)) quantity = float(data.get('quantity', 1))
session_id = data.get('session_id') session_id = data.get('session_id')
# ★ 新增: 提取备注字段 # ★ 新增: 提取备注字段

View File

@ -234,7 +234,7 @@ const handleLogout = () => {
<footer v-if="!isLoginPage" class="app-footer"> <footer v-if="!isLoginPage" class="app-footer">
<span class="version-tag"> <span class="version-tag">
<el-icon style="vertical-align: middle; margin-right: 4px"><InfoFilled /></el-icon> <el-icon style="vertical-align: middle; margin-right: 4px"><InfoFilled /></el-icon>
当前版本:V3.13(4.23部署基础信息导入 当前版本:V3.14(4.27部署
</span> </span>
</footer> </footer>

View File

@ -202,6 +202,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted, computed, nextTick } from 'vue' import { ref, reactive, onMounted, computed, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus' import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus'
import { Plus, Search } from '@element-plus/icons-vue' import { Plus, Search } from '@element-plus/icons-vue'
import { getBomList, getBomDetail, saveBom, deleteBom } from '@/api/bom' import { getBomList, getBomDetail, saveBom, deleteBom } from '@/api/bom'
@ -231,6 +232,7 @@ interface ChildRow {
} }
const userStore = useUserStore() const userStore = useUserStore()
const route = useRoute()
const loading = ref(false) const loading = ref(false)
const dialogVisible = ref(false) const dialogVisible = ref(false)
const saving = ref(false) const saving = ref(false)
@ -746,6 +748,55 @@ const submitForm = async () => {
onMounted(() => { onMounted(() => {
fetchBomList() fetchBomList()
// 【新增】:处理外部跳转自动打开 BOM带查重保护
if (route.query.create_for_id) {
const parentId = Number(route.query.create_for_id);
const parentName = (route.query.parent_name as string) || '';
const parentSpec = (route.query.parent_spec as string) || '';
// 把名称填入背景搜索框让背后的表格也只显示相关的BOM
searchKeyword.value = parentName;
// 延迟等待基础渲染
setTimeout(() => {
// 1. 先用 keyword 查询是否已有该父件的 BOM
getBomList({ keyword: parentName }).then((res: any) => {
const rows = res.data || [];
// 严格校验 parent_id
const existingBom = rows.find((b: any) => b.parent_id === parentId);
if (existingBom) {
// ★ 情况 A已经有BOM了直接打开编辑弹窗并拉取历史数据
ElMessage.success('检测到该物料已有 BOM已自动为您打开编辑');
handleEdit(existingBom);
} else {
// ★ 情况 B还没建过BOM打开新建并注入父件
handleCreate();
// 强行注入父件远程搜索选项
parentOptions.value = [{
id: parentId,
name: parentName,
spec: parentSpec
}];
// 给表单赋值
form.parent_id = parentId;
// 触发联动逻辑(自动带出版本和生成编号)
if (typeof onParentChange === 'function') {
setTimeout(() => {
onParentChange(parentId);
}, 100);
}
}
}).catch(err => {
console.error('BOM 查重失败', err);
ElMessage.error('获取 BOM 状态失败,请手动操作');
});
}, 300);
}
}) })
</script> </script>

View File

@ -18,6 +18,32 @@
<div class="card-body"> <div class="card-body">
<h2>IRIS 库存管理系统</h2> <h2>IRIS 库存管理系统</h2>
<div style="display: flex; justify-content: center; margin: 20px 0 30px;">
<el-autocomplete
v-model="globalSearchText"
:fetch-suggestions="queryGlobalSearch"
placeholder="全局搜索:输入物料名称、规格、条码或 BOM 编号..."
style="width: 60%; max-width: 600px;"
size="large"
clearable
@select="handleSearchSelect"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
<template #default="{ item }">
<div style="display: flex; justify-content: space-between; align-items: center; line-height: 1.5; padding: 4px 0;">
<div>
<div style="font-size: 14px; font-weight: bold; color: #303133;">{{ item.title }}</div>
<div style="font-size: 12px; color: #909399;">{{ item.subtitle }}</div>
</div>
<el-tag size="small" :type="getBadgeType(item.type)">{{ item.badge }}</el-tag>
</div>
</template>
</el-autocomplete>
</div>
<p class="subtitle">请选择您要进行的业务操作</p> <p class="subtitle">请选择您要进行的业务操作</p>
<div class="action-buttons"> <div class="action-buttons">
@ -215,8 +241,9 @@ import { useRouter } from 'vue-router'
// 1. 引入 User Store // 1. 引入 User Store
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
// 引入需要的图标 // 引入需要的图标
import { Box, TrendCharts, ShoppingCart, Operation, Setting, Location, Plus, Edit, Delete, Close } from '@element-plus/icons-vue' import { Box, TrendCharts, ShoppingCart, Operation, Setting, Location, Plus, Edit, Delete, Close, Search } from '@element-plus/icons-vue'
import { getPrinterConfig, updatePrinterConfig } from '@/api/common/print' import { getPrinterConfig, updatePrinterConfig } from '@/api/common/print'
import request from '@/utils/request'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { getWarehouseTree, createWarehouse, updateWarehouse, deleteWarehouse, batchDeleteWarehouse, batchGenerateWarehouse } from '@/api/common/warehouse' import { getWarehouseTree, createWarehouse, updateWarehouse, deleteWarehouse, batchDeleteWarehouse, batchGenerateWarehouse } from '@/api/common/warehouse'
@ -234,6 +261,61 @@ const printerForm = reactive({
}) })
const loading = ref(false) const loading = ref(false)
// 全局搜索相关
const globalSearchText = ref('')
const getBadgeType = (type: string) => {
const map: Record<string, string> = {
'material': 'success',
'stock_buy': 'primary',
'bom': 'warning'
}
return map[type] || 'info'
}
const queryGlobalSearch = async (queryString: string, cb: (data: any[]) => void) => {
if (!queryString || queryString.trim() === '') {
cb([])
return
}
try {
const res: any = await request({
url: '/v1/common/global-search',
method: 'get',
params: { keyword: queryString.trim() }
})
if (res.code === 200 && res.data) {
cb(res.data)
} else {
cb([])
}
} catch (error) {
console.error('全局搜索失败:', error)
cb([])
}
}
const handleSearchSelect = (item: any) => {
globalSearchText.value = ''
if (item.type === 'material') {
router.push({
path: '/material/index',
query: { edit_id: item.id, keyword: item.title }
})
} else if (item.type === 'stock_buy') {
router.push({
path: '/inventory/buy',
query: { keyword: item.title }
})
} else if (item.type === 'bom') {
router.push({
path: '/bom',
query: { keyword: item.title }
})
}
}
const openPrinterDialog = async () => { const openPrinterDialog = async () => {
try { try {
loading.value = true loading.value = true

View File

@ -326,7 +326,6 @@
<el-dialog <el-dialog
v-model="dialog.visible" v-model="dialog.visible"
:title="dialog.title"
width="700px" width="700px"
append-to-body append-to-body
destroy-on-close destroy-on-close
@ -335,6 +334,20 @@
:close-on-press-escape="!isUploading" :close-on-press-escape="!isUploading"
:show-close="!isUploading" :show-close="!isUploading"
> >
<template #header>
<div style="display: flex; align-items: center; justify-content: space-between; padding-right: 20px;">
<span style="font-size: 18px; font-weight: 500;">{{ dialog.title }}</span>
<el-link
v-if="form.id"
type="success"
:underline="false"
style="font-size: 14px;"
@click="createBomForMaterial"
>
<el-icon style="margin-right: 4px"><Plus /></el-icon>加入或查看BOM
</el-link>
</div>
</template>
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px"> <el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
<el-row> <el-row>
@ -597,8 +610,9 @@ import { Plus, Document, Refresh, Setting, Rank, Camera, Link, Download, Bell, C
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus'; import { ElMessage, ElMessageBox, ElLoading } from 'element-plus';
import type { FormInstance, FormRules } from 'element-plus'; import type { FormInstance, FormRules } from 'element-plus';
import { useUserStore } from '@/stores/user'; import { useUserStore } from '@/stores/user';
import { useRoute } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
const route = useRoute(); const route = useRoute();
const router = useRouter();
import { import {
listMaterialBase, listMaterialBase,
@ -1307,6 +1321,22 @@ const cancel = () => {
resetForm(); resetForm();
}; };
// 快速基于此物料查看/创建 BOM
const createBomForMaterial = () => {
if (!form.value.id) {
return ElMessage.warning('请先保存物料基础信息后再操作');
}
const routeUrl = router.resolve({
path: '/bom',
query: {
create_for_id: form.value.id,
parent_name: form.value.name,
parent_spec: form.value.spec
}
});
window.open(routeUrl.href, '_blank');
};
const resetForm = () => { const resetForm = () => {
form.value = JSON.parse(JSON.stringify(initForm)); form.value = JSON.parse(JSON.stringify(initForm));
fileListImage.value = []; fileListImage.value = [];
@ -1587,47 +1617,48 @@ const resetAdvancedFilter = () => {
}; };
onMounted(() => { onMounted(() => {
// 1. 修复背景联动:直接对 reactive 对象赋值
if (route.query.keyword) {
queryParams.keyword = route.query.keyword as string;
queryParams.searchField = 'all';
}
// 先根据权限初始化列显示状态 // 先根据权限初始化列显示状态
initColumnPermissions(); initColumnPermissions();
// 此时 getList 会带着正确的 keyword 向后端请求过滤后的数据
getList(); getList();
getOptionsList(); getOptionsList();
// 【优化】:处理外部跳转自动打开编辑弹窗 // 2. 修复弹窗锁定逻辑
console.log('--- 准备检测外部跳转参数 ---', route.query); console.log('--- 准备检测外部跳转参数 ---', route.query);
if (route.query.edit_id) { if (route.query.edit_id) {
const editId = Number(route.query.edit_id); const editId = Number(route.query.edit_id);
console.log('检测到 edit_id:', editId); const searchKeyword = (route.query.keyword as string) || '';
console.log('检测到 edit_id:', editId, '使用 keyword 搜索:', searchKeyword);
// 为了防止 API 不支持直接传 id我们改用 keyword 搜索(因为大部分 list 接口都支持 keyword // 改用 keyword 而不是无效的 id 去向后端请求数据,确保目标物料在返回的列表中
// 或者直接请求列表,拿到第一页数据进行比对 listMaterialBase({ page: 1, pageSize: 50, keyword: searchKeyword }).then((res: any) => {
listMaterialBase({ page: 1, pageSize: 50, id: editId }).then((res: any) => { let rawData = res?.data?.list ?? res?.data?.items ?? res?.data ?? [];
// 1. 尝试获取各种可能的数据结构 if (!Array.isArray(rawData) && typeof rawData === 'object' && rawData !== null) {
let rawData = res?.data?.list ?? res?.data?.items ?? res?.data ?? []; rawData = [rawData];
}
const rows = Array.isArray(rawData) ? rawData : [];
// 2. 如果后端聪明地直接返回了单条数据(对象),把它包装成数组 // 3. 去掉危险的 rows[0] 兜底,严格匹配 ID
if (!Array.isArray(rawData) && typeof rawData === 'object' && rawData !== null) { const targetRow = rows.find((r: any) => r.id === editId);
rawData = [rawData];
}
// 3. 终极兜底,确保 rows 绝对是数组 if (targetRow) {
const rows = Array.isArray(rawData) ? rawData : []; console.log('找到精准目标物料,准备弹窗:', targetRow);
console.log('兼容处理后的数组数据:', rows); setTimeout(() => {
handleEdit(targetRow);
// 4. 寻找目标行并弹窗 }, 800);
const targetRow = rows.find((r: any) => r.id === editId) || rows[0]; } else {
console.warn('未能在搜索结果中匹配到对应 ID 的物料,可能 keyword 与 ID 不匹配');
if (targetRow) { }
console.log('找到目标物料,准备弹窗:', targetRow); }).catch((error: any) => {
setTimeout(() => { console.error('自动获取物料详情失败', error);
handleEdit(targetRow); });
}, 800); }
} else {
console.warn('未在返回数据中找到该物料');
}
}).catch((error: any) => {
console.error('自动获取物料详情失败', error);
});
}
}); });
</script> </script>

View File

@ -1414,7 +1414,9 @@ const openMaterialInNewTab = () => {
const routeUrl = router.resolve({ const routeUrl = router.resolve({
path: '/material', path: '/material',
query: { query: {
edit_id: form.base_id edit_id: form.base_id,
// 【新增】:优先传递规格型号,如果没有则传名称,用于背景表格过滤
keyword: form.spec_model || form.material_name || ''
} }
}) })
window.open(routeUrl.href, '_blank') window.open(routeUrl.href, '_blank')

View File

@ -421,7 +421,12 @@
</div> </div>
<div class="form-card production-card"> <div class="form-card production-card">
<div class="card-title"><el-icon class="icon"><Setting /></el-icon><span>3. 生产与销售信息</span></div> <div class="card-title">
<el-icon class="icon"><Setting /></el-icon><span>3. 生产与销售信息</span>
<el-link type="success" :underline="false" style="margin-left: 15px; font-size: 13px;" @click="createBomForMaterial">
<el-icon style="margin-right: 4px"><Plus /></el-icon>加入或查看BOM
</el-link>
</div>
<div class="card-content"> <div class="card-content">
<el-row :gutter="24"> <el-row :gutter="24">
@ -557,6 +562,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted, watch, computed } from 'vue' import { ref, reactive, onMounted, watch, computed } from 'vue'
import { Plus, Setting, Refresh, Search, Box, House, Link, InfoFilled, Printer, Camera, Picture } from '@element-plus/icons-vue' import { Plus, Setting, Refresh, Search, Box, House, Link, InfoFilled, Printer, Camera, Picture } from '@element-plus/icons-vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElLoading } from 'element-plus' import { ElMessage, ElLoading } from 'element-plus'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import request from '@/utils/request' import request from '@/utils/request'
@ -614,6 +620,7 @@ const vLoadmore = {
} }
const userStore = useUserStore() const userStore = useUserStore()
const router = useRouter()
const loading = ref(false) const loading = ref(false)
const submitting = ref(false) const submitting = ref(false)
const visible = ref(false) const visible = ref(false)
@ -1263,6 +1270,20 @@ const handleScannerConfirm = (result: string) => {
ElMessage.success('序列号已提取') ElMessage.success('序列号已提取')
} }
// 快速基于此物料创建 BOM
const createBomForMaterial = () => {
if (!form.base_id) return ElMessage.warning('请先锁定物料基础信息')
const routeUrl = router.resolve({
path: '/bom',
query: {
create_for_id: form.base_id,
parent_name: form.material_name,
parent_spec: form.spec_model
}
})
window.open(routeUrl.href, '_blank')
}
const submitForm = async () => { const submitForm = async () => {
await formRef.value.validate(async (valid: boolean) => { await formRef.value.validate(async (valid: boolean) => {
if(valid) { if(valid) {

View File

@ -489,6 +489,9 @@
<div style="display: flex; align-items: center;"> <div style="display: flex; align-items: center;">
<el-icon class="icon"><Setting/></el-icon> <el-icon class="icon"><Setting/></el-icon>
<span>3. 生产与成本信息</span> <span>3. 生产与成本信息</span>
<el-link type="success" :underline="false" style="margin-left: 15px; font-size: 13px;" @click="createBomForMaterial">
<el-icon style="margin-right: 4px"><Plus /></el-icon>加入或查看BOM
</el-link>
</div> </div>
</div> </div>
<div class="card-content"> <div class="card-content">
@ -614,6 +617,7 @@
<script setup lang="ts"> <script setup lang="ts">
import {ref, reactive, onMounted, watch, computed} from 'vue' import {ref, reactive, onMounted, watch, computed} from 'vue'
import {Plus, Setting, Refresh, Search, Lock, Box, House, InfoFilled, Link, Printer, Camera, Picture} from '@element-plus/icons-vue' import {Plus, Setting, Refresh, Search, Lock, Box, House, InfoFilled, Link, Printer, Camera, Picture} from '@element-plus/icons-vue'
import { useRouter } from 'vue-router'
import {ElMessage, ElLoading} from 'element-plus' import {ElMessage, ElLoading} from 'element-plus'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import request from '@/utils/request' import request from '@/utils/request'
@ -673,6 +677,7 @@ const vLoadmore = {
// 状态与变量 // 状态与变量
// ------------------------------------ // ------------------------------------
const userStore = useUserStore() const userStore = useUserStore()
const router = useRouter()
const loading = ref(false) const loading = ref(false)
const submitting = ref(false) const submitting = ref(false)
const visible = ref(false) const visible = ref(false)
@ -1346,6 +1351,20 @@ const handleScannerConfirm = (result: string) => {
ElMessage.success('序列号已提取') ElMessage.success('序列号已提取')
} }
// 快速基于此物料创建 BOM
const createBomForMaterial = () => {
if (!form.base_id) return ElMessage.warning('请先锁定物料基础信息')
const routeUrl = router.resolve({
path: '/bom',
query: {
create_for_id: form.base_id,
parent_name: form.material_name,
parent_spec: form.spec_model
}
})
window.open(routeUrl.href, '_blank')
}
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) => {