This commit is contained in:
2026-08-06 19:03:13 +08:00
parent 7034a40aa8
commit 8695eee8c8
28 changed files with 1100 additions and 162 deletions
@@ -0,0 +1,33 @@
import { beforeEach, describe, expect, it } from 'vitest'
import {
preferenceStorageKey,
readLayoutPreference,
reconcileOrder,
removeLayoutPreference,
writeLayoutPreference,
} from './uiLayoutPreference'
describe('UI layout preferences', () => {
beforeEach(() => localStorage.clear())
it('reconciles stored order with the current stable keys', () => {
expect(reconcileOrder(['actions', 'name', 'status', 'createdAt'], ['status', 'removed', 'name', 'status']))
.toEqual(['status', 'name', 'actions', 'createdAt'])
})
it('falls back safely when cached JSON is invalid', () => {
localStorage.setItem(preferenceStorageKey('table', 'user-1', 'customers'), '{broken')
expect(readLayoutPreference('table', 'user-1', 'customers', ['actions', 'name'], ['name']))
.toEqual({ version: 1, order: ['actions', 'name'], hidden: ['name'] })
})
it('keeps preferences isolated by user and removes them on reset', () => {
writeLayoutPreference('query', 'user-1', 'orders', { version: 1, order: ['status', 'keyword'], hidden: [] })
expect(readLayoutPreference('query', 'user-1', 'orders', ['keyword', 'status']).order).toEqual(['status', 'keyword'])
expect(readLayoutPreference('query', 'user-2', 'orders', ['keyword', 'status']).order).toEqual(['keyword', 'status'])
removeLayoutPreference('query', 'user-1', 'orders')
expect(readLayoutPreference('query', 'user-1', 'orders', ['keyword', 'status']).order).toEqual(['keyword', 'status'])
})
})
+82
View File
@@ -0,0 +1,82 @@
import type { ComputedRef, InjectionKey } from 'vue'
export type LayoutPreferenceKind = 'query' | 'table'
export interface LayoutPreferenceSnapshot {
version: 1
order: string[]
hidden: string[]
}
export const UI_PREFERENCE_SCOPE: InjectionKey<ComputedRef<string>> = Symbol('ui-preference-scope')
function uniqueKeys(values: readonly unknown[]) {
const seen = new Set<string>()
return values.flatMap((value) => {
if (typeof value !== 'string' || !value || seen.has(value)) return []
seen.add(value)
return [value]
})
}
export function preferenceStorageKey(kind: LayoutPreferenceKind, scope: string, preferenceKey: string) {
return `cc-admin-${kind}-layout:v1:${scope}:${preferenceKey}`
}
export function reconcileOrder(availableKeys: readonly string[], storedOrder: readonly string[]) {
const available = uniqueKeys(availableKeys)
const availableSet = new Set(available)
const retained = uniqueKeys(storedOrder).filter((key) => availableSet.has(key))
const retainedSet = new Set(retained)
return [...retained, ...available.filter((key) => !retainedSet.has(key))]
}
export function readLayoutPreference(
kind: LayoutPreferenceKind,
scope: string,
preferenceKey: string,
availableKeys: readonly string[],
defaultHidden: readonly string[] = [],
): LayoutPreferenceSnapshot {
const fallback: LayoutPreferenceSnapshot = {
version: 1,
order: reconcileOrder(availableKeys, []),
hidden: uniqueKeys(defaultHidden).filter((key) => availableKeys.includes(key)),
}
if (!scope || !preferenceKey) return fallback
try {
const parsed = JSON.parse(localStorage.getItem(preferenceStorageKey(kind, scope, preferenceKey)) || 'null') as Partial<LayoutPreferenceSnapshot> | null
if (!parsed || parsed.version !== 1 || !Array.isArray(parsed.order) || !Array.isArray(parsed.hidden)) return fallback
return {
version: 1,
order: reconcileOrder(availableKeys, parsed.order),
hidden: uniqueKeys(parsed.hidden).filter((key) => availableKeys.includes(key)),
}
} catch {
return fallback
}
}
export function writeLayoutPreference(
kind: LayoutPreferenceKind,
scope: string,
preferenceKey: string,
snapshot: LayoutPreferenceSnapshot,
) {
if (!scope || !preferenceKey) return
try {
localStorage.setItem(preferenceStorageKey(kind, scope, preferenceKey), JSON.stringify(snapshot))
} catch {
// Layout preferences are optional and must never block business workflows.
}
}
export function removeLayoutPreference(kind: LayoutPreferenceKind, scope: string, preferenceKey: string) {
if (!scope || !preferenceKey) return
try {
localStorage.removeItem(preferenceStorageKey(kind, scope, preferenceKey))
} catch {
// Ignore unavailable browser storage and keep the in-memory default layout.
}
}