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:
yueli
2026-09-17 09:18:06 +08:00
parent f7c49f41a8
commit ece5dd8d72
4 changed files with 452 additions and 14 deletions

View File

@ -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'
})
}
/**
* 提交「借出未归还」的**报废申请**(需审批人审批,通过后由库管执行报废)。
*

View File

@ -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(() => {

View File

@ -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">&nbsp;</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 {

View File

@ -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 {