feat(assets): add dimension analysis and authorized user previews
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
Refactor asset drilldowns with combined dimension filters, parent summaries and progressive loading. Add the hidden /who read-only permission preview with scoped authorization, identity mapping and audit logging. Refine inventory counts and AI问数 presentation.
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
type GroupSort = 'count-desc' | 'count-asc' | 'label';
|
||||
|
||||
export function dimensionCountUnit(dimension: string): string {
|
||||
if (dimension === 'customer') return '家';
|
||||
if (dimension === 'manager') return '位';
|
||||
if (dimension === 'date') return '天';
|
||||
if (['type', 'model', 'assetStatus', 'assetDisposition', 'operationKind', 'inventoryState', 'abnormalType', 'contract_type'].includes(dimension)) return '种';
|
||||
if (dimension === 'abnormalAge') return '组';
|
||||
return '个';
|
||||
}
|
||||
|
||||
/** Distinct immediate children within each full parent prefix, across all filtered results. */
|
||||
export function dimensionChildCounts<T>(rows: readonly T[], dimensionCount: number, values: (row: T) => readonly string[]): number[][] {
|
||||
const children = new Map<string, Set<string>>();
|
||||
const keys = rows.map(row => {
|
||||
const parts = values(row);
|
||||
return Array.from({ length: Math.max(0, dimensionCount - 1) }, (_, depth) => {
|
||||
const key = JSON.stringify(parts.slice(0, depth + 1));
|
||||
const set = children.get(key) ?? new Set<string>();
|
||||
set.add(parts[depth + 1]);
|
||||
children.set(key, set);
|
||||
return key;
|
||||
});
|
||||
});
|
||||
return keys.map(row => row.map(key => children.get(key)!.size));
|
||||
}
|
||||
|
||||
/** Use all filtered groups before rendering a prefix so each parent includes every descendant. */
|
||||
export function dimensionParentTotals<T>(
|
||||
rows: readonly T[],
|
||||
dimensionCount: number,
|
||||
values: (row: T) => readonly string[],
|
||||
count: (row: T) => number,
|
||||
): number[][] {
|
||||
const keys = rows.map(row => Array.from({ length: Math.max(0, dimensionCount - 1) }, (_, depth) => JSON.stringify(values(row).slice(0, depth + 1))));
|
||||
const totals = new Map<string, number>();
|
||||
rows.forEach((row, index) => {
|
||||
for (const key of keys[index]) totals.set(key, (totals.get(key) || 0) + count(row));
|
||||
});
|
||||
return keys.map(row => row.map(key => totals.get(key) || 0));
|
||||
}
|
||||
|
||||
/** Sort siblings by their combined count, keeping every parent subtree contiguous. */
|
||||
export function orderDimensionGroups<T>(
|
||||
rows: readonly T[],
|
||||
dimensionCount: number,
|
||||
values: (row: T) => readonly string[],
|
||||
count: (row: T) => number,
|
||||
sort: GroupSort = 'count-desc',
|
||||
compareAt?: (a: T, b: T, index: number) => number,
|
||||
): T[] {
|
||||
const visit = (siblings: readonly T[], depth: number): T[] => {
|
||||
if (depth >= dimensionCount || siblings.length < 2) return [...siblings];
|
||||
const groups = new Map<string, { rows: T[]; count: number }>();
|
||||
for (const row of siblings) {
|
||||
const key = values(row)[depth];
|
||||
const group = groups.get(key) ?? { rows: [], count: 0 };
|
||||
group.rows.push(row);
|
||||
group.count += count(row);
|
||||
groups.set(key, group);
|
||||
}
|
||||
return [...groups.values()].sort((a, b) => {
|
||||
const difference = sort === 'count-desc' ? b.count - a.count : sort === 'count-asc' ? a.count - b.count : 0;
|
||||
return difference || (compareAt
|
||||
? compareAt(a.rows[0], b.rows[0], depth)
|
||||
: values(a.rows[0])[depth].localeCompare(values(b.rows[0])[depth], 'zh-CN', { numeric: true }));
|
||||
}).flatMap(group => visit(group.rows, depth + 1));
|
||||
};
|
||||
return visit(rows, 0);
|
||||
}
|
||||
|
||||
/** A child is merged only within its full parent prefix. */
|
||||
export function dimensionRowSpans<T>(
|
||||
rows: readonly T[],
|
||||
dimensionCount: number,
|
||||
values: (row: T) => readonly string[],
|
||||
): number[][] {
|
||||
const keys = rows.map(values);
|
||||
const spans = rows.map(() => Array<number>(dimensionCount).fill(1));
|
||||
for (let dimension = 0; dimension < dimensionCount - 1; dimension += 1) {
|
||||
let start = 0;
|
||||
while (start < rows.length) {
|
||||
let end = start + 1;
|
||||
while (end < rows.length && keys[start].slice(0, dimension + 1).every((value, index) => value === keys[end][index])) end += 1;
|
||||
spans[start][dimension] = end - start;
|
||||
for (let row = start + 1; row < end; row += 1) spans[row][dimension] = 0;
|
||||
start = end;
|
||||
}
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
|
||||
/** Grow parent cells with the rendered prefix while reusing the complete span calculation. */
|
||||
export function visibleDimensionRowSpans(spans: readonly (readonly number[])[], visibleCount: number): number[][] {
|
||||
const count = Math.min(spans.length, Math.max(0, Math.floor(visibleCount)));
|
||||
return spans.slice(0, count).map((row, index) => row.map(span => Math.min(span, count - index)));
|
||||
}
|
||||
Reference in New Issue
Block a user