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:
@ -215,12 +215,18 @@ export function getBorrowStockList(params: { page?: number; pageSize?: number; k
|
||||
|
||||
/**
|
||||
* 执行借库扣减(审批通过后调用)
|
||||
* @param data approval_id + 扫码选中的物品 + 借用人信息 + 签名
|
||||
*
|
||||
* ★ borrower_id 为**必填**:借用人身份从「姓名」升级为「ID 锚点」后,
|
||||
* 后端以 borrower_id 反查 sys_user 生成姓名快照;缺失会被直接拒绝。
|
||||
* borrower_name 仅作兼容保留,后端不采信。
|
||||
*
|
||||
* @param data approval_id + 扫码选中的物品 + 借用人ID + 签名
|
||||
*/
|
||||
export function dispatchBorrow(data: {
|
||||
approval_id: number
|
||||
items: Array<any>
|
||||
borrower_name: string
|
||||
borrower_id: number
|
||||
borrower_name?: string
|
||||
signature_path: string
|
||||
remark?: string
|
||||
expected_return_time?: string | null
|
||||
@ -232,6 +238,67 @@ export function dispatchBorrow(data: {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 借库责任链人员名单(借用人 / 转交接收人 / 实际归还人共用)
|
||||
* @returns [{ id, name }]
|
||||
*/
|
||||
export function getBorrowUsers() {
|
||||
return request({
|
||||
url: '/v1/transactions/borrow/users',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 借库转交(一期:仅支持整单全量转交)
|
||||
*
|
||||
* ★ transfer_qty 必须严格等于待还量,部分转交会被后端拒绝 ——
|
||||
* 单行模型无法同时追踪两个持有人。
|
||||
* ★ 转交是纯持有权变更,不触碰任何库存字段。
|
||||
*
|
||||
* @param borrowId trans_borrow.id
|
||||
* @param data transfer_qty + to_user_id + remark
|
||||
*/
|
||||
export function transferBorrow(borrowId: number, data: {
|
||||
transfer_qty: number
|
||||
to_user_id: number
|
||||
to_user_name?: string
|
||||
remark?: string
|
||||
}) {
|
||||
return request({
|
||||
url: `/v1/transactions/borrow/${borrowId}/transfer`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 借出单的流转历史(**单品**维度):转交链 + 逐次归还明细
|
||||
* @returns { record, transfers[], returns[] }
|
||||
*/
|
||||
export function getBorrowHistory(borrowId: number) {
|
||||
return request({
|
||||
url: `/v1/transactions/borrow/${borrowId}/history`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 整单流转时间线(**borrow_no** 维度):借出 → 转交(可多次) → 归还 → 报废
|
||||
*
|
||||
* ★ 与 getBorrowHistory 的分工:列表页是 borrow_no 主子表结构(单张单最多
|
||||
* 21 条明细),逐条明细调用单品接口会产生 21 个请求,且各条时间线无法全局
|
||||
* 排序。本接口一次返回合并后的事件流,按时间**倒序**。
|
||||
*
|
||||
* @returns { borrow_no, events[], records[] }
|
||||
*/
|
||||
export function getBorrowSlipHistory(borrowNo: string) {
|
||||
return request({
|
||||
url: `/v1/transactions/borrow/slip/${encodeURIComponent(borrowNo)}/history`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交「借出未归还」的**报废申请**(需审批人审批,通过后由库管执行报废)。
|
||||
*
|
||||
|
||||
@ -207,8 +207,25 @@
|
||||
<el-form :model="form" ref="formRef" :rules="rules" label-position="top">
|
||||
<el-row :gutter="15">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="领用人/借用人" prop="borrower_name">
|
||||
<el-input v-model="form.borrower_name" placeholder="请输入姓名" size="large" />
|
||||
<!-- ★ 借用人由「自由输入姓名」改为「选择用户」:
|
||||
姓名无法唯一锚定一个人(重名即责任链断裂),
|
||||
转交/归还的持有人校验都依赖 borrower_id 这个唯一锚点。 -->
|
||||
<el-form-item label="领用人/借用人" prop="borrower_id">
|
||||
<el-select
|
||||
v-model="form.borrower_id"
|
||||
filterable
|
||||
placeholder="请选择借用人"
|
||||
size="large"
|
||||
style="width: 100%"
|
||||
@change="handleBorrowerChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="u in borrowerOptions"
|
||||
:key="u.id"
|
||||
:label="u.name"
|
||||
:value="u.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
@ -360,7 +377,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Scissor, EditPen, Delete, CameraFilled, Close, Refresh, Select, LocationInformation } from '@element-plus/icons-vue'
|
||||
import QrScanner from '@/components/QrScanner/index.vue'
|
||||
import { getStockByBarcode, getStockAlternatives, getScanDraft, saveScanDraft, clearScanDraft } from '@/api/outbound'
|
||||
import { dispatchBorrow, getBorrowApprovalList } from '@/api/transaction'
|
||||
import { dispatchBorrow, getBorrowApprovalList, getBorrowUsers } from '@/api/transaction'
|
||||
import { uploadFile } from '@/api/common/upload'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
@ -709,13 +726,31 @@ const lastX = ref(0)
|
||||
const lastY = ref(0)
|
||||
|
||||
const form = reactive({
|
||||
borrower_name: '',
|
||||
borrower_id: null as number | null, // ★ 唯一身份锚点,提交给后端
|
||||
borrower_name: '', // 由所选用户派生,仅用于本地展示
|
||||
expected_return_time: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// ★ 借用人名单:与转交接收人、归还人共用同一数据源,按公司隔离
|
||||
const borrowerOptions = ref<Array<{ id: number; name: string }>>([])
|
||||
|
||||
const loadBorrowerOptions = async () => {
|
||||
try {
|
||||
const res: any = await getBorrowUsers()
|
||||
borrowerOptions.value = res?.data || []
|
||||
} catch {
|
||||
borrowerOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const handleBorrowerChange = (id: number) => {
|
||||
const hit = borrowerOptions.value.find(u => u.id === id)
|
||||
form.borrower_name = hit ? hit.name : ''
|
||||
}
|
||||
|
||||
const rules = computed(() => ({
|
||||
borrower_name: [{ required: true, message: '请输入借用人姓名', trigger: 'blur' }],
|
||||
borrower_id: [{ required: true, message: '请选择借用人', trigger: 'change' }],
|
||||
expected_return_time: [
|
||||
{ required: !isIndefinite.value, message: '请选择预计归还日期', trigger: 'change' }
|
||||
]
|
||||
@ -870,6 +905,7 @@ const clearAll = () => {
|
||||
ElMessageBox.confirm('确定清空所有已选物品吗?', '提示', { type: 'warning' })
|
||||
.then(() => {
|
||||
cartItems.value = []
|
||||
form.borrower_id = null
|
||||
form.borrower_name = ''
|
||||
form.remark = ''
|
||||
form.expected_return_time = ''
|
||||
@ -933,7 +969,8 @@ const submitForm = async () => {
|
||||
await dispatchBorrow({
|
||||
approval_id: selectedApprovalId.value,
|
||||
items: itemsPayload,
|
||||
borrower_name: form.borrower_name,
|
||||
borrower_id: form.borrower_id as number, // ★ 唯一身份锚点
|
||||
borrower_name: form.borrower_name, // 仅作兼容,后端不采信
|
||||
signature_path: signatureUrl,
|
||||
remark: form.remark,
|
||||
expected_return_time: isIndefinite.value ? null : form.expected_return_time
|
||||
@ -947,6 +984,7 @@ const submitForm = async () => {
|
||||
}
|
||||
|
||||
cartItems.value = []
|
||||
form.borrower_id = null
|
||||
form.borrower_name = ''
|
||||
form.expected_return_time = ''
|
||||
form.remark = ''
|
||||
@ -1043,6 +1081,7 @@ const handleSignCancel = () => { showSignatureDialog.value = false }
|
||||
import { onMounted } from 'vue'
|
||||
onMounted(() => {
|
||||
loadApprovalRequests()
|
||||
loadBorrowerOptions()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -46,6 +46,13 @@
|
||||
<div v-if="returnList.length > 0">
|
||||
<el-table :data="returnList" border stripe style="width: 100%">
|
||||
<el-table-column v-if="hasColumnPermission('borrower_name')" prop="borrower_name" label="借用人" width="90" show-overflow-tooltip />
|
||||
<!-- ★ 当前持有人:转交后可能与「借用人」不是同一人,归还时必须由他本人来还 -->
|
||||
<el-table-column label="当前持有人" width="100" show-overflow-tooltip>
|
||||
<template #default="{row}">
|
||||
<el-tag v-if="row.current_holder_name" type="warning">{{ row.current_holder_name }}</el-tag>
|
||||
<span v-else style="color: #c0c4cc">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="hasColumnPermission('sku')" prop="sku" label="SKU" width="120" show-overflow-tooltip />
|
||||
<el-table-column label="借出数量" width="80" align="center">
|
||||
<template #default="{row}">
|
||||
@ -109,6 +116,27 @@
|
||||
</div>
|
||||
|
||||
<el-form label-position="top">
|
||||
<!-- ★ 实际归还人:后端强校验其必须等于物品的当前持有人,
|
||||
不匹配会整单拒绝。默认取首件物品的当前持有人,可手动改。 -->
|
||||
<el-form-item label="实际归还人" required>
|
||||
<el-select
|
||||
v-model="returnerId"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="请选择实际把物品交回的人"
|
||||
size="large"
|
||||
style="width: 100%"
|
||||
:disabled="!userStore.hasPermission('op_return:operation')"
|
||||
>
|
||||
<el-option
|
||||
v-for="u in returnerOptions"
|
||||
:key="u.id"
|
||||
:label="u.name"
|
||||
:value="u.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item required>
|
||||
<div class="signature-box" @click="openSignatureDialog" v-if="userStore.hasPermission('op_return:operation')">
|
||||
<div v-if="signaturePreviewUrl" class="signed-img">
|
||||
@ -194,8 +222,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick, onUnmounted } from 'vue'
|
||||
import { ref, nextTick, onMounted, onUnmounted } from 'vue'
|
||||
import request from '@/utils/request'
|
||||
import { getBorrowUsers } from '@/api/transaction'
|
||||
import { uploadFile } from '@/api/common/upload'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Scissor, EditPen, Delete, CameraFilled, Close, Refresh, Select } from '@element-plus/icons-vue'
|
||||
@ -228,6 +257,24 @@ const loading = ref(false)
|
||||
const showCamera = ref(false)
|
||||
const barcodeRef = ref()
|
||||
|
||||
// ★ 实际归还人(一期转交改造):必须等于物品的当前持有人,后端强校验。
|
||||
// 与借出页的借用人下拉共用同一份公司隔离名单。
|
||||
const returnerId = ref<number | null>(null)
|
||||
const returnerOptions = ref<Array<{ id: number; name: string }>>([])
|
||||
|
||||
const loadReturnerOptions = async () => {
|
||||
try {
|
||||
const res: any = await getBorrowUsers()
|
||||
returnerOptions.value = res?.data || []
|
||||
} catch {
|
||||
returnerOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadReturnerOptions()
|
||||
})
|
||||
|
||||
// 签名状态
|
||||
const showSignatureDialog = ref(false)
|
||||
const signaturePreviewUrl = ref('')
|
||||
@ -285,6 +332,12 @@ const scanItem = async () => {
|
||||
item.return_qty = item.pending_quantity
|
||||
|
||||
returnList.value.push(item)
|
||||
// ★ 首次扫码时用物品的当前持有人预填归还人(可手动改):
|
||||
// 绝大多数场景就是本人来还,预填省一步操作;若清单里混入了他人持有的
|
||||
// 物品,后端会拒绝并明确指出是谁持有的,不会被静默放过。
|
||||
if (returnerId.value === null && item.current_holder_id) {
|
||||
returnerId.value = item.current_holder_id
|
||||
}
|
||||
barcode.value = ''
|
||||
ElMessage.success('识别成功')
|
||||
} catch(e) {
|
||||
@ -305,6 +358,7 @@ const clearAll = () => {
|
||||
signatureFile.value = null
|
||||
signaturePreviewUrl.value = ''
|
||||
barcode.value = ''
|
||||
returnerId.value = null
|
||||
})
|
||||
}
|
||||
|
||||
@ -317,6 +371,12 @@ const preSubmitCheck = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
// ★ 归还人必须选择:后端要求其等于每件物品的当前持有人
|
||||
if (!returnerId.value) {
|
||||
ElMessage.error('请选择实际归还人')
|
||||
return
|
||||
}
|
||||
|
||||
// 1. 处理空输入:默认为原库位
|
||||
returnList.value.forEach(item => {
|
||||
if (!item.return_location || item.return_location.trim() === '') {
|
||||
@ -362,7 +422,8 @@ const submitReturn = async () => {
|
||||
method: 'post',
|
||||
data: {
|
||||
items: returnList.value,
|
||||
signature_path: upRes.data.url
|
||||
signature_path: upRes.data.url,
|
||||
returner_id: returnerId.value // ★ 实际归还人,后端强校验 == 当前持有人
|
||||
}
|
||||
})
|
||||
ElMessage.success('还库成功')
|
||||
@ -370,6 +431,7 @@ const submitReturn = async () => {
|
||||
signatureFile.value = null
|
||||
signaturePreviewUrl.value = ''
|
||||
showCamera.value = false
|
||||
returnerId.value = null
|
||||
} catch(e: any) {
|
||||
ElMessage.error(e.response?.data?.msg || '提交失败')
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user