Compare commits
4 Commits
b002c50d81
...
40e405becd
| Author | SHA1 | Date | |
|---|---|---|---|
| 40e405becd | |||
| d6ae9499db | |||
| ec71cb24f4 | |||
| 9fa471f68a |
@ -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. 预加载数据模型
|
||||||
# =========================================================
|
# =========================================================
|
||||||
|
|||||||
@ -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')
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
99
inventory-backend/app/api/v1/common/search.py
Normal file
99
inventory-backend/app/api/v1/common/search.py
Normal 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})
|
||||||
@ -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)
|
||||||
@ -218,14 +244,15 @@ def get_stock_list():
|
|||||||
|
|
||||||
# 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.product_name.ilike(f'%{keyword}%'),
|
StockProduct.base.has(MaterialBase.name.ilike(f'%{keyword}%')),
|
||||||
StockProduct.spec_model.ilike(f'%{keyword}%'),
|
StockProduct.base.has(MaterialBase.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()
|
rows = q.all()
|
||||||
@ -234,14 +261,13 @@ def get_stock_list():
|
|||||||
d['stock_type'] = 'product'
|
d['stock_type'] = 'product'
|
||||||
d['type'] = 'product'
|
d['type'] = 'product'
|
||||||
d['typeLabel'] = '成品'
|
d['typeLabel'] = '成品'
|
||||||
d['name'] = d.get('product_name', d.get('name', ''))
|
d['name'] = d.get('material_name', d.get('name', ''))
|
||||||
d['standard'] = d.get('spec_model', d.get('standard', ''))
|
d['standard'] = d.get('spec_model', d.get('standard', ''))
|
||||||
d['available_quantity'] = d.get('qty_available', d.get('available_quantity', 0))
|
d['available_quantity'] = d.get('qty_available', d.get('available_quantity', 0))
|
||||||
all_items.append(d)
|
all_items.append(d)
|
||||||
except Exception:
|
|
||||||
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')
|
||||||
# ★ 新增: 提取备注字段
|
# ★ 新增: 提取备注字段
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,42 +1617,43 @@ 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) => {
|
|
||||||
// 1. 尝试获取各种可能的数据结构
|
|
||||||
let rawData = res?.data?.list ?? res?.data?.items ?? res?.data ?? [];
|
let rawData = res?.data?.list ?? res?.data?.items ?? res?.data ?? [];
|
||||||
|
|
||||||
// 2. 如果后端聪明地直接返回了单条数据(对象),把它包装成数组
|
|
||||||
if (!Array.isArray(rawData) && typeof rawData === 'object' && rawData !== null) {
|
if (!Array.isArray(rawData) && typeof rawData === 'object' && rawData !== null) {
|
||||||
rawData = [rawData];
|
rawData = [rawData];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 终极兜底,确保 rows 绝对是数组
|
|
||||||
const rows = Array.isArray(rawData) ? rawData : [];
|
const rows = Array.isArray(rawData) ? rawData : [];
|
||||||
console.log('兼容处理后的数组数据:', rows);
|
|
||||||
|
|
||||||
// 4. 寻找目标行并弹窗
|
// 3. 去掉危险的 rows[0] 兜底,严格匹配 ID
|
||||||
const targetRow = rows.find((r: any) => r.id === editId) || rows[0];
|
const targetRow = rows.find((r: any) => r.id === editId);
|
||||||
|
|
||||||
if (targetRow) {
|
if (targetRow) {
|
||||||
console.log('找到目标物料,准备弹窗:', targetRow);
|
console.log('找到精准目标物料,准备弹窗:', targetRow);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
handleEdit(targetRow);
|
handleEdit(targetRow);
|
||||||
}, 800);
|
}, 800);
|
||||||
} else {
|
} else {
|
||||||
console.warn('未在返回数据中找到该物料');
|
console.warn('未能在搜索结果中匹配到对应 ID 的物料,可能 keyword 与 ID 不匹配');
|
||||||
}
|
}
|
||||||
}).catch((error: any) => {
|
}).catch((error: any) => {
|
||||||
console.error('自动获取物料详情失败', error);
|
console.error('自动获取物料详情失败', error);
|
||||||
|
|||||||
@ -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')
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user