提交
This commit is contained in:
1 parent
42ad896456
commit
6b2147aa46
66 files changed
+617071
-28046
No files matched your search
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user