diff --git a/inventory-backend/app/api/v1/purchase.py b/inventory-backend/app/api/v1/purchase.py index fe37823..a8d04cb 100644 --- a/inventory-backend/app/api/v1/purchase.py +++ b/inventory-backend/app/api/v1/purchase.py @@ -205,6 +205,61 @@ def approve_purchase_request(purchase_id): # 5. 获取可选审批人列表 # GET /api/v1/purchase/approvers # -------------------------------------------------------- +@purchase_bp.route('/batch-approve', methods=['POST']) +@jwt_required() +@permission_required('inbound_purchase:operation') +def batch_approve_purchase(): + """ + 批量审批采购申请(解决循环提交导致的多条记录需多次审批问题) + + 请求体: + { + "ids": [1, 2, 3], + "action": "approve" | "reject", + "reject_reason": "驳回原因(reject 时必填)" + } + """ + try: + user_id = get_current_user_id() + data = request.get_json() or {} + ids = data.get('ids', []) + action = data.get('action', 'approve') + reject_reason = data.get('reject_reason') + + if not ids or not isinstance(ids, list): + return jsonify({'code': 400, 'msg': 'ids 不能为空'}), 400 + if action not in ('approve', 'reject'): + return jsonify({'code': 400, 'msg': '无效的审批操作'}), 400 + if action == 'reject' and not reject_reason: + return jsonify({'code': 400, 'msg': '驳回时必须提供原因'}), 400 + + success_ids = [] + error_items = [] + for pid in ids: + try: + purchase = PurchaseService.approve_purchase_request( + purchase_id=pid, + user_id=user_id, + action=action, + reject_reason=reject_reason + ) + success_ids.append(purchase.id) + except ValueError as e: + error_items.append({'id': pid, 'msg': str(e)}) + except Exception as e: + traceback.print_exc() + error_items.append({'id': pid, 'msg': str(e)}) + + msg = f'批量审批完成: 成功 {len(success_ids)} 条' + if error_items: + msg += f', 失败 {len(error_items)} 条' + return jsonify({'code': 200, 'msg': msg, 'data': {'success_ids': success_ids, 'errors': error_items}}), 200 + + except Exception as e: + traceback.print_exc() + return jsonify({'code': 500, 'msg': f'服务器内部错误: {str(e)}'}), 500 + + @purchase_bp.route('/approvers', methods=['GET']) @jwt_required() def get_purchase_approvers(): diff --git a/inventory-web/src/api/purchase.ts b/inventory-web/src/api/purchase.ts index fc2ed11..85f3b4b 100644 --- a/inventory-web/src/api/purchase.ts +++ b/inventory-web/src/api/purchase.ts @@ -87,6 +87,19 @@ export function approvePurchase(id: number, data: { }) } +// 批量审批采购申请(循环提交产生的多条记录可一次审批) +export function batchApprovePurchase(data: { + ids: number[] + action: 'approve' | 'reject' + reject_reason?: string +}) { + return request({ + url: '/purchase/batch-approve', + method: 'post', + data + }) +} + // 获取可选审批人列表 export function getPurchaseApprovers() { return request({ diff --git a/inventory-web/src/views/purchase/index.vue b/inventory-web/src/views/purchase/index.vue index 43069bd..506a6b3 100644 --- a/inventory-web/src/views/purchase/index.vue +++ b/inventory-web/src/views/purchase/index.vue @@ -17,8 +17,19 @@ + +
+ 已选 {{ selectedRows.length }} 条待审批申请 + 批量通过 + 批量驳回 + 取消选择 +
+ - + + + @@ -277,6 +288,22 @@ + + + + + {{ selectedRows.length }} 条 + + + + + + + + @@ -289,7 +316,7 @@ import { useUserStore } from '@/stores/user' import { searchMaterialPurchase } from '@/api/purchase' import { getPurchaseList, createPurchase, getPurchaseDetail, - approvePurchase, getPurchaseApprovers, autoFillPurchase + approvePurchase, batchApprovePurchase, getPurchaseApprovers, autoFillPurchase } from '@/api/purchase' import { uploadFile, deleteFile } from '@/api/common/upload' import type { FormInstance } from 'element-plus' @@ -311,6 +338,72 @@ const rejectDialogVisible = ref(false) const currentRejectRow = ref(null) const rejectReason = ref('') +// ★ 批量审批 +const selectedRows = ref([]) +const batchLoading = ref(false) +const batchRejectDialogVisible = ref(false) +const batchRejectReason = ref('') + +// 多选变化 +const handleSelectionChange = (rows: any[]) => { + selectedRows.value = rows +} + +// 清除选择 +const clearSelection = () => { + selectedRows.value = [] +} + +// 批量通过 +const handleBatchApprove = async () => { + if (selectedRows.value.length === 0) return + const ids = selectedRows.value.map(r => r.id) + try { + await ElMessageBox.confirm( + `确定批量通过 ${ids.length} 条采购申请吗?`, + '批量审批确认', + { confirmButtonText: '批量通过', cancelButtonText: '取消', type: 'info' } + ) + } catch { return } + + batchLoading.value = true + try { + const res: any = await batchApprovePurchase({ ids, action: 'approve' }) + ElMessage.success(res?.msg || `已通过 ${ids.length} 条`) + selectedRows.value = [] + await fetchData() + } catch (err: any) { + ElMessage.error(err?.msg || '批量通过失败') + } finally { + batchLoading.value = false + } +} + +// 批量驳回弹窗 +const openBatchRejectDialog = () => { + batchRejectReason.value = '' + batchRejectDialogVisible.value = true +} + +// 确认批量驳回 +const confirmBatchReject = async () => { + if (selectedRows.value.length === 0) return + if (!batchRejectReason.value.trim()) { ElMessage.warning('请填写驳回原因'); return } + const ids = selectedRows.value.map(r => r.id) + batchLoading.value = true + try { + const res: any = await batchApprovePurchase({ ids, action: 'reject', reject_reason: batchRejectReason.value }) + ElMessage.success(res?.msg || `已驳回 ${ids.length} 条`) + batchRejectDialogVisible.value = false + selectedRows.value = [] + await fetchData() + } catch (err: any) { + ElMessage.error(err?.msg || '批量驳回失败') + } finally { + batchLoading.value = false + } +} + // 详情 const detailDialogVisible = ref(false) const detail = ref({}) @@ -869,6 +962,19 @@ onMounted(() => { .app-container { padding: 20px; } .filter-container { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; } +/* 批量审批操作栏 */ +.batch-bar { + display: flex; + align-items: center; + gap: 10px; + margin-top: 12px; + padding: 8px 12px; + background: #ecf5ff; + border: 1px solid #d9ecff; + border-radius: 4px; + font-size: 13px; +} + /* 表单分组 */ .form-section { background: #fafbfc;