feat(admin-ui): add tags view state
This commit is contained in:
1 parent
3c8c6490fd
commit
96612607e5
3 files changed
+541
No files matched your search
@@ -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); }
|
||||||
|
}
|
||||||
@@ -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 }));
|
||||||
|
}
|
||||||
@@ -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