diff --git a/inventory-web/src/utils/format.ts b/inventory-web/src/utils/format.ts
index 1a84029..42edfcb 100644
--- a/inventory-web/src/utils/format.ts
+++ b/inventory-web/src/utils/format.ts
@@ -34,3 +34,37 @@ export function normalizeQty(value: any, fallback = 0): number {
const n = Number(value)
return Number.isFinite(n) ? Number(n.toFixed(4)) : fallback
}
+
+// 中日韩统一表意文字。用于判断一个字符是否占一个「汉字宽」。
+const CJK_CHAR = /[㐀-䶿一-鿿豈-]/
+
+/**
+ * 姓名等宽显示格式化 —— 两个字的中文姓名在中间补一个全角空格,
+ * 视觉上撑成三个字宽,与三字姓名纵向对齐。
+ *
+ * '高雪' -> '高 雪' (中间是 U+3000,占一个汉字宽)
+ * '郭俊玥' -> '郭俊玥'
+ * '欧阳娜娜' -> '欧阳娜娜' (4 字不动)
+ *
+ * ★ 纯展示层函数:只在模板渲染时调用,**绝不写回数据**。
+ * 姓名是转交/归还责任链的匹配键(见 records.vue 的 currentHolderList
+ * 与 return.vue 的 isTransferred),往数据里插不可见字符会让比对、检索、
+ * 导出、复制全部带上它。本函数返回的是「显示用副本」,
+ * row.borrower_name 始终是原值。
+ *
+ * ★ 为什么用 U+3000 而不是普通空格:
+ * 普通空格宽度随字体变化(通常约半个汉字),补进去反而更难对齐;
+ * U+3000 是东亚字体的全角空格,稳定占一个汉字宽 —— 与「1 个汉字 ≈ 1em」
+ * 的等宽前提一致。
+ *
+ * ★ 为什么限定两个字符都是汉字:
+ * 这条规则的目的是「与三字中文姓名对齐」。若姓名为两个拉丁字符(如 'AB'),
+ * 补一个全角空格并不会让它获得汉字宽度,只会渲染出 'A B' 这样的怪相。
+ * 故非汉字组合一律原样返回。
+ */
+export function formatName(name: any): string {
+ if (name === null || name === undefined) return ''
+ const s = String(name)
+ if (s.length !== 2) return s
+ return CJK_CHAR.test(s[0]) && CJK_CHAR.test(s[1]) ? `${s[0]} ${s[1]}` : s
+}
diff --git a/inventory-web/src/views/transaction/records.vue b/inventory-web/src/views/transaction/records.vue
index 776da43..df97ee8 100644
--- a/inventory-web/src/views/transaction/records.vue
+++ b/inventory-web/src/views/transaction/records.vue
@@ -138,7 +138,7 @@
- {{ row.borrower_name || '—' }}
+ {{ formatName(row.borrower_name) || '—' }}
-
+
- {{ row.current_holder_name }}
- —
+ {{ formatName(row.borrower_name) || '—' }}
+
+
+
+
+
+ {{ formatName(row.current_holder_name) }}
+ —
@@ -225,6 +235,7 @@
import { ref, nextTick, onMounted, onUnmounted } from 'vue'
import request from '@/utils/request'
import { getBorrowUsers } from '@/api/transaction'
+import { formatName } from '@/utils/format'
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'
@@ -257,6 +268,16 @@ const loading = ref(false)
const showCamera = ref(false)
const barcodeRef = ref()
+// ★ 是否发生过转交:当前持有人 ≠ 借用人。
+// 两者都是后端 to_dict 的展示名口径,仍归一化到「斜杠前段」再比对 ——
+// 历史数据里可能混入「姓名/拼音」的完整 username 写法,不归一化会把同一人
+// 误判成转交,让整列无故飘蓝。
+const isTransferred = (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
+}
+
// ★ 实际归还人(一期转交改造):必须等于物品的当前持有人,后端强校验。
// 与借出页的借用人下拉共用同一份公司隔离名单。
const returnerId = ref(null)
@@ -630,4 +651,27 @@ onUnmounted(() => {
.sidebar-actions { flex-direction: row; width: 100%; gap: 10px; }
.sidebar-actions .el-button { flex: 1; height: 40px; }
}
+
+/* ==========================================================================
+ ★ 姓名等宽对齐
+ 中文 1 字 ≈ 1em。formatName() 已把两字姓名补成全角三字宽,这里再加一个
+ 最小宽度兜底,保证单字姓名及后续可能出现的异常长度也不破坏列对齐。
+ (视觉对齐只做在样式与展示副本上,row.borrower_name 始终是原值)
+ ========================================================================== */
+.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;
+}