This commit is contained in:
zlwu committed 2026-08-17 09:22:31 +08:00
1 parent 42ad896456
commit 6b2147aa46
66 files changed
+617071 -28046

No files matched your search

+248
View File
@@ -0,0 +1,248 @@
export type ColumnStatMethod = 'sum' | 'avg' | 'max' | 'min'
export type StatsCellAlign = 'left' | 'center' | 'right'
export type StatsRowColumn = {
key: string
interactive: boolean
prop?: string
money?: boolean
/** 显式对齐;缺省时由统计行从主表表头同步 */
align?: StatsCellAlign
}
export const COLUMN_STAT_METHOD_OPTIONS: { value: ColumnStatMethod | ''; label: string }[] = [
{ value: '', label: '无' },
{ value: 'sum', label: '求和' },
{ value: 'avg', label: '平均' },
{ value: 'max', label: '最大值' },
{ value: 'min', label: '最小值' }
]
export const COLUMN_STAT_METHOD_LABEL: Record<ColumnStatMethod, string> = {
sum: '求和',
avg: '平均',
max: '最大值',
min: '最小值'
}
export function normalizeCssTextAlign(value: string | null | undefined): StatsCellAlign {
const ta = String(value || 'left').toLowerCase()
if (ta === 'right' || ta === 'end') return 'right'
if (ta === 'center') return 'center'
return 'left'
}
/**
* 优先列配置 align;金额列默认 right;
* 其余同步主表列对齐(表体 `align`,无数据时回退表头)。
* 注意:多数业务表 `header-align=center` 且 `align=right`,不可只读表头。
*/
export function resolveStatsCellAlign(
col: Pick<StatsRowColumn, 'align' | 'money'>,
columnAlign?: string | null
): StatsCellAlign {
if (col.align === 'left' || col.align === 'center' || col.align === 'right') return col.align
if (col.money) return 'right'
if (columnAlign) return normalizeCssTextAlign(columnAlign)
return 'left'
}
/** 从主表读取各列 text-align:优先首行表体(对应 `align`),无行时回退表头 */
export function getTableColumnTextAligns(table: HTMLElement | null | undefined): string[] {
if (!table) return []
const collect = (cells: NodeListOf<Element>): string[] => {
const aligns: string[] = []
cells.forEach((node) => {
const el = node as HTMLElement
if (el.classList.contains('gutter')) return
aligns.push(getComputedStyle(el).textAlign || 'left')
})
return aligns
}
const firstRow = table.querySelector('.el-table__body-wrapper tbody tr')
if (firstRow) {
const fromBody = collect(firstRow.querySelectorAll('td.el-table__cell'))
if (fromBody.length) return fromBody
}
return collect(table.querySelectorAll('.el-table__header-wrapper th.el-table__cell'))
}
export function applyStatsCellAlignStyles(
cellEl: HTMLElement,
align: StatsCellAlign,
triggerSelector: string
) {
cellEl.style.justifyContent =
align === 'right' ? 'flex-end' : align === 'center' ? 'center' : 'flex-start'
cellEl.classList.remove('is-align-left', 'is-align-center', 'is-align-right')
cellEl.classList.add(`is-align-${align}`)
const trigger = cellEl.querySelector(triggerSelector) as HTMLElement | null
if (trigger) trigger.style.textAlign = align
}
export function parseStatNumber(raw: unknown): number | null {
if (raw == null || raw === '') return null
if (typeof raw === 'number') return Number.isFinite(raw) ? raw : null
let s = String(raw).trim()
if (!s) return null
s = s.replace(/^¥/, '').replace(/%$/, '').replace(/,/g, '')
const n = Number(s)
return Number.isFinite(n) ? n : null
}
function nonEmptyString(raw: unknown): string | null {
if (raw == null) return null
const s = String(raw).trim()
return s ? s : null
}
export function aggregateColumnValues(
values: unknown[],
method: ColumnStatMethod
): number | string | null {
const nums: number[] = []
const strs: string[] = []
for (const v of values) {
const n = parseStatNumber(v)
if (n != null) nums.push(n)
else {
const s = nonEmptyString(v)
if (s != null) strs.push(s)
}
}
if (method === 'sum') {
if (!nums.length) return null
return nums.reduce((a, b) => a + b, 0)
}
if (method === 'avg') {
if (!nums.length) return null
return nums.reduce((a, b) => a + b, 0) / nums.length
}
if (method === 'max') {
if (nums.length) return Math.max(...nums)
if (strs.length) return strs.reduce((a, b) => (a >= b ? a : b))
return null
}
if (nums.length) return Math.min(...nums)
if (strs.length) return strs.reduce((a, b) => (a <= b ? a : b))
return null
}
export function formatStatNumberResult(
value: number,
opts: { money?: boolean } = {}
): string {
if (opts.money) {
return value.toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})
}
if (Number.isInteger(value)) return String(value)
return value.toLocaleString('zh-CN', { maximumFractionDigits: 4 })
}
export function formatColumnStatDisplay(
method: ColumnStatMethod | '' | null | undefined,
values: unknown[],
opts: { money?: boolean } = {}
): string {
if (!method) return ''
const label = COLUMN_STAT_METHOD_LABEL[method]
const agg = aggregateColumnValues(values, method)
if (agg == null) return `${label} -`
if (typeof agg === 'number') {
return `${label} ${formatStatNumberResult(agg, opts)}`
}
return `${label} ${agg}`
}
export type ColumnStatConfigMap = Partial<Record<string, ColumnStatMethod>>
export function loadColumnStatConfig(storageKey: string): ColumnStatConfigMap {
try {
const raw = localStorage.getItem(storageKey)
if (!raw) return {}
const parsed = JSON.parse(raw) as Record<string, unknown>
if (!parsed || typeof parsed !== 'object') return {}
const out: ColumnStatConfigMap = {}
for (const [k, v] of Object.entries(parsed)) {
if (v === 'sum' || v === 'avg' || v === 'max' || v === 'min') out[k] = v
}
return out
} catch {
return {}
}
}
export function saveColumnStatConfig(
config: ColumnStatConfigMap,
storageKey: string
): void {
const cleaned: ColumnStatConfigMap = {}
for (const [k, v] of Object.entries(config)) {
if (v === 'sum' || v === 'avg' || v === 'max' || v === 'min') cleaned[k] = v
}
localStorage.setItem(storageKey, JSON.stringify(cleaned))
}
export function setColumnStatMethod(
config: ColumnStatConfigMap,
prop: string,
method: ColumnStatMethod | ''
): ColumnStatConfigMap {
const next = { ...config }
if (!method) delete next[prop]
else next[prop] = method
return next
}
/** 构建与主表列顺序一致的统计行列(前导占位 + 固定列 + 可见可选列) */
export function buildStatsRowColumns(options: {
leadingSpacers?: number
alwaysColumns?: { prop: string; money?: boolean }[]
optionalDefs?: { prop: string }[]
isColumnVisible?: (prop: string) => boolean
moneyProps?: Set<string> | string[]
}): StatsRowColumn[] {
const {
leadingSpacers = 0,
alwaysColumns = [],
optionalDefs = [],
isColumnVisible = () => true,
moneyProps
} = options
const moneySet =
moneyProps instanceof Set
? moneyProps
: new Set(moneyProps || [])
const cols: StatsRowColumn[] = []
for (let i = 0; i < leadingSpacers; i++) {
cols.push({ key: `__spacer_${i}`, interactive: false })
}
for (const col of alwaysColumns) {
const money = col.money ?? moneySet.has(col.prop)
cols.push({
key: col.prop,
interactive: true,
prop: col.prop,
money,
...(money ? { align: 'right' as const } : {})
})
}
for (const def of optionalDefs) {
if (!isColumnVisible(def.prop)) continue
const money = moneySet.has(def.prop)
cols.push({
key: def.prop,
interactive: true,
prop: def.prop,
money,
...(money ? { align: 'right' as const } : {})
})
}
return cols
}