单号:{{ transferBorrowNo }}
- 以下 {{ transferCandidates.length }} 项未归还物品将一并转交(共 {{ transferTotalQty }} 件):
+ 勾选要转交的物品(已选 {{ transferSelected.length }} 项,共 {{ transferTotalQty }} 件)。
+ 转交粒度是单件物品,可以只转其中一件。
+
+
+
+
+
+
+
+ 转交待确认
+ 可转交
+
+
(null)
const transferRemark = ref('')
const transferUsers = ref>([])
-// 整单待还合计 —— 与后端按 borrow_no 汇总的口径一致
-const transferTotalQty = computed(() =>
- transferCandidates.value.reduce((s: number, c: any) => s + (Number(c.pending_quantity) || 0), 0)
+// 弹窗内已勾选的明细(默认全选可转交项)
+const transferSelected = computed(() =>
+ transferCandidates.value.filter((c: any) => c.selected)
)
+// 已勾选项的待还合计
+const transferTotalQty = computed(() =>
+ transferSelected.value.reduce((s: number, c: any) => s + (Number(c.pending_quantity) || 0), 0)
+)
+
+// 该单下所有「待我接收」的转交 —— 主行聚合用。
+// (明细行只看自己那一条,见模板里的 c.pending_transfer)
+const myPendings = (row: any): any[] =>
+ (row.children || [])
+ .map((c: any) => c.pending_transfer)
+ .filter((p: any) => p && p.is_mine)
const loadTransferUsers = async () => {
if (transferUsers.value.length) return // 名单会话内复用,避免每次开弹窗都请求
@@ -725,15 +782,29 @@ const loadTransferUsers = async () => {
}
}
-const openTransferDialog = (row: any) => {
- // 仅未还清的明细可转交(与后端的状态准入一致)
+/**
+ * 打开转交弹窗。
+ *
+ * @param row 主行(单号层),用它的 children 作为候选明细
+ * @param detail 从**明细行**进入时传入该明细,只预选这一条;
+ * 从主行进入则不传,默认勾选全部可转交项。
+ *
+ * ★ 转交粒度已下沉到明细行:一张单可以只转其中一件(借 2 件转 1 件),
+ * 故这里用勾选而非「整单」。
+ */
+const openTransferDialog = (row: any, detail?: any) => {
const candidates = (row.children || []).filter((c: any) => (c.pending_quantity || 0) > 0)
if (!candidates.length) {
ElMessage.warning('该单号下没有未归还的明细,无法转交')
return
}
transferBorrowNo.value = row.borrow_no
- transferCandidates.value = candidates
+ // 已有待接收的明细不可再发起(后端也会拒)——直接置灰,避免用户白填一遍
+ transferCandidates.value = candidates.map((c: any) => ({
+ ...c,
+ selected: detail ? c.id === detail.id : !c.pending_transfer,
+ disabled: !!c.pending_transfer,
+ }))
transferToUserId.value = null
transferRemark.value = ''
transferDialogVisible.value = true
@@ -741,28 +812,21 @@ const openTransferDialog = (row: any) => {
}
const confirmTransfer = async () => {
+ const picked = transferSelected.value
+ if (!picked.length) return ElMessage.warning('请至少勾选一项要转交的物品')
if (!transferToUserId.value) return ElMessage.warning('请选择接收人')
- // ★ 整单转交:以后端为准,不再让用户挑明细。
- // 原实现只转所选的那一行,一张 2 明细的单转完会出现两个持有人
- // (前端按单号聚合就同时显示两个名字)—— 这是必须消灭的单内撕裂。
const receiver = transferUsers.value.find(u => u.id === transferToUserId.value)
- 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}】,无需转交`)
+ if (picked.every((c: any) => c.current_holder_id === transferToUserId.value)) {
+ return ElMessage.warning(`所选物品的当前持有人已是【${receiver?.name}】,无需转交`)
}
const qty = transferTotalQty.value
- const anchorId = transferCandidates.value[0].id
-
try {
await ElMessageBox.confirm(
- `将【${transferBorrowNo.value}】整单(${transferCandidates.value.length} 项,共 ${qty} 件)\n`
- + `的持有权转交给【${receiver?.name || ''}】?\n\n`
+ `将【${transferBorrowNo.value}】下的 ${picked.length} 项物品(共 ${qty} 件)转交给【${receiver?.name || ''}】?\n\n`
+ `发起后对方需在其借用列表中确认接收,责任才正式转移;\n`
- + `在对方确认前,物品与责任仍归当前持有人。`,
+ + `在对方确认前,物品与责任仍归各自的当前持有人。`,
'发起转交',
{ confirmButtonText: '确认发起', cancelButtonText: '取消', type: 'warning' }
)
@@ -770,19 +834,31 @@ const confirmTransfer = async () => {
return // 用户取消
}
- transferSubmitting.value = true // ★ 按钮 loading 防抖,避免重复提交
+ transferSubmitting.value = true
+ // ★ 逐项发起:转交粒度是明细行,每一项各自成立一条待接收流水。
+ // 刻意不做「全部成功才算成功」的伪原子 —— 物理上每件物品本就是独立交接的,
+ // 部分成功同样有意义。失败项由 request 拦截器 toast 具体原因,
+ // 列表刷新后未成功的仍显示为可转交。
+ let ok = 0
try {
- 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 || '已发起转交,等待对方确认')
- transferDialogVisible.value = false
- fetchData() // 刷新列表:状态列变为「转交待确认」
- } catch {
- // ★ 不重复弹错:request 拦截器已按后端 msg 全局 toast,此处再弹会双份
+ for (const c of picked) {
+ try {
+ await transferBorrow(c.id, {
+ transfer_qty: Number(c.pending_quantity) || 0, // 整行转交,后端二次校验
+ to_user_id: transferToUserId.value,
+ to_user_name: receiver?.name,
+ remark: transferRemark.value,
+ })
+ ok++
+ } catch {
+ // 单条失败不中断其余
+ }
+ }
+ if (ok) {
+ ElMessage.success(`已发起 ${ok}/${picked.length} 项转交,等待对方确认`)
+ transferDialogVisible.value = false
+ fetchData()
+ }
} finally {
transferSubmitting.value = false
}
@@ -790,41 +866,35 @@ 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
+const doAccept = async (pt: any, label: string): Promise => {
try {
await ElMessageBox.confirm(
- `确认接收【${pt.from_user_name || '对方'}】转交的这批资产?\n\n`
- + `单号:${row.borrow_no}(${cnt} 项)\n`
- + `确认后,该单全部未归还物品的持有责任转移到您名下,归还时须由您本人办理。`,
+ `确认接收【${pt.from_user_name || '对方'}】转交的物品?\n\n`
+ + `${label}\n`
+ + `确认后,该物品的持有责任转移到您名下,归还时须由您本人办理。`,
'确认接收转交',
{ confirmButtonText: '确认接收', cancelButtonText: '取消', type: 'warning' }
)
} catch {
- return
+ return false
}
- transferActionLoading.value = true
try {
const res: any = await acceptBorrowTransfer(pt.id)
ElMessage.success(res?.msg || '已接收')
- fetchData()
+ return true
} catch {
- // 错误提示由 request 拦截器统一处理
- } finally {
- transferActionLoading.value = false
+ return false // 错误提示由 request 拦截器统一处理
}
}
-const confirmRejectTransfer = async (row: any) => {
- const pt = row.pending_transfer
- if (!pt) return
+const doReject = async (pt: any, label: string): Promise => {
let reason = ''
try {
const r: any = await ElMessageBox.prompt(
- `拒绝【${pt.from_user_name || '对方'}】转交给您的这批资产?\n拒绝后责任仍由对方承担。`,
+ `拒绝【${pt.from_user_name || '对方'}】转交给您的物品?\n拒绝后责任仍由对方承担。\n\n${label}`,
'拒绝转交',
{
confirmButtonText: '确认拒绝',
@@ -835,16 +905,97 @@ const confirmRejectTransfer = async (row: any) => {
}
)
reason = r?.value || ''
+ } catch {
+ return false
+ }
+ try {
+ const res: any = await rejectBorrowTransfer(pt.id, reason)
+ ElMessage.success(res?.msg || '已拒绝')
+ return true
+ } catch {
+ return false
+ }
+}
+
+// --- 明细行入口(精准处理自己那一条)---
+const acceptOne = async (detail: any) => {
+ const label = `物品:${detail.material_name || detail.sku || ''}`
+ transferActionLoading.value = true
+ try {
+ if (await doAccept(detail.pending_transfer, label)) fetchData()
+ } finally {
+ transferActionLoading.value = false
+ }
+}
+
+const rejectOne = async (detail: any) => {
+ const label = `物品:${detail.material_name || detail.sku || ''}`
+ transferActionLoading.value = true
+ try {
+ if (await doReject(detail.pending_transfer, label)) fetchData()
+ } finally {
+ transferActionLoading.value = false
+ }
+}
+
+// --- 主行入口(聚合该单下所有属于我的待接收)---
+const acceptSlip = async (row: any) => {
+ const mine = myPendings(row)
+ if (!mine.length) return
+ try {
+ await ElMessageBox.confirm(
+ `确认接收该单下 ${mine.length} 项转交给您的物品?\n\n单号:${row.borrow_no}\n`
+ + `确认后,这些物品的持有责任一并转移到您名下。`,
+ '确认接收转交',
+ { confirmButtonText: '确认接收', cancelButtonText: '取消', type: 'warning' }
+ )
} catch {
return
}
transferActionLoading.value = true
try {
- const res: any = await rejectBorrowTransfer(pt.id, reason)
- ElMessage.success(res?.msg || '已拒绝')
- fetchData()
+ let ok = 0
+ for (const pt of mine) {
+ try {
+ await acceptBorrowTransfer(pt.id)
+ ok++
+ } catch {
+ // 单条失败不中断其余(拦截器已提示原因)
+ }
+ }
+ ElMessage.success(`已接收 ${ok}/${mine.length} 项`)
+ if (ok) fetchData()
+ } finally {
+ transferActionLoading.value = false
+ }
+}
+
+const rejectSlip = async (row: any) => {
+ const mine = myPendings(row)
+ if (!mine.length) return
+ try {
+ await ElMessageBox.confirm(
+ `拒绝该单下 ${mine.length} 项转交给您的物品?\n\n单号:${row.borrow_no}\n`
+ + `拒绝后责任仍由对方承担。`,
+ '拒绝转交',
+ { confirmButtonText: '确认拒绝', cancelButtonText: '取消', type: 'warning' }
+ )
} catch {
- // 同上
+ return
+ }
+ transferActionLoading.value = true
+ try {
+ let ok = 0
+ for (const pt of mine) {
+ try {
+ await rejectBorrowTransfer(pt.id)
+ ok++
+ } catch {
+ // 同上
+ }
+ }
+ ElMessage.success(`已拒绝 ${ok}/${mine.length} 项`)
+ if (ok) fetchData()
} finally {
transferActionLoading.value = false
}