diff --git a/inventory-backend/app/__init__.py b/inventory-backend/app/__init__.py index c6fa8f2..a35c4e2 100644 --- a/inventory-backend/app/__init__.py +++ b/inventory-backend/app/__init__.py @@ -200,6 +200,19 @@ def create_app(): except ImportError as 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. 预加载数据模型 # ========================================================= @@ -227,4 +240,4 @@ def create_app(): except Exception as e: print(f"⚠️ 模型预加载发生未知错误: {e}") - return app \ No newline at end of file + return app diff --git a/inventory-backend/app/api/v1/__init__.py b/inventory-backend/app/api/v1/__init__.py index 0313ea0..a467a45 100644 --- a/inventory-backend/app/api/v1/__init__.py +++ b/inventory-backend/app/api/v1/__init__.py @@ -1,7 +1,9 @@ from flask import Blueprint from .inbound import inbound_bp from .bom import bom_bp +from .common import common_bp v1_bp = Blueprint('v1', __name__) v1_bp.register_blueprint(inbound_bp, url_prefix='/inbound') v1_bp.register_blueprint(bom_bp, url_prefix='/bom') +v1_bp.register_blueprint(common_bp, url_prefix='/common') diff --git a/inventory-backend/app/api/v1/common/__init__.py b/inventory-backend/app/api/v1/common/__init__.py index e69de29..408784d 100644 --- a/inventory-backend/app/api/v1/common/__init__.py +++ b/inventory-backend/app/api/v1/common/__init__.py @@ -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 diff --git a/inventory-backend/app/api/v1/common/search.py b/inventory-backend/app/api/v1/common/search.py new file mode 100644 index 0000000..a765183 --- /dev/null +++ b/inventory-backend/app/api/v1/common/search.py @@ -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}) diff --git a/inventory-web/src/views/dashboard/index.vue b/inventory-web/src/views/dashboard/index.vue index e8f21e5..54f0589 100644 --- a/inventory-web/src/views/dashboard/index.vue +++ b/inventory-web/src/views/dashboard/index.vue @@ -18,6 +18,32 @@

IRIS 库存管理系统

+ +
+ + + + +
+

请选择您要进行的业务操作:

@@ -215,8 +241,9 @@ import { useRouter } from 'vue-router' // 1. 引入 User Store 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 request from '@/utils/request' import { ElMessage, ElMessageBox } from 'element-plus' import { getWarehouseTree, createWarehouse, updateWarehouse, deleteWarehouse, batchDeleteWarehouse, batchGenerateWarehouse } from '@/api/common/warehouse' @@ -234,6 +261,61 @@ const printerForm = reactive({ }) const loading = ref(false) +// 全局搜索相关 +const globalSearchText = ref('') + +const getBadgeType = (type: string) => { + const map: Record = { + '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 () => { try { loading.value = true