feat(admin-ui): add tags view state
This commit is contained in:
255
WonderQ-Admin-UI-Vue/src/lib/tags.test.ts
Normal file
255
WonderQ-Admin-UI-Vue/src/lib/tags.test.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import {
|
||||
addVisitedTag,
|
||||
closeAllTags,
|
||||
closeTag,
|
||||
closeTagsToLeft,
|
||||
closeTagsToRight,
|
||||
getTagsViewsKey,
|
||||
isVisitedTag,
|
||||
TAGS_VIEWS_KEY,
|
||||
type VisitedTag,
|
||||
} from "./tags";
|
||||
import { useTagsStore } from "../stores/tags";
|
||||
import { LAYOUT_SETTINGS_KEY } from "./layout-settings";
|
||||
|
||||
const home: VisitedTag = {
|
||||
path: "/index",
|
||||
fullPath: "/index",
|
||||
title: "首页",
|
||||
name: "Index",
|
||||
affix: true,
|
||||
};
|
||||
const reports: VisitedTag = { path: "/reports", fullPath: "/reports", title: "报表" };
|
||||
const users: VisitedTag = { path: "/users", fullPath: "/users", title: "用户" };
|
||||
const settings: VisitedTag = { path: "/settings", fullPath: "/settings", title: "设置" };
|
||||
const validQueryTag: VisitedTag = {
|
||||
path: "/query",
|
||||
fullPath: "/query?tab=all",
|
||||
title: "查询",
|
||||
query: { tab: "all", filters: ["open", "owned"] },
|
||||
};
|
||||
|
||||
describe("tags pure functions", () => {
|
||||
it("deduplicates visited tags by fullPath without mutating the input", () => {
|
||||
const views = [home, reports];
|
||||
const result = addVisitedTag(views, { ...reports, title: "报表更新" });
|
||||
|
||||
expect(result).toEqual(views);
|
||||
expect(result).not.toBe(views);
|
||||
});
|
||||
|
||||
it("keeps affix home when closing a tag", () => {
|
||||
expect(closeTag([home, reports], "/index", "/index")).toEqual({
|
||||
views: [home, reports],
|
||||
activePath: "/index",
|
||||
});
|
||||
});
|
||||
|
||||
it("chooses previous, then next, then home when closing the active tag", () => {
|
||||
expect(closeTag([home, reports, users], "/reports", "/reports")).toEqual({
|
||||
views: [home, users],
|
||||
activePath: "/index",
|
||||
});
|
||||
expect(closeTag([home, reports, users], "/users", "/users")).toEqual({
|
||||
views: [home, reports],
|
||||
activePath: "/reports",
|
||||
});
|
||||
expect(closeTag([home], "/index", "/index")).toEqual({ views: [home], activePath: "/index" });
|
||||
});
|
||||
|
||||
it("closes only the requested side and never removes the selected tag", () => {
|
||||
const views = [home, reports, users, settings];
|
||||
|
||||
expect(closeTagsToLeft(views, "/settings")).toEqual([home, settings]);
|
||||
expect(closeTagsToRight(views, "/reports")).toEqual([home, reports]);
|
||||
expect(closeTagsToLeft(views, "/users")).toEqual([home, users, settings]);
|
||||
});
|
||||
|
||||
it("closes all non-affix tags", () => {
|
||||
expect(closeAllTags([home, reports, users])).toEqual([home]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("useTagsStore", () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
Object.defineProperty(globalThis, "window", {
|
||||
configurable: true,
|
||||
value: { localStorage: new MapStorage() },
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Reflect.deleteProperty(globalThis, "window");
|
||||
});
|
||||
|
||||
it("keeps activePath in sync and exposes close fallback to callers", () => {
|
||||
const store = useTagsStore();
|
||||
store.addView(home);
|
||||
store.addView(reports);
|
||||
store.addView(users);
|
||||
|
||||
expect(store.activePath).toBe("/users");
|
||||
expect(store.closeSelectedTag()).toBe("/reports");
|
||||
expect(store.activePath).toBe("/reports");
|
||||
expect(store.visitedViews.map((view) => view.fullPath)).toEqual(["/index", "/reports"]);
|
||||
});
|
||||
|
||||
it("syncs activePath after closing either side or all tags", () => {
|
||||
const store = useTagsStore();
|
||||
store.addView(home);
|
||||
store.addView(reports);
|
||||
store.addView(users);
|
||||
store.addView(settings);
|
||||
|
||||
store.closeRightTags("/reports");
|
||||
expect(store.activePath).toBe("/reports");
|
||||
|
||||
store.addView(users);
|
||||
store.addView(settings);
|
||||
store.activePath = "/reports";
|
||||
store.closeLeftTags("/settings");
|
||||
expect(store.activePath).toBe("/settings");
|
||||
|
||||
store.closeAllTags();
|
||||
expect(store.activePath).toBe("/index");
|
||||
expect(store.refreshSelectedTag()).toEqual(home);
|
||||
});
|
||||
|
||||
it("reads and writes only when tagsView and tagsViewPersist are both true", () => {
|
||||
const storage = new MapStorage();
|
||||
const combinations = [
|
||||
{ tagsView: false, tagsViewPersist: true },
|
||||
{ tagsView: true, tagsViewPersist: false },
|
||||
{ tagsView: true, tagsViewPersist: true },
|
||||
];
|
||||
|
||||
for (const options of combinations) {
|
||||
storage.clear();
|
||||
const store = createStore(storage);
|
||||
store.setPersistenceEnabled(options);
|
||||
store.addView(reports);
|
||||
|
||||
const persisted = storage.getItem(TAGS_VIEWS_KEY);
|
||||
expect(persisted === null).toBe(!options.tagsView || !options.tagsViewPersist);
|
||||
if (options.tagsView && options.tagsViewPersist) {
|
||||
expect(JSON.parse(persisted ?? "null")).toEqual([home, reports]);
|
||||
}
|
||||
}
|
||||
|
||||
storage.setItem(LAYOUT_SETTINGS_KEY, "layout-sentinel");
|
||||
const store = createStore(storage);
|
||||
store.setPersistenceEnabled({ tagsView: true, tagsViewPersist: true });
|
||||
store.updateView({ ...reports, title: "报表页" });
|
||||
expect(storage.getItem(LAYOUT_SETTINGS_KEY)).toBe("layout-sentinel");
|
||||
});
|
||||
|
||||
it("restores only with both settings enabled", () => {
|
||||
const storage = new MapStorage();
|
||||
storage.setItem(TAGS_VIEWS_KEY, JSON.stringify([home, reports]));
|
||||
const disabledStore = createStore(storage);
|
||||
disabledStore.setPersistenceEnabled({ tagsView: false, tagsViewPersist: true });
|
||||
disabledStore.restorePersistedViews();
|
||||
expect(disabledStore.visitedViews).toEqual([home]);
|
||||
|
||||
disabledStore.setPersistenceEnabled({ tagsView: true, tagsViewPersist: true });
|
||||
expect(disabledStore.visitedViews).toEqual([home]);
|
||||
disabledStore.restorePersistedViews();
|
||||
expect(disabledStore.visitedViews).toEqual([home, reports]);
|
||||
|
||||
const enabledStore = createStore(storage);
|
||||
enabledStore.setPersistenceEnabled({ tagsView: true, tagsViewPersist: true });
|
||||
enabledStore.restorePersistedViews();
|
||||
expect(enabledStore.visitedViews).toEqual([home, reports]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["old format object", JSON.stringify({ views: [home, reports] })],
|
||||
["empty path", JSON.stringify([{ path: " ", fullPath: "/reports", title: "报表" }])],
|
||||
["empty fullPath", JSON.stringify([{ path: "/reports", fullPath: "", title: "报表" }])],
|
||||
["empty title", JSON.stringify([{ path: "/reports", fullPath: "/reports", title: " " }])],
|
||||
["non-array", JSON.stringify({ path: "/reports", fullPath: "/reports", title: "报表" })],
|
||||
["invalid JSON", "not-json"],
|
||||
])("falls back to home for %s persisted data", (_label, serialized) => {
|
||||
const storage = new MapStorage();
|
||||
storage.setItem(TAGS_VIEWS_KEY, serialized);
|
||||
const store = createStore(storage);
|
||||
store.setPersistenceEnabled({ tagsView: true, tagsViewPersist: true });
|
||||
store.restorePersistedViews();
|
||||
expect(store.visitedViews).toEqual([home]);
|
||||
});
|
||||
|
||||
it("drops invalid optional fields without allowing truthy strings to control affix behavior", () => {
|
||||
expect(isVisitedTag({ ...validQueryTag, name: 42, icon: false, affix: "false", query: { bad: false } })).toBe(false);
|
||||
const storage = new MapStorage();
|
||||
storage.setItem(TAGS_VIEWS_KEY, JSON.stringify([
|
||||
{ ...validQueryTag, name: 42, icon: false, affix: "false", query: { bad: false } },
|
||||
]));
|
||||
const store = createStore(storage);
|
||||
store.setPersistenceEnabled({ tagsView: true, tagsViewPersist: true });
|
||||
store.restorePersistedViews();
|
||||
|
||||
expect(store.visitedViews).toEqual([home]);
|
||||
store.closeAllTags();
|
||||
expect(store.visitedViews).toEqual([home]);
|
||||
});
|
||||
|
||||
it("preserves a valid query object and string arrays during restore", () => {
|
||||
const storage = new MapStorage();
|
||||
storage.setItem(TAGS_VIEWS_KEY, JSON.stringify([validQueryTag]));
|
||||
const store = createStore(storage);
|
||||
store.setPersistenceEnabled({ tagsView: true, tagsViewPersist: true });
|
||||
store.restorePersistedViews();
|
||||
|
||||
expect(store.visitedViews).toEqual([home, validQueryTag]);
|
||||
});
|
||||
|
||||
it("isolates persisted views by safe scope without touching layout settings", () => {
|
||||
const storage = new MapStorage();
|
||||
storage.setItem(LAYOUT_SETTINGS_KEY, "layout-sentinel");
|
||||
|
||||
const accountA = createStore(storage);
|
||||
accountA.setPersistenceScope("account/A");
|
||||
accountA.setPersistenceEnabled({ tagsView: true, tagsViewPersist: true });
|
||||
accountA.addView(reports);
|
||||
|
||||
const accountB = createStore(storage);
|
||||
accountB.setPersistenceScope("account B");
|
||||
accountB.setPersistenceEnabled({ tagsView: true, tagsViewPersist: true });
|
||||
accountB.addView(users);
|
||||
|
||||
expect(storage.getItem(getTagsViewsKey("account/A"))).toContain("/reports");
|
||||
expect(storage.getItem(getTagsViewsKey("account B"))).toContain("/users");
|
||||
expect(storage.getItem(getTagsViewsKey("account/A"))).not.toContain("/users");
|
||||
expect(storage.getItem(LAYOUT_SETTINGS_KEY)).toBe("layout-sentinel");
|
||||
|
||||
const longScope = "x".repeat(200);
|
||||
expect(getTagsViewsKey("account/A")).not.toBe(getTagsViewsKey("account_A"));
|
||||
expect(getTagsViewsKey(longScope)).not.toBe(getTagsViewsKey(`${longScope}a`));
|
||||
|
||||
const restoredA = createStore(storage);
|
||||
restoredA.setPersistenceScope("account/A");
|
||||
restoredA.setPersistenceEnabled({ tagsView: true, tagsViewPersist: true });
|
||||
restoredA.restorePersistedViews();
|
||||
expect(restoredA.visitedViews).toEqual([home, reports]);
|
||||
});
|
||||
});
|
||||
|
||||
function createStore(storage: Storage) {
|
||||
Object.defineProperty(globalThis, "window", { configurable: true, value: { localStorage: storage } });
|
||||
setActivePinia(createPinia());
|
||||
return useTagsStore();
|
||||
}
|
||||
|
||||
class MapStorage implements Storage {
|
||||
private readonly values = new Map<string, string>();
|
||||
|
||||
get length() { return this.values.size; }
|
||||
clear() { this.values.clear(); }
|
||||
getItem(key: string) { return this.values.get(key) ?? null; }
|
||||
key(index: number) { return [...this.values.keys()][index] ?? null; }
|
||||
removeItem(key: string) { this.values.delete(key); }
|
||||
setItem(key: string, value: string) { this.values.set(key, value); }
|
||||
}
|
||||
120
WonderQ-Admin-UI-Vue/src/lib/tags.ts
Normal file
120
WonderQ-Admin-UI-Vue/src/lib/tags.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
export type TagQuery = Record<string, string | string[]>;
|
||||
|
||||
export type VisitedTag = {
|
||||
path: string;
|
||||
fullPath: string;
|
||||
title: string;
|
||||
name?: string;
|
||||
query?: TagQuery;
|
||||
icon?: string;
|
||||
affix?: boolean;
|
||||
};
|
||||
|
||||
export type CloseTagResult = {
|
||||
views: VisitedTag[];
|
||||
activePath: string;
|
||||
};
|
||||
|
||||
export const HOME_TAG: VisitedTag = {
|
||||
path: "/index",
|
||||
fullPath: "/index",
|
||||
title: "首页",
|
||||
name: "Index",
|
||||
affix: true,
|
||||
};
|
||||
|
||||
export const TAGS_VIEWS_KEY = "wonderq-admin-tags-views:anonymous";
|
||||
|
||||
const TAGS_VIEWS_KEY_PREFIX = "wonderq-admin-tags-views";
|
||||
|
||||
function safePersistenceScope(scope: string | null): string {
|
||||
if (scope === null || scope === "" || scope === "anonymous") return "anonymous";
|
||||
return encodeURIComponent(scope);
|
||||
}
|
||||
|
||||
export function getTagsViewsKey(scope: string | null = null): string {
|
||||
const safeScope = safePersistenceScope(scope);
|
||||
return safeScope === "anonymous" ? TAGS_VIEWS_KEY : `${TAGS_VIEWS_KEY_PREFIX}:${safeScope}`;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function normalizeQuery(value: unknown): TagQuery | undefined {
|
||||
if (!isRecord(value)) return undefined;
|
||||
const entries = Object.entries(value);
|
||||
if (!entries.every(([, item]) => typeof item === "string" || (Array.isArray(item) && item.every((part) => typeof part === "string")))) {
|
||||
return undefined;
|
||||
}
|
||||
return Object.fromEntries(entries.map(([key, item]) => [key, Array.isArray(item) ? [...item] : item])) as TagQuery;
|
||||
}
|
||||
|
||||
export function normalizeVisitedTag(value: unknown): VisitedTag | null {
|
||||
if (!isRecord(value)) return null;
|
||||
const { path, fullPath, title } = value;
|
||||
if (typeof path !== "string" || path.trim() === ""
|
||||
|| typeof fullPath !== "string" || fullPath.trim() === ""
|
||||
|| typeof title !== "string" || title.trim() === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tag: VisitedTag = { path, fullPath, title };
|
||||
if (typeof value.name === "string") tag.name = value.name;
|
||||
if (typeof value.icon === "string") tag.icon = value.icon;
|
||||
if (typeof value.affix === "boolean") tag.affix = value.affix;
|
||||
const query = normalizeQuery(value.query);
|
||||
if (query) tag.query = query;
|
||||
return tag;
|
||||
}
|
||||
|
||||
export function isVisitedTag(value: unknown): value is VisitedTag {
|
||||
if (!isRecord(value) || normalizeVisitedTag(value) === null) return false;
|
||||
if (Object.prototype.hasOwnProperty.call(value, "name") && typeof value.name !== "string") return false;
|
||||
if (Object.prototype.hasOwnProperty.call(value, "icon") && typeof value.icon !== "string") return false;
|
||||
if (Object.prototype.hasOwnProperty.call(value, "affix") && typeof value.affix !== "boolean") return false;
|
||||
if (Object.prototype.hasOwnProperty.call(value, "query") && normalizeQuery(value.query) === undefined) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function addVisitedTag(views: readonly VisitedTag[], tag: VisitedTag): VisitedTag[] {
|
||||
if (views.some((view) => view.fullPath === tag.fullPath)) {
|
||||
return [...views];
|
||||
}
|
||||
return [...views, { ...tag }];
|
||||
}
|
||||
|
||||
export function closeTag(
|
||||
views: readonly VisitedTag[],
|
||||
fullPath: string,
|
||||
activePath: string,
|
||||
): CloseTagResult {
|
||||
const index = views.findIndex((view) => view.fullPath === fullPath);
|
||||
if (index < 0 || views[index]?.affix) {
|
||||
return { views: [...views], activePath };
|
||||
}
|
||||
|
||||
const nextViews = views.filter((view) => view.fullPath !== fullPath);
|
||||
if (fullPath !== activePath) {
|
||||
return { views: nextViews, activePath };
|
||||
}
|
||||
|
||||
const fallback = views[index - 1] ?? views[index + 1] ?? HOME_TAG;
|
||||
return { views: nextViews, activePath: fallback.fullPath };
|
||||
}
|
||||
|
||||
export function closeTagsToLeft(views: readonly VisitedTag[], fullPath: string): VisitedTag[] {
|
||||
const index = views.findIndex((view) => view.fullPath === fullPath);
|
||||
if (index < 0) return [...views];
|
||||
return views.filter((view, viewIndex) => view.affix || viewIndex >= index).map((view) => ({ ...view }));
|
||||
}
|
||||
|
||||
export function closeTagsToRight(views: readonly VisitedTag[], fullPath: string): VisitedTag[] {
|
||||
const index = views.findIndex((view) => view.fullPath === fullPath);
|
||||
if (index < 0) return [...views];
|
||||
return views.filter((view, viewIndex) => view.affix || viewIndex <= index).map((view) => ({ ...view }));
|
||||
}
|
||||
|
||||
export function closeAllTags(views: readonly VisitedTag[]): VisitedTag[] {
|
||||
return views.filter((view) => view.affix).map((view) => ({ ...view }));
|
||||
}
|
||||
166
WonderQ-Admin-UI-Vue/src/stores/tags.ts
Normal file
166
WonderQ-Admin-UI-Vue/src/stores/tags.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { ref } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
import {
|
||||
addVisitedTag,
|
||||
closeAllTags as closeAllVisitedTags,
|
||||
closeTag,
|
||||
closeTagsToLeft,
|
||||
closeTagsToRight,
|
||||
getTagsViewsKey,
|
||||
HOME_TAG,
|
||||
isVisitedTag,
|
||||
normalizeVisitedTag,
|
||||
type VisitedTag,
|
||||
} from "../lib/tags";
|
||||
|
||||
export type TagsPersistenceOptions = {
|
||||
tagsView: boolean;
|
||||
tagsViewPersist: boolean;
|
||||
};
|
||||
|
||||
function getStorage(): Storage | null {
|
||||
try {
|
||||
return typeof window === "undefined" ? null : window.localStorage;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function readPersistedViews(storageKey: string): VisitedTag[] {
|
||||
try {
|
||||
const serialized = getStorage()?.getItem(storageKey);
|
||||
if (serialized === null || serialized === undefined) return [HOME_TAG];
|
||||
const parsed: unknown = JSON.parse(serialized);
|
||||
if (!Array.isArray(parsed)) return [HOME_TAG];
|
||||
|
||||
const views = parsed
|
||||
.filter(isVisitedTag)
|
||||
.map(normalizeVisitedTag)
|
||||
.filter((view): view is VisitedTag => view !== null)
|
||||
.reduce<VisitedTag[]>((result, view) => addVisitedTag(result, view), []);
|
||||
return [HOME_TAG, ...views.filter((view) => view.fullPath !== HOME_TAG.fullPath && !view.affix)];
|
||||
} catch {
|
||||
return [HOME_TAG];
|
||||
}
|
||||
}
|
||||
|
||||
export const useTagsStore = defineStore("tags", () => {
|
||||
const visitedViews = ref<VisitedTag[]>([{ ...HOME_TAG }]);
|
||||
const activePath = ref(HOME_TAG.fullPath);
|
||||
const tagsView = ref(false);
|
||||
const tagsViewPersist = ref(false);
|
||||
let persistenceScope: string | null = null;
|
||||
|
||||
function storageKey() {
|
||||
return getTagsViewsKey(persistenceScope);
|
||||
}
|
||||
|
||||
function persist() {
|
||||
if (!tagsView.value || !tagsViewPersist.value) return;
|
||||
try {
|
||||
getStorage()?.setItem(storageKey(), JSON.stringify(visitedViews.value));
|
||||
} catch {
|
||||
// Storage may be unavailable or disabled in the current browser context.
|
||||
}
|
||||
}
|
||||
|
||||
function setPersistenceEnabled(options: TagsPersistenceOptions) {
|
||||
tagsView.value = options.tagsView;
|
||||
tagsViewPersist.value = options.tagsViewPersist;
|
||||
}
|
||||
|
||||
function setPersistenceScope(scope: string | null) {
|
||||
persistenceScope = scope;
|
||||
}
|
||||
|
||||
function addView(view: VisitedTag) {
|
||||
visitedViews.value = addVisitedTag(visitedViews.value, view);
|
||||
activePath.value = view.fullPath;
|
||||
persist();
|
||||
}
|
||||
|
||||
function updateView(view: VisitedTag) {
|
||||
const index = visitedViews.value.findIndex((item) => item.fullPath === view.fullPath);
|
||||
if (index < 0) {
|
||||
addView(view);
|
||||
return;
|
||||
}
|
||||
visitedViews.value = visitedViews.value.map((item, itemIndex) => itemIndex === index ? { ...view } : { ...item });
|
||||
activePath.value = view.fullPath;
|
||||
persist();
|
||||
}
|
||||
|
||||
function closeSelectedTag(fullPath = activePath.value): string {
|
||||
const result = closeTag(visitedViews.value, fullPath, activePath.value);
|
||||
visitedViews.value = result.views;
|
||||
activePath.value = result.activePath;
|
||||
persist();
|
||||
return result.activePath;
|
||||
}
|
||||
|
||||
function closeOthersTags(fullPath = activePath.value) {
|
||||
visitedViews.value = visitedViews.value.filter((view) => view.affix || view.fullPath === fullPath).map((view) => ({ ...view }));
|
||||
activePath.value = visitedViews.value.some((view) => view.fullPath === fullPath) ? fullPath : HOME_TAG.fullPath;
|
||||
persist();
|
||||
}
|
||||
|
||||
function closeLeftTags(fullPath = activePath.value) {
|
||||
visitedViews.value = closeTagsToLeft(visitedViews.value, fullPath);
|
||||
if (!visitedViews.value.some((view) => view.fullPath === activePath.value)) {
|
||||
activePath.value = visitedViews.value.some((view) => view.fullPath === fullPath)
|
||||
? fullPath
|
||||
: HOME_TAG.fullPath;
|
||||
}
|
||||
persist();
|
||||
}
|
||||
|
||||
function closeRightTags(fullPath = activePath.value) {
|
||||
visitedViews.value = closeTagsToRight(visitedViews.value, fullPath);
|
||||
if (!visitedViews.value.some((view) => view.fullPath === activePath.value)) {
|
||||
activePath.value = visitedViews.value.some((view) => view.fullPath === fullPath)
|
||||
? fullPath
|
||||
: HOME_TAG.fullPath;
|
||||
}
|
||||
persist();
|
||||
}
|
||||
|
||||
function closeAllTags() {
|
||||
visitedViews.value = closeAllVisitedTags(visitedViews.value);
|
||||
activePath.value = HOME_TAG.fullPath;
|
||||
persist();
|
||||
}
|
||||
|
||||
function refreshSelectedTag(): VisitedTag {
|
||||
const selected = visitedViews.value.find((view) => view.fullPath === activePath.value);
|
||||
if (selected) return selected;
|
||||
const fallback = visitedViews.value.find((view) => view.fullPath === HOME_TAG.fullPath) ?? HOME_TAG;
|
||||
activePath.value = fallback.fullPath;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function restorePersistedViews() {
|
||||
if (!tagsView.value || !tagsViewPersist.value) return;
|
||||
visitedViews.value = readPersistedViews(storageKey());
|
||||
activePath.value = visitedViews.value.some((view) => view.fullPath === activePath.value)
|
||||
? activePath.value
|
||||
: HOME_TAG.fullPath;
|
||||
}
|
||||
|
||||
return {
|
||||
visitedViews,
|
||||
activePath,
|
||||
tagsView,
|
||||
tagsViewPersist,
|
||||
setPersistenceEnabled,
|
||||
setPersistenceScope,
|
||||
addView,
|
||||
updateView,
|
||||
closeSelectedTag,
|
||||
closeOthersTags,
|
||||
closeLeftTags,
|
||||
closeRightTags,
|
||||
closeAllTags,
|
||||
refreshSelectedTag,
|
||||
restorePersistedViews,
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user