fix(crm): sync quote list stats row widths to table headers

Keep the interactive summary cells aligned when columns resize.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-16 16:02:38 +08:00
1 parent 072790989f
commit 42ad896456
1 file changed
+129 -4
+129 -4
View File
@@ -124,6 +124,7 @@
</div>
<div class="list-body list-wrap">
<div ref="quoteTableWrapRef" class="quote-table-wrap">
<ListTable table-key="crm-quote"
v-loading="loading"
class="list-table"
@@ -133,6 +134,8 @@
@selection-change="handleSelectionChange"
@expand-change="handleExpandChange"
@row-dblclick="handleView"
@header-dragend="onQuoteHeaderDragEnd"
@scroll="onQuoteTableScroll"
>
<el-table-column type="expand" width="55">
<template #default="scope">
@@ -379,12 +382,19 @@
<el-table-column v-if="isColumnVisible('remark')" label="备注" align="center" prop="remark" min-width="200" show-overflow-tooltip />
</ListTable>
<div class="quote-list-stats-row" aria-label="列表列统计">
<div
class="quote-list-stats-row"
aria-label="列表列统计"
>
<div class="quote-list-stats-row__fixed">
<div class="quote-list-stats-row__cell is-fixed" style="width: 55px" />
<div class="quote-list-stats-row__cell is-fixed" style="width: 55px" />
</div>
<div class="quote-list-stats-row__scroll">
<div
ref="quoteStatsScrollRef"
class="quote-list-stats-row__scroll"
@scroll="onStatsRowScroll"
>
<div
v-for="col in statsBusinessColumns"
:key="col.prop"
@@ -412,6 +422,7 @@
</div>
</div>
</div>
</div>
<div class="pagination list-pager-bar">
<div class="list-pager-selected">
@@ -807,7 +818,7 @@ import DictTag from '@/components/DictTag/index.vue'
import { $modal } from '@/utils/modal'
import { formatMoneyDisplay } from '@/utils/money-format'
import { formatBrandForPrint } from '@/utils/crm-product-brand'
import { ref, reactive, onMounted, nextTick, getCurrentInstance, toRefs, computed, watch, type ComponentInternalInstance } from 'vue';
import { ref, reactive, onMounted, onUnmounted, nextTick, getCurrentInstance, toRefs, computed, watch, type ComponentInternalInstance } from 'vue';
import { useRouter } from 'vue-router';
import { useClipboard } from '@vueuse/core';
import type { FormInstance } from 'element-plus';
@@ -1933,6 +1944,105 @@ function onStatCommand(prop: string, cmd: string | number) {
)
}
const quoteTableWrapRef = ref<HTMLElement | null>(null)
const quoteStatsScrollRef = ref<HTMLElement | null>(null)
let quoteStatsScrollSyncing = false
let quoteHeaderResizeObserver: ResizeObserver | null = null
function getQuoteMainTableEl(): Element | null {
const wrap = quoteTableWrapRef.value
if (!wrap) return null
return wrap.querySelector(':scope > .el-table') || wrap.querySelector('.el-table')
}
function getQuoteTableScrollEl(): HTMLElement | null {
const table = getQuoteMainTableEl()
if (!table) return null
return (
(table.querySelector('.el-table__body-wrapper .el-scrollbar__wrap') as HTMLElement | null) ||
(table.querySelector('.el-table__body-wrapper') as HTMLElement | null)
)
}
function syncStatsColumnWidths() {
const table = getQuoteMainTableEl()
if (!table) return
const headerCells = table.querySelectorAll('.el-table__header-wrapper th')
const cells = quoteTableWrapRef.value?.querySelectorAll(
'.quote-list-stats-row__scroll .quote-list-stats-row__cell'
)
if (!cells) return
const offset = 2
cells.forEach((el, i) => {
const th = headerCells[i + offset] as HTMLElement | undefined
if (!th) return
;(el as HTMLElement).style.flex = 'none'
;(el as HTMLElement).style.width = `${th.offsetWidth}px`
;(el as HTMLElement).style.minWidth = `${th.offsetWidth}px`
})
}
function scheduleSyncStatsColumnWidths() {
nextTick(() => {
syncStatsColumnWidths()
ensureQuoteHeaderResizeObserver()
})
}
function ensureQuoteHeaderResizeObserver() {
const table = getQuoteMainTableEl()
const header = table?.querySelector('.el-table__header-wrapper') as HTMLElement | null
if (!header) return
if (quoteHeaderResizeObserver) {
quoteHeaderResizeObserver.disconnect()
}
quoteHeaderResizeObserver = new ResizeObserver(() => {
syncStatsColumnWidths()
})
quoteHeaderResizeObserver.observe(header)
header.querySelectorAll('th').forEach((th) => {
quoteHeaderResizeObserver?.observe(th)
})
}
function onQuoteHeaderDragEnd() {
scheduleSyncStatsColumnWidths()
}
function onQuoteTableScroll() {
if (quoteStatsScrollSyncing) return
const tableScroll = getQuoteTableScrollEl()
const statsScroll = quoteStatsScrollRef.value
if (!tableScroll || !statsScroll) return
if (statsScroll.scrollLeft === tableScroll.scrollLeft) return
quoteStatsScrollSyncing = true
statsScroll.scrollLeft = tableScroll.scrollLeft
requestAnimationFrame(() => {
quoteStatsScrollSyncing = false
})
}
function onStatsRowScroll() {
if (quoteStatsScrollSyncing) return
const tableScroll = getQuoteTableScrollEl()
const statsScroll = quoteStatsScrollRef.value
if (!tableScroll || !statsScroll) return
if (tableScroll.scrollLeft === statsScroll.scrollLeft) return
quoteStatsScrollSyncing = true
tableScroll.scrollLeft = statsScroll.scrollLeft
requestAnimationFrame(() => {
quoteStatsScrollSyncing = false
})
}
watch(
[statsBusinessColumns, quoteList, visibleColumns],
() => {
scheduleSyncStatsColumnWidths()
},
{ deep: true }
)
const TEMPLATE_SUPPLIER_MARKER = /<!--\s*quote-template-supplier:([A-Z]+)\s*-->/
const TEMPLATE_DYNAMIC_PRODUCTS_PATTERN =
/<!--\s*quote-dynamic-products-start\s*-->[\s\S]*?<!--\s*quote-dynamic-products-end\s*-->/
@@ -2852,6 +2962,16 @@ watchDebounced(
onMounted(() => {
getList();
scheduleSyncStatsColumnWidths();
// ListTable 会在挂载后异步回写持久化列宽,补一次对齐
setTimeout(() => scheduleSyncStatsColumnWidths(), 150);
window.addEventListener('resize', scheduleSyncStatsColumnWidths);
});
onUnmounted(() => {
window.removeEventListener('resize', scheduleSyncStatsColumnWidths);
quoteHeaderResizeObserver?.disconnect();
quoteHeaderResizeObserver = null;
});
</script>
@@ -3160,6 +3280,10 @@ onMounted(() => {
color: #ef4444;
}
.quote-table-wrap {
min-width: 0;
}
.quote-list-stats-row {
display: flex;
align-items: stretch;
@@ -3168,7 +3292,7 @@ onMounted(() => {
font-weight: 600;
color: var(--mint-text, #1e293b);
min-height: 44px;
overflow-x: auto;
overflow: hidden;
}
.quote-list-stats-row__fixed {
@@ -3180,6 +3304,7 @@ onMounted(() => {
display: flex;
flex: 1;
min-width: 0;
overflow-x: auto;
}
.quote-list-stats-row__cell {