feat: 新增首页全局搜索功能,支持跨模块多词搜索
This commit is contained in:
@ -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. 预加载数据模型
|
||||||
# =========================================================
|
# =========================================================
|
||||||
@ -227,4 +240,4 @@ def create_app():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ 模型预加载发生未知错误: {e}")
|
print(f"⚠️ 模型预加载发生未知错误: {e}")
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|||||||
@ -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})
|
||||||
@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user