@@ -168,8 +185,11 @@
实际
{{ row.return_time || '-' }}
+
- 无限期
+ 无限期
预计 {{ formatExpectedTime(row.expected_return_time).text }}
@@ -319,7 +339,8 @@
- {{ row.current_holder_name || '—' }}
+ {{ row.current_holder_name }}
+ —
@@ -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()
+ ;(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()
+ 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;
+}