style(borrow): 借还记录页视觉降噪、移除前端假排序、主行改按整单汇总

一、视觉降噪
----
· 「当前持有人」由橙色标签改为纯文本:
    未转交 → 普通文本;发生过转交(当前持有人 ≠ 借用人)→ 主题色加粗。
  原先一律用色块,一屏几十个标签会把真正需要关注的「已转交」淹没。
· 「无限期」由蓝色标签改为灰色小字:它不是异常,不该和红色的「已逾期」抢
  视觉重心。(沿用系统既有术语「无限期」,与借出页「无限期/长期借用」一致)
· 转交弹窗内的持有人列同步降噪,保持同一表达。

二、移除前端假排序
----
「借出时间」列的 sortable 是 Element Plus 的**前端排序**,只会重排当前页的
10 条,反而打乱后端按单号聚合的「逾期优先」排序(有限期在前、按最早应还
升序、无限期按最早借出升序)。已移除。若将来需要按其他字段排序,应改用
sortable="custom" 并由后端接管。

三、主行改按整单汇总
----
主行字段原样取「首条明细」(groupMap 首次遇到即定型),多明细单会失真。
现对四项做整单聚合:
  · 状态     —— 全部报废→已报废;全部结清→已还;仍有未还且已还过一部分→
                部分归还;全部未还→未还。
                修复「首条已还清、其余未还 → 主行显示已还,却仍出现在未归还
                页签」的口径矛盾(该页签由后端按整单聚合过滤)。
  · 归还人   —— 去重汇总(原只显示首条明细的经手人)
  · 归还时间 —— 取**最晚**(整单结清的那一刻;原显示最早那笔)
  · 应还时间 —— 取**最早**(与后端排序键 min(expected_return_time) 对齐)

四、姓名定宽对齐
----
中文 1 字 ≈ 1em,故姓名统一占 3em(.name-fixed):「石利」与「郭俊玥」占同样
横向空间,一列名字纵向对齐。
★ 刻意用 CSS 而非往数据里插全角空格:插字符会污染真实数据(复制、导出、检索、
  姓名比对全带着它),而转交/归还的责任链恰恰依赖姓名比对。

★ 如实说明:上述聚合里,状态/归还人/应还时间三项在当前数据上**尚无可见变化**
  —— 实测单号内状态完全一致、归还人同为一人、应还时间因同批发货天然相同。
  它们是防住「部分归还 + 多明细」组合的前瞻性修正。真正改变显示的只有「归还
  时间取最晚」:BOR-20260616-0001(21 条明细、21 个不同归还时间)等 2 张单。
This commit is contained in:
yueli
2026-09-17 09:38:19 +08:00
parent 8d83d4404e
commit ba5394f70c

View File

@ -136,31 +136,48 @@
</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')" prop="borrower_name" label="借用人" width="100" />
<!-- 当前持有人转交后可能与借用人不是同一人
一张单的明细可能持有人各不相同故去重后逐个展示 -->
<el-table-column label="当前持有人" width="120">
<el-table-column v-if="hasColumnPermission('borrower_name')" label="借用人" width="100">
<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>
<span class="name-fixed">{{ row.borrower_name || '—' }}</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="130">
<template #default="{row}">
<template v-if="currentHolderList(row).length">
<span
v-for="h in currentHolderList(row)"
:key="h.name"
class="name-fixed"
:class="{ 'text-primary': h.transferred }"
>{{ 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')" prop="return_operator" label="归还人" width="100" />
<!-- 归还人改为整单汇总主行原取首条明细的归还人多明细分批归还时
会只显示其中一位令人误以为其他人没还过 -->
<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">{{ 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}">
@ -168,8 +185,11 @@
<el-tag type="success" size="small">实际</el-tag>
{{ row.return_time || '-' }}
</div>
<!-- 降噪无限期不是异常不该用蓝色标签抢视觉重心
把注意力留给红色的已逾期改为灰色小字沿用系统既有术语
无限期与借出页的无限期/长期借用保持一致 -->
<div v-else-if="!row.expected_return_time">
<el-tag type="primary" size="small">无限期</el-tag>
<span class="text-info">无限期</span>
</div>
<div v-else>
<span>预计 {{ formatExpectedTime(row.expected_return_time).text }} </span>
@ -319,7 +339,8 @@
<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>
<span v-if="row.current_holder_name" class="name-fixed">{{ row.current_holder_name }}</span>
<span v-else class="text-info">—</span>
</template>
</el-table-column>
<el-table-column label="待还数量" width="90" align="center">
@ -602,14 +623,59 @@ const confirmScrap = async () => {
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 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 }))
}
// ==========================================================================
// ★ 整单汇总:主行字段原样取「首条明细」,多明细单会失真,此处按整单重算
//
// 失真场景:一张单有 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 transferBorrowNo = ref('')
@ -753,13 +819,21 @@ const fetchData = async () => {
groupMap.get(item.borrow_no).children.push(item)
})
// ★ 整单状态重算:所有明细都已报废 → 整单标记 scrapped
// ★ 整单汇总重算:主行字段默认取「首条明细」,多明细单在部分归还/分批归还时
// 会失真,故对状态、归还人、时间字段按整单重算(详见 aggregateStatus 注释)
list.value = Array.from(groupMap.values()).map(group => {
const allScrapped = group.children.length > 0 &&
group.children.every((c: any) => c.status === 'scrapped')
if (allScrapped) {
group.status = 'scrapped'
}
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
})
@ -857,4 +931,31 @@ onBeforeUnmount(() => {
align-items: center;
margin-bottom: 10px;
}
/* ==========================================================================
★ 姓名定宽对齐
中文姓名以「字」为单位占宽1 个全角字 ≈ 1em故统一占 3em
「石利」与「郭俊玥」占同样的横向空间,一列名字纵向对齐、不再参差。
★ 为什么用 CSS 而不是往数据里插空格(如「石 利」):
插字符会污染真实数据 —— 复制出去、导出、检索、姓名比对全都带着那个空格,
而转交/归还的责任链恰恰依赖姓名比对(见 currentHolderList 的归一化逻辑)。
同一个视觉目标,用样式实现不伤数据。
========================================================================== */
.name-fixed {
display: inline-block;
min-width: 3em;
}
/* 发生过转交的持有人:主题色加粗,替代原先的橙色色块标签 */
.text-primary {
color: var(--el-color-primary);
font-weight: 600;
}
/* 中性/次要信息:灰字,不抢视觉重心 */
.text-info {
color: #909399;
font-size: 12px;
}
</style>