diff --git a/inventory-backend/app/api/v1/inbound/product.py b/inventory-backend/app/api/v1/inbound/product.py index 870c038..51ab9eb 100644 --- a/inventory-backend/app/api/v1/inbound/product.py +++ b/inventory-backend/app/api/v1/inbound/product.py @@ -92,12 +92,14 @@ def search_base(): """ try: keyword = request.args.get('keyword', '') + page = request.args.get('page', 1, type=int) # 调用 Service 层已修复的 search_base_material 方法 - data = ProductInboundService.search_base_material(keyword) + result = ProductInboundService.search_base_material(keyword, page) # 字段级脱敏 user_permissions = get_current_user_permissions() - filtered_data = [filter_item_by_permissions(item, user_permissions) for item in data] - return jsonify({"code": 200, "msg": "success", "data": filtered_data}) + if result.get('items'): + result['items'] = [filter_item_by_permissions(item, user_permissions) for item in result['items']] + return jsonify({"code": 200, "msg": "success", "data": result}) except Exception as e: # 捕获异常并打印堆栈,方便调试 traceback.print_exc() @@ -146,7 +148,7 @@ def get_list(): company = request.args.get('company', '') result = ProductInboundService.get_list( - page, limit, keyword, statuses, + page, limit, keyword, statuses, category, material_type, company ) # 字段级脱敏 @@ -337,3 +339,20 @@ def get_options(): return jsonify({"code": 200, "msg": "success", "data": data}) except Exception as e: return jsonify({"code": 500, "msg": str(e)}), 500 + + +# ------------------------------------------------------------------ +# 8. 获取历史生产负责人建议 (新增) +# ------------------------------------------------------------------ +@inbound_product_bp.route('/suggestions/managers', methods=['GET']) +@permission_required('inbound_product') +def get_manager_history(): + base_id = request.args.get('base_id', type=int) + if not base_id: + return jsonify({"code": 400, "msg": "缺少 base_id"}), 400 + try: + data = ProductInboundService.get_history_managers(base_id) + return jsonify({"code": 200, "msg": "success", "data": data}) + except Exception as e: + traceback.print_exc() + return jsonify({"code": 500, "msg": str(e)}), 500 \ No newline at end of file diff --git a/inventory-backend/app/api/v1/inbound/semi.py b/inventory-backend/app/api/v1/inbound/semi.py index 4e9b940..cb20d12 100644 --- a/inventory-backend/app/api/v1/inbound/semi.py +++ b/inventory-backend/app/api/v1/inbound/semi.py @@ -92,15 +92,17 @@ def search_base(): """ try: keyword = request.args.get('keyword', '') - # 这里复用 Service 中的搜索逻辑 - data = SemiInboundService.search_base_material(keyword) + page = request.args.get('page', 1, type=int) + # 这里复用 Service 中的搜索逻辑 (已修改接收 page) + result = SemiInboundService.search_base_material(keyword, page) # 字段级脱敏 user_permissions = get_current_user_permissions() - filtered_data = [filter_item_by_permissions(item, user_permissions) for item in data] + if result.get('items'): + result['items'] = [filter_item_by_permissions(item, user_permissions) for item in result['items']] return jsonify({ "code": 200, "msg": "success", - "data": filtered_data + "data": result }) except Exception as e: traceback.print_exc() @@ -337,3 +339,19 @@ def get_options(): return jsonify({"code": 200, "msg": "success", "data": data}) except Exception as e: return jsonify({"code": 500, "msg": str(e)}), 500 + +# ------------------------------------------------------------------ +# 8. 获取历史生产负责人建议 (新增) +# ------------------------------------------------------------------ +@inbound_semi_bp.route('/suggestions/managers', methods=['GET']) +@permission_required('inbound_semi') +def get_manager_history(): + base_id = request.args.get('base_id', type=int) + if not base_id: + return jsonify({"code": 400, "msg": "缺少 base_id"}), 400 + try: + data = SemiInboundService.get_history_managers(base_id) + return jsonify({"code": 200, "msg": "success", "data": data}) + except Exception as e: + traceback.print_exc() + return jsonify({"code": 500, "msg": str(e)}), 500 \ No newline at end of file diff --git a/inventory-backend/app/services/inbound/product_service.py b/inventory-backend/app/services/inbound/product_service.py index 0163eed..0b285d8 100644 --- a/inventory-backend/app/services/inbound/product_service.py +++ b/inventory-backend/app/services/inbound/product_service.py @@ -7,7 +7,6 @@ from sqlalchemy import or_, func, text, and_ import traceback import json - class ProductInboundService: # ============================================================ @@ -26,10 +25,10 @@ class ProductInboundService: raise ValueError(f"序列号【{serial_number}】已存在!被成品 [{occupied_name}] 占用,请核查。") # ============================================================ - # 1. 基础物料搜索 + # 1. 基础物料搜索 (已修改支持分页) # ============================================================ @staticmethod - def search_base_material(keyword): + def search_base_material(keyword, page=1, limit=50): try: query = MaterialBase.query.filter(MaterialBase.is_enabled == True) if keyword: @@ -41,9 +40,10 @@ class ProductInboundService: MaterialBase.company_name.ilike(kw) # [新增] ) ) - query = query.order_by(MaterialBase.id.desc()).limit(20) + query = query.order_by(MaterialBase.id.desc()) + pagination = query.paginate(page=page, per_page=limit, error_out=False) results = [] - for item in query.all(): + for item in pagination.items: results.append({ 'id': item.id, 'company_name': item.company_name, # [新增] @@ -54,10 +54,15 @@ class ProductInboundService: 'type': item.material_type, 'status': '启用' }) - return results + return { + "items": results, + "total": pagination.total, + "page": page, + "has_next": pagination.has_next + } except Exception: traceback.print_exc() - return [] + return {"items": [], "total": 0, "page": 1, "has_next": False} # ============================================================ # 1.5 BOM 搜索逻辑 @@ -391,4 +396,21 @@ class ProductInboundService: except Exception: import traceback traceback.print_exc() - return {"categories": [], "types": [], "companies": []} \ No newline at end of file + return {"categories": [], "types": [], "companies": []} + + # ============================================================ + # 8. 获取历史负责人建议 (新增) + # ============================================================ + @staticmethod + def get_history_managers(base_id): + from app.models.inbound.product import StockProduct + try: + records = db.session.query(StockProduct.production_manager).filter( + StockProduct.base_id == base_id, + StockProduct.production_manager.isnot(None), + StockProduct.production_manager != '' + ).distinct().all() + return [r[0] for r in records if r[0]] + except Exception: + traceback.print_exc() + return [] \ No newline at end of file diff --git a/inventory-backend/app/services/inbound/semi_service.py b/inventory-backend/app/services/inbound/semi_service.py index a032cbe..f370f59 100644 --- a/inventory-backend/app/services/inbound/semi_service.py +++ b/inventory-backend/app/services/inbound/semi_service.py @@ -7,7 +7,6 @@ from sqlalchemy import or_, func, text, and_ import traceback import json - class SemiInboundService: # ============================================================ @@ -36,10 +35,10 @@ class SemiInboundService: raise ValueError(f"该物料已存在批号【{batch_number}】,请勿重复建单,建议在原批次上追加库存。") # ============================================================ - # 1. 基础物料搜索 + # 1. 基础物料搜索 (已修改支持分页) # ============================================================ @staticmethod - def search_base_material(keyword): + def search_base_material(keyword, page=1, limit=50): try: query = MaterialBase.query.filter(MaterialBase.is_enabled == True) if keyword: @@ -51,9 +50,10 @@ class SemiInboundService: MaterialBase.company_name.ilike(kw) # [新增] 支持搜公司 ) ) - query = query.order_by(MaterialBase.id.desc()).limit(20) + query = query.order_by(MaterialBase.id.desc()) + pagination = query.paginate(page=page, per_page=limit, error_out=False) results = [] - for item in query.all(): + for item in pagination.items: results.append({ 'id': item.id, 'company_name': item.company_name, # [新增] @@ -64,10 +64,15 @@ class SemiInboundService: 'type': item.material_type, 'status': '启用' }) - return results + return { + "items": results, + "total": pagination.total, + "page": page, + "has_next": pagination.has_next + } except Exception as e: traceback.print_exc() - return [] + return {"items": [], "total": 0, "page": 1, "has_next": False} # ============================================================ # 1.5 BOM 搜索逻辑 @@ -479,4 +484,21 @@ class SemiInboundService: except Exception: import traceback traceback.print_exc() - return {"categories": [], "types": [], "companies": []} \ No newline at end of file + return {"categories": [], "types": [], "companies": []} + + # ============================================================ + # 8. 获取历史生产负责人 (新增) + # ============================================================ + @staticmethod + def get_history_managers(base_id): + from app.models.inbound.semi import StockSemi + try: + records = db.session.query(StockSemi.production_manager).filter( + StockSemi.base_id == base_id, + StockSemi.production_manager.isnot(None), + StockSemi.production_manager != '' + ).distinct().all() + return [r[0] for r in records if r[0]] + except Exception: + traceback.print_exc() + return [] \ No newline at end of file diff --git a/inventory-web/src/api/inbound/product.ts b/inventory-web/src/api/inbound/product.ts index 7d1f76f..4bddee6 100644 --- a/inventory-web/src/api/inbound/product.ts +++ b/inventory-web/src/api/inbound/product.ts @@ -33,11 +33,12 @@ export function deleteProductInbound(id: number) { }) } -export function searchMaterialBase(keyword: string) { +// 搜索基础物料 (已增加 page 参数) +export function searchMaterialBase(keyword: string, page: number = 1) { return request({ url: '/inbound/product/search-base', method: 'get', - params: { keyword } + params: { keyword, page } }) } @@ -65,4 +66,13 @@ export function getFilterOptions() { url: '/inbound/product/options', method: 'get' }) +} + +// [新增] 负责人历史记录 +export function getManagerHistory(params: any) { + return request({ + url: '/inbound/product/suggestions/managers', + method: 'get', + params + }) } \ No newline at end of file diff --git a/inventory-web/src/api/inbound/semi.ts b/inventory-web/src/api/inbound/semi.ts index a077b85..58da771 100644 --- a/inventory-web/src/api/inbound/semi.ts +++ b/inventory-web/src/api/inbound/semi.ts @@ -35,12 +35,12 @@ export function deleteSemiInbound(id: number) { }) } -// 5. 搜索基础物料 -export function searchMaterialBase(keyword: string) { +// 5. 搜索基础物料 (已增加 page 参数) +export function searchMaterialBase(keyword: string, page: number = 1) { return request({ url: '/inbound/semi/search-base', method: 'get', - params: { keyword } + params: { keyword, page } }) } @@ -68,4 +68,13 @@ export function getFilterOptions() { url: '/inbound/semi/options', method: 'get' }) +} + +// [新增] 生产负责人历史记录 +export function getManagerHistory(params: any) { + return request({ + url: '/inbound/semi/suggestions/managers', + method: 'get', + params + }) } \ No newline at end of file diff --git a/inventory-web/src/views/stock/inbound/product.vue b/inventory-web/src/views/stock/inbound/product.vue index 8a5affc..6090ecc 100644 --- a/inventory-web/src/views/stock/inbound/product.vue +++ b/inventory-web/src/views/stock/inbound/product.vue @@ -469,7 +469,8 @@ import { deleteProductInbound, searchMaterialBase, searchBom, - getFilterOptions // [新增] + getFilterOptions, // [新增] + getManagerHistory // [新增] } from '@/api/inbound/product' import { uploadFile, deleteFile } from '@/api/inbound/buy' import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue' @@ -634,7 +635,7 @@ const defaultVisibleCols = ['company_name', 'material_name', 'sku', 'serial_numb const visibleColumnProps = ref(defaultVisibleCols) const form = reactive({ - id: undefined, base_id: undefined, + id: undefined, base_id: undefined as number | undefined, company_name: '', // [新增] material_name: '', spec_model: '', material_type: '', category: '', unit: '', sku: '', barcode: '', serial_number: '', in_date: '', @@ -739,7 +740,7 @@ const rules = { // ------------------------------------ -// Material Search & Population Logic +// Material Search & Population Logic (已修改) // ------------------------------------ const handleMaterialDropdownVisible = (visible: boolean) => { if (visible && materialOptions.value.length === 0) handleSearchMaterialDebounced('') } @@ -758,9 +759,9 @@ const handleSearchMaterial = async (query: string) => { try { const res: any = await searchMaterialBase(query, 1) - const apiResults = (res.data || []).map((i: any) => ({ ...i, isHistory: false })) + const apiResults = (res.data?.items || []).map((i: any) => ({ ...i, isHistory: false })) materialOptions.value = apiResults - hasNextPage.value = res.has_next + hasNextPage.value = res.data?.has_next ?? false } finally { searchLoading.value = false } } @@ -770,10 +771,10 @@ const loadMoreMaterials = async () => { searchPage.value += 1 try { const res: any = await searchMaterialBase(searchKeyword.value, searchPage.value) - if (res.data && res.data.length > 0) { - const newItems = res.data.map((i: any) => ({...i, isHistory: false})) + if (res.data && res.data.items && res.data.items.length > 0) { + const newItems = res.data.items.map((i: any) => ({...i, isHistory: false})) materialOptions.value.push(...newItems) - hasNextPage.value = res.has_next + hasNextPage.value = res.data.has_next } else { hasNextPage.value = false } @@ -797,12 +798,21 @@ const onMaterialSelected = (val: number) => { } // ------------------------------------ -// Autocomplete (Manager) - 后端驱动 +// Autocomplete (Manager) - 后端历史记录驱动 (已修改) // ------------------------------------ const querySearchManager = async (query: string, cb: any) => { - cb([]) + if (!form.base_id) { cb([]); return } + try { + const res: any = await getManagerHistory({ base_id: form.base_id }) + if (res.code === 200) { + const managers = (res.data || []).map((name: string) => ({ value: name })) + const filtered = query ? managers.filter((item: any) => item.value.toLowerCase().includes(query.toLowerCase())) : managers + cb(filtered) + } else { cb([]) } + } catch (e) { cb([]) } } const handleManagerSelect = (item: any) => { + form.production_manager = item.value } const fetchData = async () => { diff --git a/inventory-web/src/views/stock/inbound/semi.vue b/inventory-web/src/views/stock/inbound/semi.vue index d44227b..4693e7e 100644 --- a/inventory-web/src/views/stock/inbound/semi.vue +++ b/inventory-web/src/views/stock/inbound/semi.vue @@ -539,7 +539,8 @@ import { deleteSemiInbound, searchMaterialBase, searchBom, - getFilterOptions + getFilterOptions, + getManagerHistory // [新增] } from '@/api/inbound/semi' import { uploadFile, deleteFile } from '@/api/inbound/buy' import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue' @@ -754,16 +755,25 @@ const handleBomSelect = (val: string) => { } // ------------------------------------ -// Autocomplete & Search Logic (后端 API 驱动) +// Autocomplete & Search Logic (后端 API 驱动) (已修改) // ------------------------------------ const querySearchManager = async (query: string, cb: any) => { - cb([]) + if (!form.base_id) { cb([]); return } + try { + const res: any = await getManagerHistory({ base_id: form.base_id }) + if (res.code === 200) { + const managers = (res.data || []).map((name: string) => ({ value: name })) + const filtered = query ? managers.filter((item: any) => item.value.toLowerCase().includes(query.toLowerCase())) : managers + cb(filtered) + } else { cb([]) } + } catch (e) { cb([]) } } const handleManagerSelect = (item: any) => { + form.production_manager = item.value } // ------------------------------------ -// Material Search (Matches Buy.vue) +// Material Search (Matches Buy.vue) (已修改) // ------------------------------------ const handleMaterialDropdownVisible = (visible: boolean) => { if (visible && materialOptions.value.length === 0) handleSearchMaterialDebounced('') } @@ -782,9 +792,9 @@ const handleSearchMaterial = async (query: string) => { try { const res: any = await searchMaterialBase(query, 1) - const apiResults = (res.data || []).map((i: any) => ({...i, isHistory: false})) + const apiResults = (res.data?.items || []).map((i: any) => ({...i, isHistory: false})) materialOptions.value = apiResults - hasNextPage.value = res.has_next + hasNextPage.value = res.data?.has_next ?? false } finally { searchLoading.value = false } } @@ -794,10 +804,10 @@ const loadMoreMaterials = async () => { searchPage.value += 1 try { const res: any = await searchMaterialBase(searchKeyword.value, searchPage.value) - if (res.data && res.data.length > 0) { - const newItems = res.data.map((i: any) => ({...i, isHistory: false})) + if (res.data && res.data.items && res.data.items.length > 0) { + const newItems = res.data.items.map((i: any) => ({...i, isHistory: false})) materialOptions.value.push(...newItems) - hasNextPage.value = res.has_next + hasNextPage.value = res.data.has_next } else { hasNextPage.value = false }