feat(purchase): 采购申请批量审批(解决循环提交需多次审批)
问题: 一次提交 10 个物品生成 10 条独立申请,审批人需操作 10 次 后端: - 新增 POST /api/v1/purchase/batch-approve 批量审批接口 接收 ids + action,循环审批,成功/失败分别返回 前端: - 待审批列表加多选列(仅 status=0 可勾选) - 顶部批量操作栏:批量通过 / 批量驳回 / 取消选择 - 批量驳回弹窗统一填写驳回原因
This commit is contained in:
@ -205,6 +205,61 @@ def approve_purchase_request(purchase_id):
|
|||||||
# 5. 获取可选审批人列表
|
# 5. 获取可选审批人列表
|
||||||
# GET /api/v1/purchase/approvers
|
# 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'])
|
@purchase_bp.route('/approvers', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
def get_purchase_approvers():
|
def get_purchase_approvers():
|
||||||
|
|||||||
@ -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() {
|
export function getPurchaseApprovers() {
|
||||||
return request({
|
return request({
|
||||||
|
|||||||
@ -17,8 +17,19 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ★ 批量审批操作栏(仅待审批页 + 有审批权限) -->
|
||||||
|
<div v-if="filterStatus === 0 && canApprove && selectedRows.length > 0" class="batch-bar">
|
||||||
|
<span>已选 <b style="color:#409EFF;">{{ selectedRows.length }}</b> 条待审批申请</span>
|
||||||
|
<el-button type="success" size="small" :loading="batchLoading" @click="handleBatchApprove">批量通过</el-button>
|
||||||
|
<el-button type="danger" size="small" :loading="batchLoading" @click="openBatchRejectDialog">批量驳回</el-button>
|
||||||
|
<el-button size="small" @click="clearSelection">取消选择</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 数据表格 -->
|
<!-- 数据表格 -->
|
||||||
<el-table v-loading="loading" :data="list" border stripe style="margin-top: 16px;" row-key="id">
|
<el-table v-loading="loading" :data="list" border stripe style="margin-top: 16px;" row-key="id" @selection-change="handleSelectionChange">
|
||||||
|
<!-- ★ 多选列:仅待审批状态可勾选 -->
|
||||||
|
<el-table-column v-if="filterStatus === 0 && canApprove" type="selection" width="50" align="center"
|
||||||
|
:selectable="(row: any) => row.status === 0" />
|
||||||
<el-table-column prop="request_no" label="申请单号" width="180" />
|
<el-table-column prop="request_no" label="申请单号" width="180" />
|
||||||
<el-table-column prop="name" label="采购物品" min-width="150" show-overflow-tooltip />
|
<el-table-column prop="name" label="采购物品" min-width="150" show-overflow-tooltip />
|
||||||
<el-table-column prop="spec_model" label="规格型号" min-width="120" show-overflow-tooltip />
|
<el-table-column prop="spec_model" label="规格型号" min-width="120" show-overflow-tooltip />
|
||||||
@ -277,6 +288,22 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- ========== 批量驳回原因弹窗 ========== -->
|
||||||
|
<el-dialog v-model="batchRejectDialogVisible" title="批量驳回" width="480px" destroy-on-close :close-on-click-modal="false" :close-on-press-escape="false">
|
||||||
|
<el-form label-width="80px">
|
||||||
|
<el-form-item label="驳回数量">
|
||||||
|
<span style="font-weight: bold; color: #F56C6C;">{{ selectedRows.length }}</span> 条
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="驳回原因" required>
|
||||||
|
<el-input v-model="batchRejectReason" type="textarea" :rows="4" placeholder="请填写统一驳回原因(必填)" maxlength="200" show-word-limit />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="batchRejectDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="danger" :loading="batchLoading" @click="confirmBatchReject">确认批量驳回</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -289,7 +316,7 @@ import { useUserStore } from '@/stores/user'
|
|||||||
import { searchMaterialPurchase } from '@/api/purchase'
|
import { searchMaterialPurchase } from '@/api/purchase'
|
||||||
import {
|
import {
|
||||||
getPurchaseList, createPurchase, getPurchaseDetail,
|
getPurchaseList, createPurchase, getPurchaseDetail,
|
||||||
approvePurchase, getPurchaseApprovers, autoFillPurchase
|
approvePurchase, batchApprovePurchase, getPurchaseApprovers, autoFillPurchase
|
||||||
} from '@/api/purchase'
|
} from '@/api/purchase'
|
||||||
import { uploadFile, deleteFile } from '@/api/common/upload'
|
import { uploadFile, deleteFile } from '@/api/common/upload'
|
||||||
import type { FormInstance } from 'element-plus'
|
import type { FormInstance } from 'element-plus'
|
||||||
@ -311,6 +338,72 @@ const rejectDialogVisible = ref(false)
|
|||||||
const currentRejectRow = ref<any>(null)
|
const currentRejectRow = ref<any>(null)
|
||||||
const rejectReason = ref('')
|
const rejectReason = ref('')
|
||||||
|
|
||||||
|
// ★ 批量审批
|
||||||
|
const selectedRows = ref<any[]>([])
|
||||||
|
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 detailDialogVisible = ref(false)
|
||||||
const detail = ref<any>({})
|
const detail = ref<any>({})
|
||||||
@ -869,6 +962,19 @@ onMounted(() => {
|
|||||||
.app-container { padding: 20px; }
|
.app-container { padding: 20px; }
|
||||||
.filter-container { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
.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 {
|
.form-section {
|
||||||
background: #fafbfc;
|
background: #fafbfc;
|
||||||
|
|||||||
Reference in New Issue
Block a user