feat(purchase): 采购批次号支持,同一批提交的记录可分组查看/统计

背景: 循环提交把一批物品生成多条独立申请,无法识别'哪些是同一批采购'

后端:
- purchase_request 模型新增 batch_no 字段(同一批提交共享)
- create 接口接收 batch_no,无则自动生成 BATCH-yyyyMMdd-HHmm-随机
- 新增 GET /purchase/batch/<batch_no> 按批次查询所有记录
- 批量审批接口支持按 batch_no 一键审批整批

前端:
- 提交时生成 batch_no 共享给所有行
- 列表新增批次号列(可点击)
- 新增'整批查看'弹窗:显示同批次所有记录 + 一键批量通过本批
- 操作列新增'整批'按钮

数据库需执行: ALTER TABLE purchase_request ADD COLUMN batch_no VARCHAR(100);
This commit is contained in:
yueli
2026-08-31 13:46:46 +08:00
parent 4b62c81baa
commit 664127ee30
5 changed files with 183 additions and 6 deletions

View File

@ -89,7 +89,8 @@ export function approvePurchase(id: number, data: {
// 批量审批采购申请(循环提交产生的多条记录可一次审批)
export function batchApprovePurchase(data: {
ids: number[]
ids?: number[]
batch_no?: string
action: 'approve' | 'reject'
reject_reason?: string
}) {
@ -100,6 +101,14 @@ export function batchApprovePurchase(data: {
})
}
// 按批次号查询同批次的采购申请记录
export function getPurchaseByBatch(batchNo: string) {
return request({
url: `/purchase/batch/${encodeURIComponent(batchNo)}`,
method: 'get'
})
}
// 获取可选审批人列表
export function getPurchaseApprovers() {
return request({

View File

@ -52,14 +52,24 @@
</el-table-column>
<el-table-column prop="requester_name" label="申请人" width="100" />
<el-table-column prop="approver_name" label="审批人" width="100" />
<!-- ★ 批次号列:同一批提交的记录共享,可点击查看整批 -->
<el-table-column label="批次号" width="170">
<template #default="{ row }">
<el-link v-if="row.batch_no" type="primary" :underline="false" @click="openBatchDialog(row.batch_no)">
{{ row.batch_no }}
</el-link>
<span v-else style="color:#ccc">-</span>
</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" size="small">{{ row.status_text }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right" align="center">
<el-table-column label="操作" width="240" fixed="right" align="center">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="openDetailDialog(row)">详情</el-button>
<el-button v-if="row.batch_no" type="info" link size="small" @click="openBatchDialog(row.batch_no)">整批</el-button>
<template v-if="row.status === 0 && canApprove">
<el-button type="success" link size="small" @click="handleApprove(row)">通过</el-button>
<el-button type="danger" link size="small" @click="openRejectDialog(row)">驳回</el-button>
@ -272,6 +282,41 @@
</div>
</el-dialog>
<!-- ========== 整批查看弹窗(同一批提交的所有记录) ========== -->
<el-dialog v-model="batchDialogVisible" title="采购批次详情" width="900px" destroy-on-close :close-on-click-modal="false" :close-on-press-escape="false">
<div style="margin-bottom: 12px;">
<span style="font-weight: 600;">批次号:</span>
<el-tag style="margin-left: 6px;">{{ currentBatchNo }}</el-tag>
<span style="margin-left: 12px; color: #909399;">共 {{ batchItems.length }} 条采购记录</span>
<el-button
v-if="canApprove && batchItems.some(i => i.status === 0)"
type="success" size="small" style="float: right;"
:loading="batchLoading" @click="handleBatchApproveByNo"
>批量通过本批</el-button>
</div>
<el-table :data="batchItems" border size="small" max-height="450">
<el-table-column type="index" label="#" width="40" align="center" />
<el-table-column prop="request_no" label="单号" width="170" />
<el-table-column prop="name" label="物品" min-width="130" show-overflow-tooltip />
<el-table-column prop="spec_model" label="规格" min-width="100" show-overflow-tooltip />
<el-table-column prop="quantity" label="数量" width="60" align="center" />
<el-table-column label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" size="small">{{ row.status_text }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="purchase_date" label="采购日期" width="110" />
<el-table-column label="操作" width="70" align="center">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="openDetailDialog(row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="batchDialogVisible = false">关闭</el-button>
</template>
</el-dialog>
<!-- ========== 驳回原因弹窗 ========== -->
<el-dialog v-model="rejectDialogVisible" title="驳回申请" width="480px" destroy-on-close :close-on-click-modal="false" :close-on-press-escape="false">
<el-form label-width="80px">
@ -316,7 +361,8 @@ import { useUserStore } from '@/stores/user'
import { searchMaterialPurchase } from '@/api/purchase'
import {
getPurchaseList, createPurchase, getPurchaseDetail,
approvePurchase, batchApprovePurchase, getPurchaseApprovers, autoFillPurchase
approvePurchase, batchApprovePurchase, getPurchaseApprovers, autoFillPurchase,
getPurchaseByBatch
} from '@/api/purchase'
import { uploadFile, deleteFile } from '@/api/common/upload'
import type { FormInstance } from 'element-plus'
@ -408,6 +454,50 @@ const confirmBatchReject = async () => {
const detailDialogVisible = ref(false)
const detail = ref<any>({})
// ★ 整批查看
const batchDialogVisible = ref(false)
const currentBatchNo = ref('')
const batchItems = ref<any[]>([])
// 打开整批查看弹窗
const openBatchDialog = async (batchNo: string) => {
currentBatchNo.value = batchNo
batchItems.value = []
batchDialogVisible.value = true
try {
const res: any = await getPurchaseByBatch(batchNo)
batchItems.value = res.data?.items || []
} catch (err: any) {
ElMessage.error(err?.msg || '加载批次记录失败')
}
}
// 批量通过本批(按批次号)
const handleBatchApproveByNo = async () => {
if (!currentBatchNo.value) return
const pendingCount = batchItems.value.filter(i => i.status === 0).length
if (pendingCount === 0) return ElMessage.warning('本批没有待审批记录')
try {
await ElMessageBox.confirm(
`确定批量通过本批 ${pendingCount} 条采购申请吗?`,
'批量审批确认',
{ confirmButtonText: '批量通过', cancelButtonText: '取消', type: 'info' }
)
} catch { return }
batchLoading.value = true
try {
const res: any = await batchApprovePurchase({ batch_no: currentBatchNo.value, action: 'approve' })
ElMessage.success(res?.msg || '本批已全部通过')
await openBatchDialog(currentBatchNo.value) // 刷新整批数据
await fetchData()
} catch (err: any) {
ElMessage.error(err?.msg || '批量通过失败')
} finally {
batchLoading.value = false
}
}
// 创建弹窗
const formDialogVisible = ref(false)
const dialogTitle = ref('新建采购申请')
@ -804,6 +894,16 @@ const syncRemarkToAll = () => {
ElMessage.success('已将第一行备注同步到所有行')
}
// ★ 生成采购批次号(与后端 generate_batch_no 格式一致)
const generateBatchNo = () => {
const now = new Date()
const pad = (n: number) => String(n).padStart(2, '0')
const dateStr = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`
const timeStr = `${pad(now.getHours())}${pad(now.getMinutes())}`
const rand = Math.random().toString(16).slice(2, 6).toUpperCase()
return `BATCH-${dateStr}-${timeStr}-${rand}`
}
// ★ 照片一键同步所有行:取第一行的照片深拷贝到所有行
const syncImagesToAll = () => {
if (formItems.value.length <= 1) {
@ -852,12 +952,15 @@ const submitForm = async () => {
}
}
// 3. 逐行提交(每行携带公共信息)
// 3. 逐行提交(每行携带公共信息 + 同一批次号)
submitLoading.value = true
let successCount = 0
// ★ 生成批次号:同一批提交的所有记录共享,便于审批端按批次查看/统计
const batchNo = generateBatchNo()
try {
for (const row of validItems) {
const payload: any = {
batch_no: batchNo,
name: row.name,
spec_model: row.spec_model,
quantity: row.quantity,