feat(purchase): 采购列表按批次分组展示 + 批次级一键审批

- 列表按批次分组(树形展开):一个批次一行,显示物品数/批次总价/申请人/状态汇总
- 展开批次行显示批内明细(每条记录状态/价格/操作)
- 批次行提供「审批本批」「驳回本批」一键操作(按批次前缀批量处理待审批记录)
- 保留逐条展开明细内的通过/驳回,以及顶部多选批量审批
- fetchData 按批次前缀聚合当前页数据
This commit is contained in:
yueli
2026-08-31 14:00:31 +08:00
parent 1000deda2f
commit 4c41e1ec13

View File

@ -17,62 +17,89 @@
</el-button>
</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="batchKey">
<!-- 展开/收起 + 批次信息 -->
<el-table-column type="expand" width="50" align="center">
<template #default="{ row }">
<div style="padding: 12px 24px; background: #f5f7fa;">
<div style="font-weight: bold; margin-bottom: 8px;">批次明细{{ row.itemCount }} </div>
<el-table :data="row.items" border size="small" style="width: 100%;">
<el-table-column prop="request_no" label="申请单号" width="200" />
<el-table-column prop="name" label="采购物品" min-width="140" show-overflow-tooltip />
<el-table-column prop="spec_model" label="规格型号" min-width="120" show-overflow-tooltip />
<el-table-column prop="quantity" label="数量" width="70" align="center" />
<el-table-column prop="purchase_date" label="采购日期" width="110" />
<el-table-column v-if="userStore.hasPermission('inbound_purchase:unit_price')" label="单价" width="90" align="right">
<template #default="{ row: it }">
{{ it.unit_price ? '¥' + Number(it.unit_price).toFixed(2) : '-' }}
</template>
</el-table-column>
<el-table-column v-if="userStore.hasPermission('inbound_purchase:total_price')" label="总价" width="100" align="right">
<template #default="{ row: it }">
{{ it.total_price ? '¥' + Number(it.total_price).toFixed(2) : '-' }}
</template>
</el-table-column>
<el-table-column label="状态" width="90" align="center">
<template #default="{ row: it }">
<el-tag :type="statusTagType(it.status)" size="small">{{ it.status_text }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="110" align="center">
<template #default="{ row: it }">
<el-button type="primary" link size="small" @click="openDetailDialog(it)">详情</el-button>
<template v-if="it.status === 0 && canApprove">
<el-button type="success" link size="small" @click="handleApprove(it)">通过</el-button>
<el-button type="danger" link size="small" @click="openRejectDialog(it)">驳回</el-button>
</template>
</template>
</el-table-column>
</el-table>
</div>
</template>
</el-table-column>
<!-- 数据表格 -->
<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="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="quantity" label="数量" width="80" align="center" />
<el-table-column prop="purchase_date" label="采购日期" width="110" />
<el-table-column v-if="userStore.hasPermission('inbound_purchase:unit_price')" label="含税单价" width="110" align="right">
<!-- 批次主行 -->
<el-table-column label="批次号" width="220">
<template #default="{ row }">
{{ row.unit_price ? '¥' + Number(row.unit_price).toFixed(2) : '-' }}
</template>
</el-table-column>
<el-table-column v-if="userStore.hasPermission('inbound_purchase:total_price')" label="含税总价" width="120" align="right">
<template #default="{ row }">
{{ row.total_price ? '¥' + Number(row.total_price).toFixed(2) : '-' }}
</template>
</el-table-column>
<el-table-column v-if="userStore.hasPermission('inbound_purchase:tax_rate')" prop="tax_rate" label="税率" width="80" align="center">
<template #default="{ row }">
{{ row.tax_rate != null ? row.tax_rate + '%' : '-' }}
</template>
</el-table-column>
<el-table-column prop="requester_name" label="申请人" width="100" />
<el-table-column prop="approver_name" label="审批人" width="100" />
<!-- 批次号列同一批提交的记录共享request_no 去掉末尾流水号可点击查看整批 -->
<el-table-column label="批次号" width="170">
<template #default="{ row }">
<el-link v-if="getBatchKey(row.request_no)" type="primary" :underline="false" @click="openBatchDialog(row.request_no)">
{{ getBatchKey(row.request_no) }}
<el-link type="primary" :underline="false" @click="openBatchDialog(row.batchKey)">
{{ row.batchKey }}
</el-link>
<span v-else style="color:#ccc">-</span>
</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<el-table-column label="物品" width="70" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" size="small">{{ row.status_text }}</el-tag>
<el-tag size="small" type="info">{{ row.itemCount }} </el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="240" fixed="right" align="center">
<el-table-column v-if="userStore.hasPermission('inbound_purchase:total_price')" label="批次总价" width="120" align="right">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="openDetailDialog(row)">详情</el-button>
<el-button v-if="getBatchKey(row.request_no)" type="info" link size="small" @click="openBatchDialog(row.request_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>
<span style="font-weight: bold; color: #303133;">¥{{ Number(row.totalAmount).toFixed(2) }}</span>
</template>
</el-table-column>
<el-table-column prop="requesterName" label="申请人" width="100" />
<el-table-column prop="purchaseDate" label="采购日期" width="110" />
<el-table-column label="状态" width="140" align="center">
<template #default="{ row }">
<template v-for="(text, i) in row.statusTexts" :key="i">
<el-tag v-if="row.statusTexts.length === 1"
:type="statusTagType(row.summaryStatus)" size="small" style="margin-right:2px;">
{{ text }}
</el-tag>
</template>
<el-tag v-if="row.statusTexts.length > 1" type="warning" size="small">混合</el-tag>
<div v-if="row.pendingCount > 0" style="font-size: 11px; color: #F56C6C;">
待审批 {{ row.pendingCount }}
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right" align="center">
<template #default="{ row }">
<el-button type="info" link size="small" @click="openBatchDialog(row.batchKey)">查看整批</el-button>
<!-- 一键审批本批 -->
<template v-if="row.pendingCount > 0 && canApprove">
<el-button type="success" link size="small" :loading="batchLoading" @click="handleApproveBatch(row.batchKey)">审批本批</el-button>
<el-button type="danger" link size="small" :loading="batchLoading" @click="openRejectBatchDialog(row.batchKey)">驳回本批</el-button>
</template>
</template>
</el-table-column>
@ -349,6 +376,22 @@
</template>
</el-dialog>
<!-- ========== 驳回本批弹窗 ========== -->
<el-dialog v-model="rejectBatchDialogVisible" 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="批次号">
<el-tag>{{ rejectBatchKey }}</el-tag>
</el-form-item>
<el-form-item label="驳回原因" required>
<el-input v-model="rejectBatchReason" type="textarea" :rows="4" placeholder="请填写统一驳回原因必填" maxlength="200" show-word-limit />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="rejectBatchDialogVisible = false">取消</el-button>
<el-button type="danger" :loading="batchLoading" @click="confirmRejectBatch">确认驳回本批</el-button>
</template>
</el-dialog>
</div>
</template>
@ -400,6 +443,60 @@ const clearSelection = () => {
selectedRows.value = []
}
// ★ 审批本批:一键通过批次内所有待审批记录
const handleApproveBatch = async (batchKey: string) => {
const batch = list.value.find(b => b.batchKey === batchKey)
if (!batch || batch.pendingCount === 0) return ElMessage.warning('本批没有待审批记录')
try {
await ElMessageBox.confirm(
`确定通过批次【${batchKey}】的 ${batch.pendingCount} 条待审批记录吗?`,
'审批本批',
{ confirmButtonText: '全部通过', cancelButtonText: '取消', type: 'info' }
)
} catch { return }
batchLoading.value = true
try {
const res: any = await batchApprovePurchase({ batch_key: batchKey, action: 'approve' })
ElMessage.success(res?.msg || '本批已全部通过')
await fetchData()
} catch (err: any) {
ElMessage.error(err?.msg || '审批失败')
} finally {
batchLoading.value = false
}
}
// ★ 驳回本批:批次内所有待审批记录统一驳回
const rejectBatchKey = ref('')
const rejectBatchDialogVisible = ref(false)
const rejectBatchReason = ref('')
const openRejectBatchDialog = (batchKey: string) => {
rejectBatchKey.value = batchKey
rejectBatchReason.value = ''
rejectBatchDialogVisible.value = true
}
const confirmRejectBatch = async () => {
if (!rejectBatchReason.value.trim()) { ElMessage.warning('请填写驳回原因'); return }
batchLoading.value = true
try {
const res: any = await batchApprovePurchase({
batch_key: rejectBatchKey.value,
action: 'reject',
reject_reason: rejectBatchReason.value
})
ElMessage.success(res?.msg || '本批已驳回')
rejectBatchDialogVisible.value = false
await fetchData()
} catch (err: any) {
ElMessage.error(err?.msg || '驳回失败')
} finally {
batchLoading.value = false
}
}
// 批量通过
const handleBatchApprove = async () => {
if (selectedRows.value.length === 0) return
@ -617,8 +714,36 @@ const fetchData = async () => {
if (filterStatus.value !== '') params.status = filterStatus.value
const res: any = await getPurchaseList(params)
list.value = res.data?.items || []
const items = res.data?.items || []
total.value = res.data?.total || 0
// ★ 按批次分组:同批次(单号去末尾-批内序号)聚合为一行
const groupMap = new Map<string, any[]>()
items.forEach(item => {
const key = getBatchKey(item.request_no) || '独立'
if (!groupMap.has(key)) groupMap.set(key, [])
groupMap.get(key)!.push(item)
})
list.value = Array.from(groupMap.entries()).map(([batchKey, batchItems]) => {
const totalAmount = batchItems.reduce((sum, it) => sum + (Number(it.total_price) || 0), 0)
const pendingCount = batchItems.filter(it => it.status === 0).length
// 状态汇总:全通过=1全驳回=2混合/待审=0
const statuses = new Set(batchItems.map(it => it.status))
let summaryStatus = batchItems[0]?.status ?? 0
if (statuses.size > 1) summaryStatus = 0 // 混合状态按待审处理
return {
batchKey,
items: batchItems,
itemCount: batchItems.length,
totalAmount,
pendingCount,
summaryStatus,
requesterName: batchItems[0]?.requester_name || '',
purchaseDate: batchItems[0]?.purchase_date || '',
statusTexts: batchItems.map(it => it.status_text).filter((v, i, a) => a.indexOf(v) === i)
}
})
} catch (err: any) {
ElMessage.error(err?.msg || '加载失败')
} finally {
@ -626,6 +751,7 @@ const fetchData = async () => {
}
}
const fetchApprovers = async () => {
try {
const res: any = await getPurchaseApprovers()