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:
1 parent
072790989f
commit
42ad896456
1 file changed
+129
-4
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user