Archived
feat(store): bench-store(tabs 开关激活/拖拽重排 + 每 tab sk-key)
sk-key 按接口路径存取,仅内存态、刷新即重置;closeTab 清除对应 key,重开为干净面板。面板组件随 tab 切换卸载重建,输入由 store 保留。
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { useBenchStore } from './bench-store';
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
});
|
||||
describe('openOrActivateTab', () => {
|
||||
it('未打开的接口:追加并激活', () => {
|
||||
const store = useBenchStore();
|
||||
store.openOrActivateTab('/v1/models');
|
||||
expect(store.openTabs).toEqual(['/v1/models']);
|
||||
expect(store.activeTab).toBe('/v1/models');
|
||||
});
|
||||
|
||||
it('已打开的接口:仅激活,不重复追加(Postman 语义)', () => {
|
||||
const store = useBenchStore();
|
||||
store.openOrActivateTab('/v1/models');
|
||||
store.openOrActivateTab('/api/log/token');
|
||||
store.openOrActivateTab('/v1/models');
|
||||
expect(store.openTabs).toEqual(['/v1/models', '/api/log/token']);
|
||||
expect(store.activeTab).toBe('/v1/models');
|
||||
});
|
||||
|
||||
it('保持开启顺序(不按字典序)', () => {
|
||||
const store = useBenchStore();
|
||||
store.openOrActivateTab('/api/usage/token');
|
||||
store.openOrActivateTab('/v1/dashboard/billing/subscription');
|
||||
store.openOrActivateTab('/v1/models');
|
||||
expect(store.openTabs).toEqual([
|
||||
'/api/usage/token',
|
||||
'/v1/dashboard/billing/subscription',
|
||||
'/v1/models',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('skKeys(每 tab 独立,仅内存态)', () => {
|
||||
it('未设置时返回空串', () => {
|
||||
const store = useBenchStore();
|
||||
expect(store.getSkKey('/v1/models')).toBe('');
|
||||
});
|
||||
|
||||
it('setSkKey 按路径存取值(tab 间互不干扰)', () => {
|
||||
const store = useBenchStore();
|
||||
store.setSkKey('/v1/models', 'sk-models');
|
||||
store.setSkKey('/api/log/token', 'sk-logs');
|
||||
expect(store.getSkKey('/v1/models')).toBe('sk-models');
|
||||
expect(store.getSkKey('/api/log/token')).toBe('sk-logs');
|
||||
});
|
||||
|
||||
it('覆盖写入生效(同 tab 换 key)', () => {
|
||||
const store = useBenchStore();
|
||||
store.setSkKey('/v1/models', 'sk-old');
|
||||
store.setSkKey('/v1/models', 'sk-new');
|
||||
expect(store.getSkKey('/v1/models')).toBe('sk-new');
|
||||
});
|
||||
|
||||
it('closeTab 清除该 tab 的 key;其他 tab 不受影响', () => {
|
||||
const store = useBenchStore();
|
||||
store.openOrActivateTab('/v1/models');
|
||||
store.openOrActivateTab('/api/log/token');
|
||||
store.setSkKey('/v1/models', 'sk-models');
|
||||
store.setSkKey('/api/log/token', 'sk-logs');
|
||||
|
||||
store.closeTab('/v1/models');
|
||||
expect(store.getSkKey('/v1/models')).toBe('');
|
||||
expect(store.getSkKey('/api/log/token')).toBe('sk-logs');
|
||||
|
||||
// 重新打开是干净面板(key 不复活)
|
||||
store.openOrActivateTab('/v1/models');
|
||||
expect(store.getSkKey('/v1/models')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('closeTab', () => {
|
||||
it('关闭非激活 tab:激活态不变', () => {
|
||||
const store = useBenchStore();
|
||||
store.openOrActivateTab('/v1/models');
|
||||
store.openOrActivateTab('/api/log/token');
|
||||
store.closeTab('/v1/models');
|
||||
expect(store.openTabs).toEqual(['/api/log/token']);
|
||||
expect(store.activeTab).toBe('/api/log/token');
|
||||
});
|
||||
|
||||
it('关闭激活 tab:优先激活右侧相邻 tab,被关的是最后一个则取左侧', () => {
|
||||
const store = useBenchStore();
|
||||
store.openOrActivateTab('/v1/models');
|
||||
store.openOrActivateTab('/v1/dashboard/billing/usage');
|
||||
store.openOrActivateTab('/api/usage/token');
|
||||
// 当前激活最后一个(/api/usage/token)→ 无右侧,激活左侧
|
||||
store.closeTab('/api/usage/token');
|
||||
expect(store.activeTab).toBe('/v1/dashboard/billing/usage');
|
||||
|
||||
// 重开 /api/usage/token → tabs = [models, billing/usage, api/usage/token]
|
||||
// 关中间的 billing/usage(非激活):激活态不变(activeTab 仍是 api/usage/token)
|
||||
store.openOrActivateTab('/api/usage/token');
|
||||
store.closeTab('/v1/dashboard/billing/usage');
|
||||
expect(store.openTabs).toEqual(['/v1/models', '/api/usage/token']);
|
||||
expect(store.activeTab).toBe('/api/usage/token');
|
||||
});
|
||||
|
||||
it('关闭激活 tab 且存在右侧相邻:右侧补位', () => {
|
||||
const store = useBenchStore();
|
||||
store.openOrActivateTab('/v1/models');
|
||||
store.openOrActivateTab('/v1/dashboard/billing/usage');
|
||||
store.openOrActivateTab('/api/usage/token');
|
||||
// 激活中间的 billing/usage 后关闭 → 右侧 api/usage/token 补位
|
||||
store.activateTab('/v1/dashboard/billing/usage');
|
||||
store.closeTab('/v1/dashboard/billing/usage');
|
||||
expect(store.activeTab).toBe('/api/usage/token');
|
||||
});
|
||||
|
||||
it('关闭唯一 tab:activeTab 置空(右侧渲染空态)', () => {
|
||||
const store = useBenchStore();
|
||||
store.openOrActivateTab('/v1/models');
|
||||
store.closeTab('/v1/models');
|
||||
expect(store.openTabs).toEqual([]);
|
||||
expect(store.activeTab).toBeNull();
|
||||
expect(store.hasOpenTabs).toBe(false);
|
||||
});
|
||||
|
||||
it('关闭未打开的路径:无操作', () => {
|
||||
const store = useBenchStore();
|
||||
store.openOrActivateTab('/v1/models');
|
||||
store.closeTab('/v1/nonexistent');
|
||||
expect(store.openTabs).toEqual(['/v1/models']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reorderTab', () => {
|
||||
function openThreeTabs(store: ReturnType<typeof useBenchStore>): void {
|
||||
store.openOrActivateTab('/v1/models');
|
||||
store.openOrActivateTab('/v1/dashboard/billing/usage');
|
||||
store.openOrActivateTab('/api/usage/token');
|
||||
}
|
||||
|
||||
it('前移:0 → 2 位', () => {
|
||||
const store = useBenchStore();
|
||||
openThreeTabs(store);
|
||||
store.reorderTab('/v1/models', 0, 2);
|
||||
expect(store.openTabs).toEqual(['/v1/dashboard/billing/usage', '/api/usage/token', '/v1/models']);
|
||||
});
|
||||
|
||||
it('后移:2 → 0 位', () => {
|
||||
const store = useBenchStore();
|
||||
openThreeTabs(store);
|
||||
store.reorderTab('/api/usage/token', 2, 0);
|
||||
expect(store.openTabs).toEqual(['/api/usage/token', '/v1/models', '/v1/dashboard/billing/usage']);
|
||||
});
|
||||
|
||||
it('相邻换位:0 ↔ 1', () => {
|
||||
const store = useBenchStore();
|
||||
openThreeTabs(store);
|
||||
store.reorderTab('/v1/models', 0, 1);
|
||||
expect(store.openTabs).toEqual(['/v1/dashboard/billing/usage', '/v1/models', '/api/usage/token']);
|
||||
});
|
||||
|
||||
it('原位不动:from === to', () => {
|
||||
const store = useBenchStore();
|
||||
openThreeTabs(store);
|
||||
store.reorderTab('/v1/dashboard/billing/usage', 1, 1);
|
||||
expect(store.openTabs).toEqual(['/v1/models', '/v1/dashboard/billing/usage', '/api/usage/token']);
|
||||
});
|
||||
|
||||
it('路径与索引不匹配:无操作(防御 t-tabs 事件异常)', () => {
|
||||
const store = useBenchStore();
|
||||
openThreeTabs(store);
|
||||
store.reorderTab('/v1/models', 2, 0); // 索引 2 上是 /api/usage/token
|
||||
expect(store.openTabs).toEqual(['/v1/models', '/v1/dashboard/billing/usage', '/api/usage/token']);
|
||||
});
|
||||
|
||||
it('越界索引:无操作', () => {
|
||||
const store = useBenchStore();
|
||||
openThreeTabs(store);
|
||||
store.reorderTab('/v1/models', 0, 99);
|
||||
store.reorderTab('/v1/models', -1, 0);
|
||||
expect(store.openTabs).toEqual(['/v1/models', '/v1/dashboard/billing/usage', '/api/usage/token']);
|
||||
});
|
||||
|
||||
it('重排不影响激活态', () => {
|
||||
const store = useBenchStore();
|
||||
openThreeTabs(store);
|
||||
store.reorderTab('/v1/models', 0, 2);
|
||||
expect(store.activeTab).toBe('/api/usage/token');
|
||||
});
|
||||
});
|
||||
|
||||
describe('activateTab', () => {
|
||||
it('激活已打开的 tab', () => {
|
||||
const store = useBenchStore();
|
||||
store.openOrActivateTab('/v1/models');
|
||||
store.openOrActivateTab('/api/log/token');
|
||||
store.activateTab('/v1/models');
|
||||
expect(store.activeTab).toBe('/v1/models');
|
||||
});
|
||||
|
||||
it('未打开的路径:忽略(激活入口统一走 openOrActivateTab)', () => {
|
||||
const store = useBenchStore();
|
||||
store.openOrActivateTab('/v1/models');
|
||||
store.activateTab('/v1/nonexistent');
|
||||
expect(store.activeTab).toBe('/v1/models');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* bench 工作台状态:tabs 的开 / 关 / 激活 + 每 tab 的 sk-key(Postman 语义)
|
||||
* sk-key 2026-09 起提升到 store:EndpointPanel 随 tab 切换卸载重建,
|
||||
* 局部 ref 会丢输入(切回要重贴 key),改由 store 持有以便切 tab 保留;
|
||||
* 无持久化(不落 localStorage),刷新即重置
|
||||
*/
|
||||
|
||||
import { defineStore } from 'pinia';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
/** tab 的唯一标识即接口路径(设计文档 §3.1:tab 标签直接显示接口路径) */
|
||||
export type EndpointPath = string;
|
||||
|
||||
export const useBenchStore = defineStore('bench', () => {
|
||||
/* ---------- state ---------- */
|
||||
|
||||
/** 已打开的 tab(按开启顺序) */
|
||||
const openTabs = ref<EndpointPath[]>([]);
|
||||
|
||||
/** 当前激活的 tab;全部关光为 null(右侧渲染空态引导页) */
|
||||
const activeTab = ref<EndpointPath | null>(null);
|
||||
|
||||
/** 每 tab 的 sk-key(仅内存态;key 值由 store 内按路径存取) */
|
||||
const skKeys = ref<Record<EndpointPath, string>>({});
|
||||
|
||||
/* ---------- getters ---------- */
|
||||
|
||||
const hasOpenTabs = computed(() => openTabs.value.length > 0);
|
||||
|
||||
/* ---------- actions ---------- */
|
||||
|
||||
/**
|
||||
* 打开或激活 tab(点击 sider 项 / 再次点击同接口时调用)
|
||||
* Postman 语义:已开则激活,未开则追加并激活
|
||||
*/
|
||||
function openOrActivateTab(path: EndpointPath): void {
|
||||
if (!openTabs.value.includes(path)) {
|
||||
openTabs.value.push(path);
|
||||
}
|
||||
activeTab.value = path;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭 tab;若关的是激活 tab,激活相邻 tab(优先右侧,无右侧取左侧)
|
||||
* 全部关光 → activeTab = null
|
||||
*/
|
||||
function closeTab(path: EndpointPath): void {
|
||||
const closingIndex = openTabs.value.indexOf(path);
|
||||
if (closingIndex === -1) return;
|
||||
|
||||
openTabs.value.splice(closingIndex, 1);
|
||||
|
||||
// 关 tab 即弃 key(Postman 语义:重新打开是干净面板)
|
||||
delete skKeys.value[path];
|
||||
|
||||
if (activeTab.value === path) {
|
||||
// 优先激活紧邻的右侧 tab;被关的是最后一个则取左侧
|
||||
const nextActive = openTabs.value[closingIndex] ?? openTabs.value[closingIndex - 1];
|
||||
activeTab.value = nextActive ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 激活已打开的 tab(未打开则忽略——激活入口统一走 openOrActivateTab) */
|
||||
function activateTab(path: EndpointPath): void {
|
||||
if (openTabs.value.includes(path)) {
|
||||
activeTab.value = path;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- sk-key(每 tab 独立,仅内存态) ---------- */
|
||||
|
||||
/** 读取某 tab 的 sk-key(未设置返回空串,与面板初始态一致) */
|
||||
function getSkKey(path: EndpointPath): string {
|
||||
return skKeys.value[path] ?? '';
|
||||
}
|
||||
|
||||
/** 写入某 tab 的 sk-key(空串即清除该项) */
|
||||
function setSkKey(path: EndpointPath, key: string): void {
|
||||
skKeys.value = { ...skKeys.value, [path]: key };
|
||||
}
|
||||
|
||||
/**
|
||||
* 拖拽重排 tab(t-tabs drag-sort 事件回调)
|
||||
* fromIndex/toIndex 均为事件原样值,越界或路径不匹配时无操作(防御 t-tabs 事件异常)
|
||||
*/
|
||||
function reorderTab(path: EndpointPath, fromIndex: number, toIndex: number): void {
|
||||
if (fromIndex < 0 || toIndex < 0) return;
|
||||
if (fromIndex >= openTabs.value.length || toIndex >= openTabs.value.length) return;
|
||||
if (openTabs.value[fromIndex] !== path) return;
|
||||
|
||||
// splice 两步走:先摘出,再插入目标位(toIndex 按「摘出后的数组」语义,与 t-tabs 一致)
|
||||
const [movedTab] = openTabs.value.splice(fromIndex, 1);
|
||||
if (movedTab === undefined) return;
|
||||
openTabs.value.splice(toIndex, 0, movedTab);
|
||||
}
|
||||
|
||||
return {
|
||||
openTabs,
|
||||
activeTab,
|
||||
hasOpenTabs,
|
||||
openOrActivateTab,
|
||||
closeTab,
|
||||
activateTab,
|
||||
reorderTab,
|
||||
getSkKey,
|
||||
setSkKey,
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user