Files
shanxia2.0/web-admin/src/utils/list-column-stats.ts
T
2026-08-17 09:22:31 +08:00

249 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}