feat(borrow): 前端适配整单转交与双向握手
· 转交弹窗由「选择明细」改为**整单**:列出该单全部未还物品(只读), 明确告知「发起后对方确认,责任才转移」。原先让用户挑一行,挑中就只转 那一行 —— 正是单内撕裂的入口,现从 UI 上根除。 · 操作栏新增【接收转交】【拒绝】:仅对 pending_transfer.is_mine 的行显示, 即「待我接收」的转交。已有 PENDING 时不再显示【转交】,避免重复发起。 · 状态列新增「转交待确认」:此时主表 current_holder 未变(东西还在原持有人 手上),必须用状态列把「责任正在转移中」显式表达出来,否则界面上看不出 任何变化。 · 新增 acceptBorrowTransfer / rejectBorrowTransfer API; 拒绝时用 prompt 收集可选原因,写入转交流水备注。 错误提示仍交由 request 拦截器统一 toast,不在本地重复弹出。
This commit is contained in:
@ -283,6 +283,31 @@ export function getBorrowHistory(borrowId: number) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认接收转交(双向握手第二步)—— 责任正式转移到接收人名下。
|
||||
*
|
||||
* ★ 仅该转交的 to_user_id 本人可调用(后端强校验,非库管权限)。
|
||||
* ★ 副作用:该单**全部未归还明细**的当前持有人一并变更。
|
||||
*/
|
||||
export function acceptBorrowTransfer(transferId: number) {
|
||||
return request({
|
||||
url: `/v1/transactions/borrow/transfer/${transferId}/accept`,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 拒绝转交 —— 主表不动,责任仍在原持有人。权限同上。
|
||||
* @param reason 可选拒绝原因,写入转交流水备注
|
||||
*/
|
||||
export function rejectBorrowTransfer(transferId: number, reason?: string) {
|
||||
return request({
|
||||
url: `/v1/transactions/borrow/transfer/${transferId}/reject`,
|
||||
method: 'post',
|
||||
data: { reason }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 整单流转时间线(**borrow_no** 维度):借出 → 转交(可多次) → 归还 → 报废
|
||||
*
|
||||
|
||||
@ -204,9 +204,13 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column v-if="hasColumnPermission('status')" label="状态" width="100" align="center">
|
||||
<el-table-column v-if="hasColumnPermission('status')" label="状态" width="110" align="center">
|
||||
<template #default="{row}">
|
||||
<el-tag v-if="row.status === 'scrapped'" type="info">已报废</el-tag>
|
||||
<!-- ★ 中间态:已发起转交但接收人尚未确认。
|
||||
此时主表 current_holder **未变**(东西还在原持有人手上),
|
||||
故必须用状态列把「责任正在转移中」显式表达出来。 -->
|
||||
<el-tag v-if="row.pending_transfer" type="warning">转交待确认</el-tag>
|
||||
<el-tag v-else-if="row.status === 'scrapped'" type="info">已报废</el-tag>
|
||||
<el-tag v-else-if="row.status === 'returned'" type="success">已还</el-tag>
|
||||
<el-tag v-else-if="row.status === 'partial_returned'" type="warning">部分归还</el-tag>
|
||||
<el-tag v-else type="danger">未还</el-tag>
|
||||
@ -232,16 +236,32 @@
|
||||
<!-- ★ 操作列
|
||||
· 申请报废 / 转交 —— 仅未归还记录,且分别受各自权限约束
|
||||
· 流转记录 —— 无特殊权限要求,凡能看到本列表者皆可查看 -->
|
||||
<el-table-column label="操作" width="210" align="center" fixed="right">
|
||||
<el-table-column label="操作" width="250" align="center" fixed="right">
|
||||
<template #default="{row}">
|
||||
<!-- ★ 待我接收的转交:最高优先级,接收人必须一眼看到并处理。
|
||||
东西还在对方手上,责任尚未转移 —— 见后端双向握手设计。 -->
|
||||
<template v-if="row.pending_transfer && row.pending_transfer.is_mine">
|
||||
<el-button
|
||||
type="success" link size="small"
|
||||
:loading="transferActionLoading"
|
||||
@click="confirmAcceptTransfer(row)"
|
||||
>接收转交</el-button>
|
||||
<el-button
|
||||
type="danger" link size="small"
|
||||
:loading="transferActionLoading"
|
||||
@click="confirmRejectTransfer(row)"
|
||||
>拒绝</el-button>
|
||||
</template>
|
||||
|
||||
<el-button
|
||||
v-if="canScrap && isUnreturned(row)"
|
||||
type="danger" link size="small"
|
||||
@click="openScrapDialog(row)"
|
||||
>申请报废</el-button>
|
||||
|
||||
<!-- 已有待接收的转交时不再显示「转交」,避免重复发起(后端也会拒) -->
|
||||
<el-button
|
||||
v-if="isUnreturned(row)"
|
||||
v-if="isUnreturned(row) && !row.pending_transfer"
|
||||
v-permission="'borrow_transfer'"
|
||||
type="primary" link size="small"
|
||||
@click="openTransferDialog(row)"
|
||||
@ -308,11 +328,11 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ★ 借库转交弹窗(一期:仅支持整单全部转交) -->
|
||||
<el-dialog v-model="transferDialogVisible" title="借库转交" width="640px" destroy-on-close>
|
||||
<!-- ★ 借库转交弹窗(整单转交 + 双向握手) -->
|
||||
<el-dialog v-model="transferDialogVisible" title="发起借库转交" width="640px" destroy-on-close>
|
||||
<el-alert
|
||||
title="仅支持整单全部转交"
|
||||
description="一张借出单同时只能有一个持有人。部分转交会让「当前持有人」无法确定,故一期不支持;如需拆分持有,请先办理部分归还,再对剩余量转交。"
|
||||
title="整单转交,且需对方确认后责任才转移"
|
||||
description="发起后只生成一条「待接收」记录,东西仍在您手上、责任仍归您;对方在其借用列表中确认接收后,本单全部未还物品的持有责任才整体转移给他。"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
@ -320,21 +340,11 @@
|
||||
/>
|
||||
<div style="margin-bottom: 10px;">
|
||||
<div style="font-weight: 600; margin-bottom: 6px;">单号:{{ transferBorrowNo }}</div>
|
||||
<div style="font-size: 13px; color: #909399;">请选择要转交的明细(转交后该明细的持有人整体变更为接收人):</div>
|
||||
<div style="font-size: 13px; color: #909399;">
|
||||
以下 {{ transferCandidates.length }} 项未归还物品将<strong>一并</strong>转交(共 {{ transferTotalQty }} 件):
|
||||
</div>
|
||||
<el-table
|
||||
:data="transferCandidates"
|
||||
border
|
||||
size="small"
|
||||
max-height="260"
|
||||
highlight-current-row
|
||||
@row-click="(row: any) => transferTargetId = row.id"
|
||||
>
|
||||
<el-table-column width="46" align="center">
|
||||
<template #default="{row}">
|
||||
<el-radio v-model="transferTargetId" :value="row.id" :label="row.id"> </el-radio>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</div>
|
||||
<el-table :data="transferCandidates" border size="small" max-height="240">
|
||||
<el-table-column prop="material_name" label="物料名称" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column prop="sku" label="SKU" width="110" show-overflow-tooltip />
|
||||
<el-table-column label="当前持有人" width="120" align="center">
|
||||
@ -371,7 +381,7 @@
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="transferDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="transferSubmitting" @click="confirmTransfer">确认转交</el-button>
|
||||
<el-button type="primary" :loading="transferSubmitting" @click="confirmTransfer">发起转交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@ -436,6 +446,8 @@ import { getApproversList } from '@/api/auth'
|
||||
import {
|
||||
submitBorrowScrapRequest,
|
||||
transferBorrow,
|
||||
acceptBorrowTransfer,
|
||||
rejectBorrowTransfer,
|
||||
getBorrowUsers,
|
||||
getBorrowSlipHistory,
|
||||
} from '@/api/transaction'
|
||||
@ -691,13 +703,18 @@ const latest = (arr: any[]): string | null =>
|
||||
|
||||
const transferDialogVisible = ref(false)
|
||||
const transferSubmitting = ref(false)
|
||||
const transferActionLoading = ref(false) // 接收/拒绝按钮 loading
|
||||
const transferBorrowNo = ref('')
|
||||
const transferCandidates = ref<any[]>([])
|
||||
const transferTargetId = ref<number | null>(null)
|
||||
const transferToUserId = ref<number | null>(null)
|
||||
const transferRemark = ref('')
|
||||
const transferUsers = ref<Array<{ id: number; name: string }>>([])
|
||||
|
||||
// 整单待还合计 —— 与后端按 borrow_no 汇总的口径一致
|
||||
const transferTotalQty = computed(() =>
|
||||
transferCandidates.value.reduce((s: number, c: any) => s + (Number(c.pending_quantity) || 0), 0)
|
||||
)
|
||||
|
||||
const loadTransferUsers = async () => {
|
||||
if (transferUsers.value.length) return // 名单会话内复用,避免每次开弹窗都请求
|
||||
try {
|
||||
@ -717,8 +734,6 @@ const openTransferDialog = (row: any) => {
|
||||
}
|
||||
transferBorrowNo.value = row.borrow_no
|
||||
transferCandidates.value = candidates
|
||||
// 实测 54/60 的单号只有 1 条明细 —— 单条时直接选中,省掉一次点击
|
||||
transferTargetId.value = candidates.length === 1 ? candidates[0].id : null
|
||||
transferToUserId.value = null
|
||||
transferRemark.value = ''
|
||||
transferDialogVisible.value = true
|
||||
@ -726,25 +741,30 @@ const openTransferDialog = (row: any) => {
|
||||
}
|
||||
|
||||
const confirmTransfer = async () => {
|
||||
if (!transferTargetId.value) return ElMessage.warning('请选择要转交的明细')
|
||||
if (!transferToUserId.value) return ElMessage.warning('请选择接收人')
|
||||
|
||||
const target = transferCandidates.value.find((c: any) => c.id === transferTargetId.value)
|
||||
// ★ 整单转交:以后端为准,不再让用户挑明细。
|
||||
// 原实现只转所选的那一行,一张 2 明细的单转完会出现两个持有人
|
||||
// (前端按单号聚合就同时显示两个名字)—— 这是必须消灭的单内撕裂。
|
||||
const receiver = transferUsers.value.find(u => u.id === transferToUserId.value)
|
||||
|
||||
// 提前拦截「转给自己」,省一次无效往返(后端也会拒)
|
||||
if (target && receiver && target.current_holder_id === receiver.id) {
|
||||
return ElMessage.warning(`接收人与当前持有人同为【${receiver.name}】,无需转交`)
|
||||
const holderIds = new Set(
|
||||
transferCandidates.value.map((c: any) => c.current_holder_id).filter((v: any) => v != null)
|
||||
)
|
||||
if (holderIds.size === 1 && holderIds.has(transferToUserId.value as number)) {
|
||||
return ElMessage.warning(`接收人与当前持有人同为【${receiver?.name}】,无需转交`)
|
||||
}
|
||||
|
||||
const qty = target?.pending_quantity ?? 0
|
||||
const qty = transferTotalQty.value
|
||||
const anchorId = transferCandidates.value[0].id
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`将【${target?.material_name || target?.sku || ''}】的持有权整单转交给【${receiver?.name || ''}】?\n\n`
|
||||
+ `转交数量:${qty}(整单全量,不支持部分转交)\n`
|
||||
+ `转交后,该物品须由接收人本人办理归还。`,
|
||||
'转交确认',
|
||||
{ confirmButtonText: '确认转交', cancelButtonText: '取消', type: 'warning' }
|
||||
`将【${transferBorrowNo.value}】整单(${transferCandidates.value.length} 项,共 ${qty} 件)\n`
|
||||
+ `的持有权转交给【${receiver?.name || ''}】?\n\n`
|
||||
+ `发起后对方需在其借用列表中确认接收,责任才正式转移;\n`
|
||||
+ `在对方确认前,物品与责任仍归当前持有人。`,
|
||||
'发起转交',
|
||||
{ confirmButtonText: '确认发起', cancelButtonText: '取消', type: 'warning' }
|
||||
)
|
||||
} catch {
|
||||
return // 用户取消
|
||||
@ -752,15 +772,15 @@ const confirmTransfer = async () => {
|
||||
|
||||
transferSubmitting.value = true // ★ 按钮 loading 防抖,避免重复提交
|
||||
try {
|
||||
const res: any = await transferBorrow(transferTargetId.value, {
|
||||
transfer_qty: qty, // 一期必须等于待还量,后端会二次校验
|
||||
const res: any = await transferBorrow(anchorId, {
|
||||
transfer_qty: qty, // 整单待还合计,后端二次校验
|
||||
to_user_id: transferToUserId.value,
|
||||
to_user_name: receiver?.name,
|
||||
remark: transferRemark.value,
|
||||
})
|
||||
ElMessage.success(res?.msg || '转交成功')
|
||||
ElMessage.success(res?.msg || '已发起转交,等待对方确认')
|
||||
transferDialogVisible.value = false
|
||||
fetchData() // 刷新列表:持有人/操作列状态已变化
|
||||
fetchData() // 刷新列表:状态列变为「转交待确认」
|
||||
} catch {
|
||||
// ★ 不重复弹错:request 拦截器已按后端 msg 全局 toast,此处再弹会双份
|
||||
} finally {
|
||||
@ -768,6 +788,68 @@ const confirmTransfer = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// ★ 双向握手第二步:接收人确认 / 拒绝
|
||||
// ==========================================================================
|
||||
const confirmAcceptTransfer = async (row: any) => {
|
||||
const pt = row.pending_transfer
|
||||
if (!pt) return
|
||||
const cnt = (row.children || []).filter((c: any) => (c.pending_quantity || 0) > 0).length
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认接收【${pt.from_user_name || '对方'}】转交的这批资产?\n\n`
|
||||
+ `单号:${row.borrow_no}(${cnt} 项)\n`
|
||||
+ `确认后,该单全部未归还物品的持有责任转移到您名下,归还时须由您本人办理。`,
|
||||
'确认接收转交',
|
||||
{ confirmButtonText: '确认接收', cancelButtonText: '取消', type: 'warning' }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
transferActionLoading.value = true
|
||||
try {
|
||||
const res: any = await acceptBorrowTransfer(pt.id)
|
||||
ElMessage.success(res?.msg || '已接收')
|
||||
fetchData()
|
||||
} catch {
|
||||
// 错误提示由 request 拦截器统一处理
|
||||
} finally {
|
||||
transferActionLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const confirmRejectTransfer = async (row: any) => {
|
||||
const pt = row.pending_transfer
|
||||
if (!pt) return
|
||||
let reason = ''
|
||||
try {
|
||||
const r: any = await ElMessageBox.prompt(
|
||||
`拒绝【${pt.from_user_name || '对方'}】转交给您的这批资产?\n拒绝后责任仍由对方承担。`,
|
||||
'拒绝转交',
|
||||
{
|
||||
confirmButtonText: '确认拒绝',
|
||||
cancelButtonText: '取消',
|
||||
inputPlaceholder: '拒绝原因(可留空)',
|
||||
inputValidator: () => true, // 允许留空,后端 reason 为可选
|
||||
type: 'warning',
|
||||
}
|
||||
)
|
||||
reason = r?.value || ''
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
transferActionLoading.value = true
|
||||
try {
|
||||
const res: any = await rejectBorrowTransfer(pt.id, reason)
|
||||
ElMessage.success(res?.msg || '已拒绝')
|
||||
fetchData()
|
||||
} catch {
|
||||
// 同上
|
||||
} finally {
|
||||
transferActionLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// ★ 流转明细时间线(借出 → 转交(可多次) → 归还 → 报废)
|
||||
// ==========================================================================
|
||||
|
||||
Reference in New Issue
Block a user