feat: 列宽拖拽 + 筛选栏 + 截图导出 + UI优化

- 新增 v-column-resize 指令,11 张表格均支持拖拽调整列宽
- 工作计划/商机跟单/要客拜访新增筛选栏(客户经理 + 状态)
- 汇总栏 chip 可点击筛选,支持重置与计数
- 新增截图导出功能(dom-to-image-more,像素级文字渲染)
- 截图范围从标题开始,自动隐藏操作按钮,5px白边
- 截图时页面闪烁动画反馈
- 全局 CSS 完善(列宽手柄、截图模式、捕获动画)
- 仪表盘 UI 细节优化 + 亮灯表标签调整
- 登录页 UI 改进

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-11 22:17:45 +08:00
parent deeafe239f
commit e8b92a6f99
17 changed files with 767 additions and 156 deletions
+152
View File
@@ -0,0 +1,152 @@
/**
* v-column-resize directive — adds drag-to-resize handles to el-table column headers.
*
* Usage: <el-table v-column-resize ...>
*
* Holders drag the right edge of any column header (except the last) to resize.
* Works with Element Plus's fixed table-layout by updating <col> widths in
* every colgroup (header + body tables stay in sync).
*/
import type { Directive, DirectiveBinding } from 'vue'
interface DragState {
startX: number
startWidth: number
th: HTMLElement
tableWrapper: HTMLElement
colIndex: number
}
const drag: DragState = {
startX: 0,
startWidth: 0,
th: null!,
tableWrapper: null!,
colIndex: -1,
}
// ── Helpers ──────────────────────────────────────────────
function getColIndex(th: HTMLElement): number {
const cells = Array.from(th.parentElement?.children || [])
return cells.indexOf(th)
}
/** All <col> elements across every colgroup inside the wrapper, by col index */
function allCols(wrapper: HTMLElement, idx: number): HTMLElement[] {
const groups = wrapper.querySelectorAll('colgroup')
const result: HTMLElement[] = []
groups.forEach((g) => {
const col = g.children[idx] as HTMLElement | undefined
if (col) result.push(col)
})
return result
}
function setColumnWidth(wrapper: HTMLElement, colIndex: number, width: number) {
const w = Math.max(40, width) + 'px'
for (const col of allCols(wrapper, colIndex)) {
col.style.width = w
}
}
// ── Event handlers ───────────────────────────────────────
function onMouseMove(e: MouseEvent) {
const diff = e.clientX - drag.startX
setColumnWidth(drag.tableWrapper, drag.colIndex, drag.startWidth + diff)
}
function onMouseUp() {
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
document.body.style.cursor = ''
document.body.style.userSelect = ''
const handle = drag.th.querySelector('.col-resize-handle') as HTMLElement | null
if (handle) handle.classList.remove('is-resizing')
}
function onHandleDown(e: MouseEvent, th: HTMLElement, wrapper: HTMLElement) {
e.preventDefault()
e.stopPropagation()
const idx = getColIndex(th)
const cols = allCols(wrapper, idx)
const currentWidth = cols[0] ? cols[0].getBoundingClientRect().width : th.offsetWidth
drag.startX = e.clientX
drag.startWidth = currentWidth
drag.th = th
drag.tableWrapper = wrapper
drag.colIndex = idx
const handle = th.querySelector('.col-resize-handle') as HTMLElement | null
if (handle) handle.classList.add('is-resizing')
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp)
document.body.style.cursor = 'col-resize'
document.body.style.userSelect = 'none'
}
// ── Setup / Teardown ─────────────────────────────────────
function injectHandles(wrapper: HTMLElement) {
const headerThs = wrapper.querySelectorAll<HTMLElement>(
'.el-table__header-wrapper th:not(.el-table__cell--selection)',
)
if (headerThs.length === 0) return
headerThs.forEach((th) => {
// Don't add handle to the last column (or to columns that already have one)
// Actually, allow all columns except the last to be resizable
if (th.querySelector('.col-resize-handle')) return
const handle = document.createElement('div')
handle.className = 'col-resize-handle'
handle.addEventListener('mousedown', (e: MouseEvent) => onHandleDown(e, th, wrapper))
// Prevent sort trigger on header click when grabbing handle
handle.addEventListener('click', (e: Event) => e.stopPropagation())
th.appendChild(handle)
})
}
// ── Directive definition ─────────────────────────────────
function findWrapper(el: HTMLElement): HTMLElement | null {
return el.querySelector<HTMLElement>('.el-table__inner-wrapper')
|| el.querySelector<HTMLElement>('.el-table__header-wrapper')?.closest('.el-table') as HTMLElement
|| null
}
export const vColumnResize: Directive<HTMLElement> = {
mounted(el: HTMLElement, _binding: DirectiveBinding) {
const trySetup = () => {
const wrapper = findWrapper(el)
if (wrapper) {
injectHandles(wrapper)
return true
}
return false
}
if (!trySetup()) {
const timer = setTimeout(() => trySetup(), 200)
;(el as any).__colResizeTimer = timer
}
// Re-inject handles when DOM changes (sort, filter, data reload)
const observer = new MutationObserver(() => {
const wrapper = findWrapper(el)
if (wrapper) injectHandles(wrapper)
})
observer.observe(el, { childList: true, subtree: true })
;(el as any).__colResizeObserver = observer
},
unmounted(el: HTMLElement) {
clearTimeout((el as any).__colResizeTimer)
const observer = (el as any).__colResizeObserver as MutationObserver | undefined
observer?.disconnect()
},
}
export default vColumnResize