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;