feat(borrow): 前端转交/流转明细 UI 与借用人选人适配
转交 UI(借还记录页) ---- · 操作列新增【转交】,v-permission="'borrow_transfer'" + 按钮 loading 防抖 · 弹窗明示「仅支持整单全部转交」与「转交后须由接收人本人归还」,并在确认框 复述接收人与转交数量 · 列表是 borrow_no 主子表结构,trans_borrow.id 在**明细行**上,而一张单的明细 可各有不同持有人(转交是逐条明细进行的),故弹窗先让用户选定明细;单条明细 时自动选中(实测 54/60 的单号只有 1 条,等于零额外点击)。 该「单号按钮 → 弹窗选明细」范式沿用本页既有的「申请报废」。 流转明细时间线 ---- · 操作列新增【流转记录】,无特殊权限要求(页面本身已由 op_records 把守) · Drawer + el-timeline 倒序展示 借出 → 转交(可多次) → 归还 → 报废, 每节点显示时间、动作、当事人(借用人 / 转出→接收 / 实际归还人)与经手库管, 转交备注一并展示 · 主列表新增「当前持有人」列:转交后可能与「借用人」不是同一人;一张单的明细 持有人可能不同,去重后逐个展示,已全部回库显示「已回库」 身份锚点适配 ---- · 借出页:借用人由「自由输入姓名」改为「用户下拉」,提交 borrower_id (姓名无法唯一锚定一个人,重名即责任链断裂) · 归还页:新增「实际归还人」下拉,默认取首件物品的当前持有人,提交 returner_id; 主表新增「当前持有人」列提示库管该由谁来还 · 错误提示不再本地重复弹出 —— request 拦截器已按后端 msg 全局 toast, 再弹一次会双份 验证:vite build 通过;接口契约经后端回归用例校验。
This commit is contained in:
@ -137,6 +137,23 @@
|
||||
|
||||
<el-table-column v-if="hasColumnPermission('borrow_no')" prop="borrow_no" label="单号" width="180" show-overflow-tooltip />
|
||||
<el-table-column v-if="hasColumnPermission('borrower_name')" prop="borrower_name" label="借用人" width="100" />
|
||||
<!-- ★ 当前持有人:转交后可能与「借用人」不是同一人。
|
||||
一张单的明细可能持有人各不相同,故去重后逐个展示。 -->
|
||||
<el-table-column label="当前持有人" width="120">
|
||||
<template #default="{row}">
|
||||
<template v-if="currentHolders(row).length">
|
||||
<el-tag
|
||||
v-for="h in currentHolders(row)"
|
||||
:key="h"
|
||||
type="warning"
|
||||
size="small"
|
||||
style="margin: 1px 2px"
|
||||
>{{ h }}</el-tag>
|
||||
</template>
|
||||
<span v-else-if="row.status === 'returned'" style="color: #67C23A; font-size: 12px">已回库</span>
|
||||
<span v-else style="color: #c0c4cc">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="hasColumnPermission('borrow_time')" prop="borrow_time" label="借出时间" width="160" sortable />
|
||||
<el-table-column label="借出物品" width="90" align="center">
|
||||
<template #default="{row}">
|
||||
@ -192,14 +209,28 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- ★ 报废操作列:仅未归还记录 + 库管/主管可见 -->
|
||||
<el-table-column v-if="canScrap" label="操作" width="90" align="center" fixed="right">
|
||||
<!-- ★ 操作列
|
||||
· 申请报废 / 转交 —— 仅未归还记录,且分别受各自权限约束
|
||||
· 流转记录 —— 无特殊权限要求,凡能看到本列表者皆可查看 -->
|
||||
<el-table-column label="操作" width="210" align="center" fixed="right">
|
||||
<template #default="{row}">
|
||||
<el-button
|
||||
v-if="row.status === 'borrowed' || row.status === 'partial_returned'"
|
||||
v-if="canScrap && isUnreturned(row)"
|
||||
type="danger" link size="small"
|
||||
@click="openScrapDialog(row)"
|
||||
>申请报废</el-button>
|
||||
|
||||
<el-button
|
||||
v-if="isUnreturned(row)"
|
||||
v-permission="'borrow_transfer'"
|
||||
type="primary" link size="small"
|
||||
@click="openTransferDialog(row)"
|
||||
>转交</el-button>
|
||||
|
||||
<el-button
|
||||
type="info" link size="small"
|
||||
@click="openHistoryDrawer(row)"
|
||||
>流转记录</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@ -256,6 +287,115 @@
|
||||
<el-button type="danger" :loading="scrapSubmitting" @click="confirmScrap">提交报废申请</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ★ 借库转交弹窗(一期:仅支持整单全部转交) -->
|
||||
<el-dialog v-model="transferDialogVisible" title="借库转交" width="640px" destroy-on-close>
|
||||
<el-alert
|
||||
title="仅支持整单全部转交"
|
||||
description="一张借出单同时只能有一个持有人。部分转交会让「当前持有人」无法确定,故一期不支持;如需拆分持有,请先办理部分归还,再对剩余量转交。"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
style="margin-bottom: 14px"
|
||||
/>
|
||||
<div style="margin-bottom: 10px;">
|
||||
<div style="font-weight: 600; margin-bottom: 6px;">单号:{{ transferBorrowNo }}</div>
|
||||
<div style="font-size: 13px; color: #909399;">请选择要转交的明细(转交后该明细的持有人整体变更为接收人):</div>
|
||||
</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>
|
||||
<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="100" align="center">
|
||||
<template #default="{row}">
|
||||
<el-tag type="warning" size="small">{{ row.current_holder_name || '—' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="待还数量" width="90" align="center">
|
||||
<template #default="{row}">
|
||||
<span style="color:#F56C6C; font-weight:bold">{{ row.pending_quantity }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-form label-width="80px" style="margin-top: 14px;">
|
||||
<el-form-item label="接收人" required>
|
||||
<el-select v-model="transferToUserId" placeholder="请选择接收人" style="width: 100%" filterable>
|
||||
<el-option v-for="u in transferUsers" :key="u.id" :label="u.name" :value="u.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="转交备注">
|
||||
<el-input
|
||||
v-model="transferRemark"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="如:项目交接 / 人员离岗,可留空"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="transferDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="transferSubmitting" @click="confirmTransfer">确认转交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ★ 流转明细时间线:借出 → 转交(可多次) → 归还 → 报废,倒序展示 -->
|
||||
<el-drawer v-model="historyDrawerVisible" title="流转明细" size="580px" destroy-on-close>
|
||||
<div v-loading="historyLoading">
|
||||
<div style="font-weight: 600; margin-bottom: 4px;">单号:{{ historyBorrowNo }}</div>
|
||||
<div style="font-size: 12px; color: #909399; margin-bottom: 16px;">
|
||||
按时间倒序,最新动作在最上方
|
||||
</div>
|
||||
<el-empty v-if="!historyEvents.length" description="暂无流转记录" :image-size="70" />
|
||||
<el-timeline v-else>
|
||||
<el-timeline-item
|
||||
v-for="(e, i) in historyEvents"
|
||||
:key="i"
|
||||
:timestamp="e.time"
|
||||
placement="top"
|
||||
:type="eventMeta(e.type).type"
|
||||
:hollow="e.type === 'borrow'"
|
||||
>
|
||||
<div style="display:flex; align-items:center; gap:8px; margin-bottom:4px;">
|
||||
<el-tag :type="eventMeta(e.type).type" size="small">{{ eventMeta(e.type).label }}</el-tag>
|
||||
<span style="font-size: 13px; color:#606266;">{{ e.material_name || e.sku }}</span>
|
||||
<span style="font-size: 13px; color:#909399;">× {{ e.quantity }}</span>
|
||||
</div>
|
||||
<div style="font-size: 13px; color:#303133;">
|
||||
<template v-if="e.type === 'borrow'">
|
||||
借用人:<b>{{ e.actor_name || '—' }}</b>
|
||||
<span v-if="e.operator_name" style="color:#909399;"> · 发货库管:{{ e.operator_name }}</span>
|
||||
</template>
|
||||
<template v-else-if="e.type === 'transfer'">
|
||||
<b>{{ e.from_name || '—' }}</b> → <b>{{ e.actor_name || '—' }}</b>
|
||||
<span v-if="e.operator_name" style="color:#909399;"> · 经手库管:{{ e.operator_name }}</span>
|
||||
</template>
|
||||
<template v-else-if="e.type === 'return'">
|
||||
归还人:<b>{{ e.actor_name || '—' }}</b>
|
||||
<span v-if="e.operator_name" style="color:#909399;"> · 经手库管:{{ e.operator_name }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
报废 · 经手库管:<b>{{ e.operator_name || '—' }}</b>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="e.remark" style="font-size: 12px; color:#909399; margin-top:2px;">
|
||||
备注:{{ e.remark }}
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -268,7 +408,12 @@ import 'dayjs/locale/zh-cn'
|
||||
dayjs.locale('zh-cn')
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { getApproversList } from '@/api/auth'
|
||||
import { submitBorrowScrapRequest } from '@/api/transaction'
|
||||
import {
|
||||
submitBorrowScrapRequest,
|
||||
transferBorrow,
|
||||
getBorrowUsers,
|
||||
getBorrowSlipHistory,
|
||||
} from '@/api/transaction'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
@ -450,6 +595,131 @@ const confirmScrap = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// ★ 借库转交(一期:仅支持整单全部转交)
|
||||
// ==========================================================================
|
||||
// 未归还判定:主表状态下 borrowed / partial_returned 都还有实物未回库
|
||||
const isUnreturned = (row: any) =>
|
||||
row.status === 'borrowed' || row.status === 'partial_returned'
|
||||
|
||||
// 当前持有人:整单下各明细可能持有人不同(转交是逐条明细进行的),去重后展示
|
||||
const currentHolders = (row: any): string[] => {
|
||||
const names = (row.children || [])
|
||||
.map((c: any) => c.current_holder_name)
|
||||
.filter((n: any) => !!n)
|
||||
return Array.from(new Set(names)) as string[]
|
||||
}
|
||||
|
||||
const transferDialogVisible = ref(false)
|
||||
const transferSubmitting = ref(false)
|
||||
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 }>>([])
|
||||
|
||||
const loadTransferUsers = async () => {
|
||||
if (transferUsers.value.length) return // 名单会话内复用,避免每次开弹窗都请求
|
||||
try {
|
||||
const res: any = await getBorrowUsers()
|
||||
transferUsers.value = res?.data || []
|
||||
} catch {
|
||||
transferUsers.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const openTransferDialog = (row: 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
|
||||
// 实测 54/60 的单号只有 1 条明细 —— 单条时直接选中,省掉一次点击
|
||||
transferTargetId.value = candidates.length === 1 ? candidates[0].id : null
|
||||
transferToUserId.value = null
|
||||
transferRemark.value = ''
|
||||
transferDialogVisible.value = true
|
||||
loadTransferUsers()
|
||||
}
|
||||
|
||||
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)
|
||||
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 qty = target?.pending_quantity ?? 0
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`将【${target?.material_name || target?.sku || ''}】的持有权整单转交给【${receiver?.name || ''}】?\n\n`
|
||||
+ `转交数量:${qty}(整单全量,不支持部分转交)\n`
|
||||
+ `转交后,该物品须由接收人本人办理归还。`,
|
||||
'转交确认',
|
||||
{ confirmButtonText: '确认转交', cancelButtonText: '取消', type: 'warning' }
|
||||
)
|
||||
} catch {
|
||||
return // 用户取消
|
||||
}
|
||||
|
||||
transferSubmitting.value = true // ★ 按钮 loading 防抖,避免重复提交
|
||||
try {
|
||||
const res: any = await transferBorrow(transferTargetId.value, {
|
||||
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,此处再弹会双份
|
||||
} finally {
|
||||
transferSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// ★ 流转明细时间线(借出 → 转交(可多次) → 归还 → 报废)
|
||||
// ==========================================================================
|
||||
const historyDrawerVisible = ref(false)
|
||||
const historyLoading = ref(false)
|
||||
const historyBorrowNo = ref('')
|
||||
const historyEvents = ref<any[]>([])
|
||||
|
||||
const EVENT_META: Record<string, { label: string; type: any }> = {
|
||||
borrow: { label: '借出', type: 'primary' },
|
||||
transfer: { label: '转交', type: 'warning' },
|
||||
return: { label: '归还', type: 'success' },
|
||||
scrap: { label: '报废', type: 'danger' },
|
||||
}
|
||||
const eventMeta = (t: string) => EVENT_META[t] || { label: t, type: 'info' }
|
||||
|
||||
const openHistoryDrawer = async (row: any) => {
|
||||
historyBorrowNo.value = row.borrow_no
|
||||
historyEvents.value = []
|
||||
historyDrawerVisible.value = true
|
||||
historyLoading.value = true
|
||||
try {
|
||||
const res: any = await getBorrowSlipHistory(row.borrow_no)
|
||||
historyEvents.value = res?.data?.events || []
|
||||
} catch {
|
||||
// 同上:错误提示由 request 拦截器统一处理
|
||||
} finally {
|
||||
historyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user