· 转交弹窗由「选择明细」改为**整单**:列出该单全部未还物品(只读), 明确告知「发起后对方确认,责任才转移」。原先让用户挑一行,挑中就只转 那一行 —— 正是单内撕裂的入口,现从 UI 上根除。 · 操作栏新增【接收转交】【拒绝】:仅对 pending_transfer.is_mine 的行显示, 即「待我接收」的转交。已有 PENDING 时不再显示【转交】,避免重复发起。 · 状态列新增「转交待确认」:此时主表 current_holder 未变(东西还在原持有人 手上),必须用状态列把「责任正在转移中」显式表达出来,否则界面上看不出 任何变化。 · 新增 acceptBorrowTransfer / rejectBorrowTransfer API; 拒绝时用 prompt 收集可选原因,写入转交流水备注。 错误提示仍交由 request 拦截器统一 toast,不在本地重复弹出。
1074 lines
44 KiB
Vue
1074 lines
44 KiB
Vue
<template>
|
||
<div v-if="userStore.hasPermission('op_records')" class="app-container">
|
||
<!-- ★ 高级搜索:与出库记录 / 报废记录统一版式 -->
|
||
<el-form :inline="true" class="filter-form" @submit.prevent>
|
||
<el-form-item>
|
||
<el-radio-group v-model="status" @change="handleSearchImmediate">
|
||
<el-radio-button label="all">全部</el-radio-button>
|
||
<el-radio-button label="borrowed">未归还</el-radio-button>
|
||
<el-radio-button label="returned">已归还</el-radio-button>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="搜索">
|
||
<el-input
|
||
v-model="keyword"
|
||
placeholder="输入关键词"
|
||
style="width: 240px"
|
||
clearable
|
||
@input="debouncedSearch"
|
||
@clear="handleClearSearch"
|
||
>
|
||
<template #prepend>
|
||
<el-select v-model="searchType" placeholder="类型" style="width: 110px" @change="handleSearchImmediate">
|
||
<el-option label="全部" value="all" />
|
||
<el-option label="单号" value="no" />
|
||
<el-option label="借用人" value="name" />
|
||
<el-option label="SKU" value="sku" />
|
||
<el-option label="物料名称" value="material_name" />
|
||
<el-option label="规格型号" value="spec_model" />
|
||
</el-select>
|
||
</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="借出日期">
|
||
<el-date-picker
|
||
v-model="dateRange"
|
||
type="daterange"
|
||
range-separator="至"
|
||
start-placeholder="开始日期"
|
||
end-placeholder="结束日期"
|
||
value-format="YYYY-MM-DD"
|
||
style="width: 260px"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item>
|
||
<el-button type="primary" @click="fetchData">查询</el-button>
|
||
<el-button @click="resetFilter">重置</el-button>
|
||
|
||
<!-- ★ 高级筛选:对齐 material/list.vue 既有模式 -->
|
||
<el-popover
|
||
v-model:visible="advancedFilterVisible"
|
||
placement="bottom"
|
||
title="高级筛选"
|
||
width="600"
|
||
trigger="manual"
|
||
>
|
||
<template #reference>
|
||
<el-button plain @click="advancedFilterVisible = !advancedFilterVisible">
|
||
高级筛选
|
||
<el-badge v-if="appliedConditions.length" :value="appliedConditions.length" style="margin-left:6px" />
|
||
</el-button>
|
||
</template>
|
||
<div class="advanced-filter">
|
||
<div
|
||
v-for="(condition, index) in advancedConditions"
|
||
:key="index"
|
||
class="condition-row"
|
||
>
|
||
<el-select v-model="condition.field" placeholder="字段" style="width:180px" :teleported="false">
|
||
<el-option v-for="f in fieldOptions" :key="f.value" :label="f.label" :value="f.value" />
|
||
</el-select>
|
||
<el-select v-model="condition.operator" placeholder="操作符" style="width:120px; margin-left:8px" :teleported="false">
|
||
<el-option v-for="op in operatorOptions" :key="op.value" :label="op.label" :value="op.value" />
|
||
</el-select>
|
||
<el-input v-model="condition.value" placeholder="值" style="width:180px; margin-left:8px" />
|
||
<el-button v-if="advancedConditions.length > 1" type="danger" link @click="removeCondition(index)" style="margin-left:8px">删除</el-button>
|
||
</div>
|
||
<div style="margin-top:12px">
|
||
<el-button type="primary" link @click="addCondition">添加条件</el-button>
|
||
<el-button type="primary" @click="applyAdvancedFilter">应用筛选</el-button>
|
||
<el-button @click="resetAdvancedFilter">重置</el-button>
|
||
</div>
|
||
</div>
|
||
</el-popover>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<el-table
|
||
:data="list"
|
||
border
|
||
stripe
|
||
style="margin-top:20px"
|
||
v-loading="loading"
|
||
type="expand"
|
||
>
|
||
<el-table-column type="expand">
|
||
<template #default="props">
|
||
<div style="padding: 10px 40px;">
|
||
<h4 style="margin: 0 0 10px; font-size: 14px; color: #606266;">借出明细</h4>
|
||
<el-table :data="props.row.children" border size="small">
|
||
<el-table-column prop="material_name" label="物料名称" min-width="140" show-overflow-tooltip />
|
||
<el-table-column prop="sku" label="SKU" width="120" show-overflow-tooltip />
|
||
<el-table-column label="借出数量" width="80" align="center">
|
||
<template #default="{row}">
|
||
<el-tag type="info">{{ row.quantity }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="已还数量" width="80" align="center">
|
||
<template #default="{row}">
|
||
<el-tag type="success">{{ row.returned_quantity || 0 }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="待还数量" width="80" align="center">
|
||
<template #default="{row}">
|
||
<el-tag v-if="row.pending_quantity > 0" type="warning">{{ row.pending_quantity }}</el-tag>
|
||
<el-tag v-else type="success">0</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="location" label="借出库位" min-width="120">
|
||
<template #default="{row}">
|
||
<span v-if="row.location">{{ row.location }}</span>
|
||
<span v-else style="color:#ccc">-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="return_location" label="归还库位" min-width="120">
|
||
<template #default="{row}">
|
||
<span v-if="row.return_location">{{ row.return_location }}</span>
|
||
<span v-else style="color:#ccc">-</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<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')" label="借用人" width="100">
|
||
<template #default="{row}">
|
||
<span class="name-fixed">{{ formatName(row.borrower_name) || '—' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<!-- ★ 当前持有人:转交后可能与「借用人」不是同一人;一张单的明细持有人
|
||
也可能各不相同,故去重后逐个展示。
|
||
表达上做了降噪:未转交=普通文本,转过交=主题色加粗。原先一律用橙色
|
||
标签,一屏几十个色块会把真正需要关注的「已转交」淹没。 -->
|
||
<el-table-column label="当前持有人" width="140">
|
||
<template #default="{row}">
|
||
<template v-if="currentHolderList(row).length">
|
||
<span
|
||
v-for="h in currentHolderList(row)"
|
||
:key="h.name"
|
||
class="holder-name"
|
||
:class="{ 'is-transferred': h.transferred }"
|
||
>{{ formatName(h.name) }}</span>
|
||
</template>
|
||
<span v-else-if="row.status === 'returned' || row.status === 'scrapped'" class="text-info">已回库</span>
|
||
<span v-else class="text-info">—</span>
|
||
</template>
|
||
</el-table-column>
|
||
<!-- ★ 去掉 sortable:这是 Element Plus 的**前端排序**,只会重排当前页的
|
||
10 条,反而打乱后端按单号聚合的「逾期优先」排序,让列表看起来没规律。
|
||
若将来确实需要按其他字段排序,应改为 sortable="custom" 并由后端接管。 -->
|
||
<el-table-column v-if="hasColumnPermission('borrow_time')" prop="borrow_time" label="借出时间" width="160" />
|
||
<el-table-column label="借出物品" width="90" align="center">
|
||
<template #default="{row}">
|
||
<el-tag type="info">{{ row.children ? row.children.length : 0 }} 项</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<!-- ★ 归还人改为整单汇总:主行原取「首条明细」的归还人,多明细分批归还时
|
||
会只显示其中一位,令人误以为其他人没还过。 -->
|
||
<el-table-column v-if="hasColumnPermission('return_operator')" label="归还人" width="110">
|
||
<template #default="{row}">
|
||
<template v-if="row.return_operators && row.return_operators.length">
|
||
<span v-for="op in row.return_operators" :key="op" class="name-fixed">{{ formatName(op) }}</span>
|
||
</template>
|
||
<span v-else class="text-info">—</span>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column v-if="hasColumnPermission('expected_return_time') || hasColumnPermission('return_time')" label="归还时间 / 预计" min-width="200">
|
||
<template #default="{row}">
|
||
<div v-if="row.status === 'returned'">
|
||
<el-tag type="success" size="small">实际</el-tag>
|
||
{{ row.return_time || '-' }}
|
||
</div>
|
||
<!-- ★ 降噪:「无限期」不是异常,不该用蓝色标签抢视觉重心 ——
|
||
把注意力留给红色的「已逾期」。改为灰色小字。(沿用系统既有术语
|
||
「无限期」,与借出页的「无限期/长期借用」保持一致) -->
|
||
<div v-else-if="!row.expected_return_time">
|
||
<span class="text-info">无限期</span>
|
||
</div>
|
||
<div v-else>
|
||
<span>预计 {{ formatExpectedTime(row.expected_return_time).text }} </span>
|
||
<span :style="{
|
||
color: formatExpectedTime(row.expected_return_time).style.color || 'inherit',
|
||
fontWeight: formatExpectedTime(row.expected_return_time).style.fontWeight || 'normal',
|
||
marginLeft: '4px'
|
||
}">
|
||
{{ formatExpectedTime(row.expected_return_time).diffText }}
|
||
</span>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column v-if="hasColumnPermission('status')" label="状态" width="110" align="center">
|
||
<template #default="{row}">
|
||
<!-- ★ 中间态:已发起转交但接收人尚未确认。
|
||
此时主表 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>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column v-if="hasColumnPermission('borrow_signature') || hasColumnPermission('return_signature')" label="电子签名" width="140" align="center">
|
||
<template #default="{row}">
|
||
<div style="display:flex; justify-content: center; gap:10px">
|
||
<el-popover trigger="hover" placement="top" v-if="row.borrow_signature && hasColumnPermission('borrow_signature')" width="220">
|
||
<template #reference><el-tag size="small">借</el-tag></template>
|
||
<img :src="row.borrow_signature" style="width:200px; border:1px solid #eee" />
|
||
</el-popover>
|
||
|
||
<el-popover trigger="hover" placement="top" v-if="row.return_signature && hasColumnPermission('return_signature')" width="220">
|
||
<template #reference><el-tag type="success" size="small">还</el-tag></template>
|
||
<img :src="row.return_signature" style="width:200px; border:1px solid #eee" />
|
||
</el-popover>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<!-- ★ 操作列
|
||
· 申请报废 / 转交 —— 仅未归还记录,且分别受各自权限约束
|
||
· 流转记录 —— 无特殊权限要求,凡能看到本列表者皆可查看 -->
|
||
<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) && !row.pending_transfer"
|
||
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>
|
||
|
||
<el-pagination
|
||
background
|
||
layout="prev, pager, next"
|
||
:total="total"
|
||
@current-change="handlePage"
|
||
style="margin-top:10px; text-align:right"
|
||
/>
|
||
|
||
<!-- ★ 借库报废**申请**弹窗(需审批,不再直报) -->
|
||
<el-dialog v-model="scrapDialogVisible" title="申请借库报废(未归还)" width="650px" destroy-on-close>
|
||
<el-alert
|
||
title="提交后进入审批流程:审批通过并执行后,该笔借出才不再追讨归还,并从总库存中扣除。"
|
||
type="warning"
|
||
:closable="false"
|
||
show-icon
|
||
style="margin-bottom: 12px"
|
||
/>
|
||
<div style="margin-bottom: 12px;">
|
||
<div style="font-weight: 600; margin-bottom: 6px;">单号: {{ scrapBorrowNo }}</div>
|
||
<div style="font-size: 13px; color: #909399;">请勾选要报废的未归还明细:</div>
|
||
</div>
|
||
<el-table :data="scrapCandidates" border size="small" max-height="300" @selection-change="handleScrapSelection">
|
||
<el-table-column type="selection" width="50" align="center" />
|
||
<el-table-column prop="material_name" label="物料名称" min-width="130" show-overflow-tooltip />
|
||
<el-table-column prop="sku" label="SKU" width="100" show-overflow-tooltip />
|
||
<el-table-column label="待还数量" width="80" align="center">
|
||
<template #default="{ row }">
|
||
<span style="color: #F56C6C; font-weight: bold;">{{ row.pending_quantity }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="borrow_time" label="借出时间" width="130" />
|
||
</el-table>
|
||
<el-form label-width="80px" style="margin-top: 14px;">
|
||
<el-form-item label="报废原因" required>
|
||
<el-input
|
||
v-model="scrapReason"
|
||
type="textarea"
|
||
:rows="2"
|
||
placeholder="请填写报废原因,如:物品丢失 / 损坏无法归还"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="审批人" required>
|
||
<el-select v-model="scrapApproverId" placeholder="请选择审批人" style="width: 100%" filterable>
|
||
<el-option v-for="u in approvers" :key="u.id" :label="u.username" :value="u.id" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="scrapDialogVisible = false">取消</el-button>
|
||
<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;">
|
||
以下 {{ transferCandidates.length }} 项未归还物品将<strong>一并</strong>转交(共 {{ transferTotalQty }} 件):
|
||
</div>
|
||
</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">
|
||
<template #default="{row}">
|
||
<span
|
||
v-if="row.current_holder_name"
|
||
class="holder-name"
|
||
:class="{ 'is-transferred': isRowTransferred(row) }"
|
||
>{{ formatName(row.current_holder_name) }}</span>
|
||
<span v-else class="text-info">—</span>
|
||
</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>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import request from '@/utils/request'
|
||
import dayjs from 'dayjs'
|
||
import 'dayjs/locale/zh-cn'
|
||
dayjs.locale('zh-cn')
|
||
import { useUserStore } from '@/stores/user'
|
||
import { getApproversList } from '@/api/auth'
|
||
import {
|
||
submitBorrowScrapRequest,
|
||
transferBorrow,
|
||
acceptBorrowTransfer,
|
||
rejectBorrowTransfer,
|
||
getBorrowUsers,
|
||
getBorrowSlipHistory,
|
||
} from '@/api/transaction'
|
||
import { formatName } from '@/utils/format'
|
||
|
||
const userStore = useUserStore()
|
||
|
||
// 防抖定时器
|
||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||
|
||
// 防抖搜索函数
|
||
const debouncedSearch = () => {
|
||
if (debounceTimer) {
|
||
clearTimeout(debounceTimer)
|
||
}
|
||
debounceTimer = setTimeout(() => {
|
||
page.value = 1
|
||
fetchData()
|
||
}, 500)
|
||
}
|
||
|
||
// 清空搜索时立即触发查询
|
||
const handleClearSearch = () => {
|
||
if (debounceTimer) {
|
||
clearTimeout(debounceTimer)
|
||
}
|
||
page.value = 1
|
||
fetchData()
|
||
}
|
||
|
||
// 列与权限Code的映射关系(数据库中的code)
|
||
const permissionMap: Record<string, string | null> = {
|
||
// 基础显示列 — 始终可见
|
||
borrow_no: null,
|
||
borrower_name: null,
|
||
sku: null,
|
||
borrow_time: null,
|
||
return_time: null,
|
||
return_operator: null,
|
||
status: null,
|
||
expected_return_time: null,
|
||
return_location: null,
|
||
// 签名列 — 始终可见(sys_element 补齐后再改为显式权限码)
|
||
borrow_signature: null,
|
||
return_signature: null,
|
||
}
|
||
|
||
// 检查列权限
|
||
const hasColumnPermission = (prop: string) => {
|
||
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') return true
|
||
if (!(prop in permissionMap)) return false
|
||
const code = permissionMap[prop]
|
||
if (code === null || code === undefined) return true
|
||
return userStore.hasPermission(code)
|
||
}
|
||
|
||
const list = ref<any[]>([])
|
||
const total = ref(0)
|
||
// ★ 修改点:默认状态改为 'borrowed' (未归还)
|
||
const status = ref('borrowed')
|
||
const keyword = ref('')
|
||
const searchType = ref('all')
|
||
const dateRange = ref<string[]>([])
|
||
const page = ref(1)
|
||
const loading = ref(false)
|
||
|
||
// --- ★ 高级筛选 ---
|
||
const advancedFilterVisible = ref(false)
|
||
const advancedConditions = ref([{ field: '', operator: '', value: '' }])
|
||
const appliedConditions = ref<any[]>([])
|
||
const advancedFilters = ref<any[]>([])
|
||
const fieldOptions = [
|
||
{ value: 'no', label: '单号' },
|
||
{ value: 'sku', label: 'SKU' },
|
||
{ value: 'material_name', label: '物料名称' },
|
||
{ value: 'borrower_name', label: '操作人' },
|
||
]
|
||
const operatorOptions = [
|
||
{ value: 'contains', label: '包含' },
|
||
{ value: 'eq', label: '等于' },
|
||
{ value: 'not_contains', label: '不包含' },
|
||
{ value: 'ne', label: '不等于' },
|
||
]
|
||
|
||
const addCondition = () => {
|
||
advancedConditions.value.push({ field: '', operator: '', value: '' })
|
||
}
|
||
const removeCondition = (index: number) => {
|
||
advancedConditions.value.splice(index, 1)
|
||
}
|
||
const applyAdvancedFilter = () => {
|
||
const valid = advancedConditions.value.filter(c => c.field && c.operator && c.value !== '')
|
||
advancedFilters.value = valid
|
||
appliedConditions.value = valid
|
||
advancedFilterVisible.value = false
|
||
page.value = 1
|
||
fetchData()
|
||
}
|
||
const resetAdvancedFilter = () => {
|
||
advancedConditions.value = [{ field: '', operator: '', value: '' }]
|
||
advancedFilters.value = []
|
||
appliedConditions.value = []
|
||
advancedFilterVisible.value = false
|
||
page.value = 1
|
||
fetchData()
|
||
}
|
||
|
||
// ★ 借库报废**申请**相关(需审批,不再直报)
|
||
const canScrap = computed(() =>
|
||
userStore.role === 'SUPER_ADMIN' ||
|
||
userStore.hasPermission('op_return:operation')
|
||
)
|
||
const scrapDialogVisible = ref(false)
|
||
const scrapSubmitting = ref(false)
|
||
const scrapBorrowNo = ref('')
|
||
const scrapCandidates = ref<any[]>([])
|
||
const scrapSelected = ref<any[]>([])
|
||
const scrapReason = ref('')
|
||
const scrapApproverId = ref<number | null>(null)
|
||
const approvers = ref<any[]>([])
|
||
|
||
const loadApprovers = async () => {
|
||
try {
|
||
const res = await getApproversList()
|
||
approvers.value = res.data || []
|
||
} catch (e) {
|
||
console.error('[借库报废申请] 审批人列表加载失败', e)
|
||
}
|
||
}
|
||
|
||
// 打开报废申请弹窗:收集该单号下所有未归还明细
|
||
const openScrapDialog = (row: any) => {
|
||
const unreturned = (row.children || []).filter((c: any) => (c.pending_quantity || 0) > 0)
|
||
scrapBorrowNo.value = row.borrow_no
|
||
scrapCandidates.value = unreturned
|
||
scrapSelected.value = []
|
||
scrapReason.value = ''
|
||
scrapApproverId.value = null
|
||
scrapDialogVisible.value = true
|
||
loadApprovers()
|
||
}
|
||
|
||
// 勾选变更
|
||
const handleScrapSelection = (rows: any[]) => {
|
||
scrapSelected.value = rows
|
||
}
|
||
|
||
// 提交报废申请
|
||
const confirmScrap = async () => {
|
||
if (scrapSelected.value.length === 0) return ElMessage.warning('请至少选择一条要申请报废的借出记录')
|
||
if (!scrapReason.value.trim()) return ElMessage.warning('请填写报废原因')
|
||
if (!scrapApproverId.value) return ElMessage.warning('请选择审批人')
|
||
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
`确定对 ${scrapSelected.value.length} 条借出记录提交报废申请吗?\n\n`
|
||
+ '提交后进入审批流程,审批通过并执行后才会扣减总库存。',
|
||
'报废申请确认',
|
||
{ confirmButtonText: '提交申请', cancelButtonText: '取消', type: 'warning' }
|
||
)
|
||
} catch (e) {
|
||
return // 用户取消
|
||
}
|
||
|
||
scrapSubmitting.value = true
|
||
try {
|
||
const recordIds = scrapSelected.value.map((c: any) => c.id)
|
||
const res = await submitBorrowScrapRequest({
|
||
record_ids: recordIds,
|
||
reason: scrapReason.value,
|
||
approver_id: scrapApproverId.value,
|
||
})
|
||
// ★ 提示里点明「尚未扣减」—— 申请不立即改状态,否则用户会以为没生效而重复提交
|
||
ElMessage.success(
|
||
`${res?.msg || '报废申请已提交'}:审批通过并执行后才会标记为已报废`,
|
||
)
|
||
scrapDialogVisible.value = false
|
||
fetchData()
|
||
} catch (err: any) {
|
||
ElMessage.error(err?.msg || err?.message || '提交报废申请失败')
|
||
} finally {
|
||
scrapSubmitting.value = false
|
||
}
|
||
}
|
||
|
||
// ==========================================================================
|
||
// ★ 借库转交(一期:仅支持整单全部转交)
|
||
// ==========================================================================
|
||
// 未归还判定:主表状态下 borrowed / partial_returned 都还有实物未回库
|
||
const isUnreturned = (row: any) =>
|
||
row.status === 'borrowed' || row.status === 'partial_returned'
|
||
|
||
// 当前持有人:整单下各明细可能持有人不同(转交是逐条明细进行的),去重后展示。
|
||
// ★ 同时标出该持有人是否「因转交而来」(≠ 初始借用人)—— 转交过的用主题色加粗,
|
||
// 未转交的保持普通文本,靠颜色而非色块承担区分度。
|
||
const currentHolderList = (row: any): Array<{ name: string; transferred: boolean }> => {
|
||
const borrower = String(row.borrower_name || '').split('/')[0].trim()
|
||
const seen = new Map<string, boolean>()
|
||
;(row.children || []).forEach((c: any) => {
|
||
const name = c.current_holder_name
|
||
if (!name) return
|
||
const transferred = String(name).split('/')[0].trim() !== borrower
|
||
// 同名只要有一次是「转交而来」就按转交样式展示
|
||
seen.set(name, (seen.get(name) || false) || transferred)
|
||
})
|
||
return Array.from(seen, ([name, transferred]) => ({ name, transferred }))
|
||
}
|
||
|
||
// 单条明细是否已转交 —— 供转交弹窗的行内展示使用。
|
||
// (组行有 children,请用上面的 currentHolderList)
|
||
const isRowTransferred = (row: any): boolean => {
|
||
const holder = String(row.current_holder_name || '').split('/')[0].trim()
|
||
const borrower = String(row.borrower_name || '').split('/')[0].trim()
|
||
return !!holder && holder !== borrower
|
||
}
|
||
|
||
// ==========================================================================
|
||
// ★ 整单汇总:主行字段原样取「首条明细」,多明细单会失真,此处按整单重算
|
||
//
|
||
// 失真场景:一张单有 2 条明细,第一条已还清、第二条未还 ——
|
||
// · 主行状态显示「已还」,却仍出现在「未归还」页签里
|
||
// (该页签由后端按整单聚合过滤:单号下至少一条未还);
|
||
// · 归还人只显示第一条的经手人,另一位经手人被隐藏。
|
||
// ==========================================================================
|
||
const aggregateStatus = (children: any[]): string => {
|
||
if (!children.length) return 'borrowed'
|
||
// 全部报废
|
||
if (children.every(c => c.status === 'scrapped')) return 'scrapped'
|
||
// 全部结清(已还 + 已报废;报废在后端口径里同样计入 is_returned)
|
||
const isClosed = (c: any) => c.status === 'returned' || c.status === 'scrapped'
|
||
if (children.every(isClosed)) return 'returned'
|
||
// 仍有未还:若已还过一部分,则呈现「部分归还」而非笼统的「未还」
|
||
const returnedSome = children.some(
|
||
(c: any) => c.status === 'returned' || c.status === 'partial_returned'
|
||
)
|
||
return returnedSome ? 'partial_returned' : 'borrowed'
|
||
}
|
||
|
||
// 归还人汇总:去重(同一库管经手多条明细时只显示一次)
|
||
const aggregateReturnOperators = (children: any[]): string[] => {
|
||
const ops = new Set<string>()
|
||
children.forEach((c: any) => {
|
||
if (c.return_operator) ops.add(String(c.return_operator))
|
||
})
|
||
return Array.from(ops)
|
||
}
|
||
|
||
// 最早 / 最晚时间取值:字符串为 'YYYY-MM-DD HH:mm' 格式,可直接比较
|
||
const earliest = (arr: any[]): string | null =>
|
||
arr.filter(Boolean).sort().shift() ?? null
|
||
const latest = (arr: any[]): string | null =>
|
||
arr.filter(Boolean).sort().pop() ?? null
|
||
|
||
const transferDialogVisible = ref(false)
|
||
const transferSubmitting = ref(false)
|
||
const transferActionLoading = ref(false) // 接收/拒绝按钮 loading
|
||
const transferBorrowNo = ref('')
|
||
const transferCandidates = ref<any[]>([])
|
||
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 {
|
||
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
|
||
transferToUserId.value = null
|
||
transferRemark.value = ''
|
||
transferDialogVisible.value = true
|
||
loadTransferUsers()
|
||
}
|
||
|
||
const confirmTransfer = async () => {
|
||
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}】,无需转交`)
|
||
}
|
||
|
||
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`
|
||
+ `发起后对方需在其借用列表中确认接收,责任才正式转移;\n`
|
||
+ `在对方确认前,物品与责任仍归当前持有人。`,
|
||
'发起转交',
|
||
{ confirmButtonText: '确认发起', cancelButtonText: '取消', type: 'warning' }
|
||
)
|
||
} catch {
|
||
return // 用户取消
|
||
}
|
||
|
||
transferSubmitting.value = true // ★ 按钮 loading 防抖,避免重复提交
|
||
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,此处再弹会双份
|
||
} finally {
|
||
transferSubmitting.value = false
|
||
}
|
||
}
|
||
|
||
// ==========================================================================
|
||
// ★ 双向握手第二步:接收人确认 / 拒绝
|
||
// ==========================================================================
|
||
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
|
||
}
|
||
}
|
||
|
||
// ==========================================================================
|
||
// ★ 流转明细时间线(借出 → 转交(可多次) → 归还 → 报废)
|
||
// ==========================================================================
|
||
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 {
|
||
const params: any = {
|
||
page: page.value,
|
||
status: status.value,
|
||
keyword: keyword.value,
|
||
search_type: searchType.value,
|
||
// ★ 高级筛选:后端约定参数名为 advancedFilters,值为 JSON 字符串
|
||
advancedFilters: JSON.stringify(advancedFilters.value || []),
|
||
}
|
||
if (dateRange.value && dateRange.value.length === 2) {
|
||
params.start_date = dateRange.value[0]
|
||
params.end_date = dateRange.value[1]
|
||
}
|
||
const res = await request({
|
||
url: '/v1/transactions/records',
|
||
method: 'get',
|
||
params
|
||
})
|
||
|
||
// ★ 按 borrow_no 分组聚合为主子表结构
|
||
const groupMap = new Map()
|
||
;(res.data.items || []).forEach(item => {
|
||
if (!groupMap.has(item.borrow_no)) {
|
||
groupMap.set(item.borrow_no, {
|
||
...item,
|
||
children: []
|
||
})
|
||
}
|
||
groupMap.get(item.borrow_no).children.push(item)
|
||
})
|
||
|
||
// ★ 整单汇总重算:主行字段默认取「首条明细」,多明细单在部分归还/分批归还时
|
||
// 会失真,故对状态、归还人、时间字段按整单重算(详见 aggregateStatus 注释)
|
||
list.value = Array.from(groupMap.values()).map(group => {
|
||
const children = group.children || []
|
||
group.status = aggregateStatus(children)
|
||
group.return_operators = aggregateReturnOperators(children)
|
||
|
||
// 预计归还时间取**最早**:后端排序用的正是单内 min(expected_return_time),
|
||
// 主行若显示首条明细的值,多明细单的可见列就会与排序不一致 ——
|
||
// 看上去「排得没规律」,实则列里显示的根本不是排序依据。
|
||
group.expected_return_time = earliest(children.map((c: any) => c.expected_return_time))
|
||
|
||
// 归还时间取**最晚**:状态为「已还」时代表整单结清的那一刻
|
||
group.return_time = latest(children.map((c: any) => c.return_time))
|
||
|
||
return group
|
||
})
|
||
|
||
total.value = res.data.total
|
||
} finally { loading.value = false }
|
||
}
|
||
|
||
const handlePage = (val: number) => {
|
||
page.value = val
|
||
fetchData()
|
||
}
|
||
|
||
// 立即查询(状态/类型切换用,取消防抖)
|
||
const handleSearchImmediate = () => {
|
||
if (debounceTimer) clearTimeout(debounceTimer)
|
||
page.value = 1
|
||
fetchData()
|
||
}
|
||
|
||
const resetFilter = () => {
|
||
if (debounceTimer) clearTimeout(debounceTimer)
|
||
status.value = 'borrowed'
|
||
keyword.value = ''
|
||
searchType.value = 'all'
|
||
dateRange.value = []
|
||
advancedFilters.value = []
|
||
advancedConditions.value = [{ field: '', operator: '', value: '' }]
|
||
appliedConditions.value = []
|
||
page.value = 1
|
||
fetchData()
|
||
}
|
||
|
||
// ★ 新增:格式化预计归还时间及倒计时逻辑(交通灯预警系统)
|
||
const formatExpectedTime = (timeStr: string) => {
|
||
if (!timeStr) return { text: '无限期', diffText: '', style: {} }
|
||
|
||
// 后端返回的可能是 YYYY-MM-DD HH:mm:ss,我们只取日期部分比较
|
||
const expected = dayjs(timeStr).startOf('day')
|
||
const today = dayjs().startOf('day')
|
||
const diffDays = expected.diff(today, 'day')
|
||
|
||
let diffText = ''
|
||
let style: Record<string, string> = {}
|
||
|
||
// 这里的 timeStr 只展示前10位 (日期),或者展示完整
|
||
const displayTime = timeStr.substring(0, 10)
|
||
|
||
if (diffDays < 0) {
|
||
// 已逾期 - 红色加粗
|
||
diffText = ` (已逾期 ${Math.abs(diffDays)} 天)`
|
||
style = { color: '#F56C6C', fontWeight: 'bold' }
|
||
} else if (diffDays <= 1) {
|
||
// 极其紧急(剩 0-1 天)- 警告黄加粗
|
||
diffText = diffDays === 0 ? ` (今天到期)` : ` (剩 ${diffDays} 天)`
|
||
style = { color: '#E6A23C', fontWeight: 'bold' }
|
||
} else if (diffDays <= 7) {
|
||
// 即将到期(剩 2-7 天)- 安全绿
|
||
diffText = ` (剩 ${diffDays} 天)`
|
||
style = { color: '#67C23A' }
|
||
} else {
|
||
// 安全/无期限 - 默认颜色
|
||
diffText = ` (剩 ${diffDays} 天)`
|
||
style = {}
|
||
}
|
||
|
||
return { text: displayTime, diffText, style }
|
||
}
|
||
|
||
onMounted(fetchData)
|
||
|
||
// 组件销毁前清理定时器,防止内存泄漏
|
||
onBeforeUnmount(() => {
|
||
if (debounceTimer) {
|
||
clearTimeout(debounceTimer)
|
||
debounceTimer = null
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* ★ 统一高级搜索版式(与出库记录 / 报废记录一致) */
|
||
.filter-form {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
}
|
||
.filter-form :deep(.el-form-item) {
|
||
margin-bottom: 0;
|
||
margin-right: 12px;
|
||
}
|
||
|
||
/* 高级筛选条件行 */
|
||
.condition-row {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
/* ==========================================================================
|
||
★ 姓名定宽对齐
|
||
中文姓名以「字」为单位占宽,1 个全角字 ≈ 1em,故统一占 3em:
|
||
「石利」与「郭俊玥」占同样的横向空间,一列名字纵向对齐、不再参差。
|
||
|
||
两字姓名由 formatName() 在**渲染副本**上补全角空格撑成三字宽(见
|
||
src/utils/format.ts),这里是兜底:单字姓名等异常长度也不会撑破列对齐。
|
||
row.borrower_name 等底层数据始终是原值。
|
||
========================================================================== */
|
||
.name-fixed {
|
||
display: inline-block;
|
||
min-width: 3em;
|
||
/* 一格内有多个姓名时(同一单的明细持有人/归还人不同)留出间隔,
|
||
否则两个 3em 的盒子会紧贴成「高 雪张 三」难以分辨 */
|
||
margin-right: 6px;
|
||
}
|
||
|
||
/* ==========================================================================
|
||
★ 当前持有人
|
||
业务方要求该列**始终高亮** —— 「东西现在在谁手上」比「当初谁借的」更需要
|
||
一眼看到,故基础态即主题色加粗,不再区分是否转交。
|
||
发生过转交时额外加浅蓝底 + 细边框,强调「经手人已变更」。
|
||
(浅蓝而非实心色块:保持降噪基调,只做一层轻标记)
|
||
========================================================================== */
|
||
.holder-name {
|
||
display: inline-block;
|
||
min-width: 3em;
|
||
margin-right: 6px;
|
||
color: var(--el-color-primary);
|
||
font-weight: 600;
|
||
}
|
||
.holder-name.is-transferred {
|
||
background: var(--el-color-primary-light-9);
|
||
border: 1px solid var(--el-color-primary-light-5);
|
||
border-radius: 4px;
|
||
padding: 1px 6px;
|
||
}
|
||
|
||
/* 中性/次要信息:灰字,不抢视觉重心 */
|
||
.text-info {
|
||
color: #909399;
|
||
font-size: 12px;
|
||
}
|
||
</style>
|