feat(web): DataTable groupBy with collapsible counted group headers
Groups keep first-seen order (or a custom order), headers show counts and toggle with aria-expanded; collapsed groups persist per storageKey in localStorage (read after mount, failures ignored). The trash list groups by registered directory when items span more than one. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
abc69886ea
commit
312e70d0bc
4 files changed
+276
-52
No files matched your search
@@ -0,0 +1,58 @@
|
||||
// DataTable groupBy 的纯逻辑:按键分组(保持首次出现顺序或自定义顺序)与展开态持久化。
|
||||
|
||||
export type RowGroup<T> = { key: string; rows: T[] };
|
||||
|
||||
/** 按 key 分组;缺省按首次出现顺序,给 order 时按 order 排组(组内保持原顺序) */
|
||||
export function groupRows<T>(
|
||||
rows: readonly T[],
|
||||
keyOf: (row: T) => string,
|
||||
order?: (a: string, b: string) => number,
|
||||
): RowGroup<T>[] {
|
||||
const map = new Map<string, T[]>();
|
||||
for (const r of rows) {
|
||||
const k = keyOf(r);
|
||||
const list = map.get(k);
|
||||
if (list) list.push(r);
|
||||
else map.set(k, [r]);
|
||||
}
|
||||
const groups = [...map].map(([key, rs]) => ({ key, rows: rs }));
|
||||
return order ? groups.sort((a, b) => order(a.key, b.key)) : groups;
|
||||
}
|
||||
|
||||
const PREFIX = "umt.table-groups:";
|
||||
type Store = Pick<Storage, "getItem" | "setItem">;
|
||||
const defaultStore = (): Store | undefined => globalThis.localStorage;
|
||||
|
||||
/** 读折叠的组(localStorage 不可用或内容损坏时为 null,由调用方取缺省) */
|
||||
export function readCollapsed(storageKey: string | undefined, store = defaultStore): Set<string> | null {
|
||||
if (!storageKey) return null;
|
||||
try {
|
||||
const raw = store()?.getItem(PREFIX + storageKey);
|
||||
if (!raw) return null;
|
||||
const v = JSON.parse(raw) as unknown;
|
||||
return Array.isArray(v) ? new Set(v.filter((x): x is string => typeof x === "string")) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function writeCollapsed(
|
||||
storageKey: string | undefined,
|
||||
collapsed: ReadonlySet<string>,
|
||||
store = defaultStore,
|
||||
): void {
|
||||
if (!storageKey) return;
|
||||
try {
|
||||
store()?.setItem(PREFIX + storageKey, JSON.stringify([...collapsed]));
|
||||
} catch {
|
||||
// 私密窗口 / 配额满:只是不记住
|
||||
}
|
||||
}
|
||||
|
||||
/** 切换一个组的折叠态,返回新集合 */
|
||||
export function toggleCollapsed(collapsed: ReadonlySet<string>, key: string): Set<string> {
|
||||
const next = new Set(collapsed);
|
||||
if (next.has(key)) next.delete(key);
|
||||
else next.add(key);
|
||||
return next;
|
||||
}
|
||||
Reference in new issue
Block a user