Archived
feat(ui): UI 骨架与请求集成(sider、tabs、接口面板 sk-key 走 store、手动查询)
t-tabs 用官方 list prop 数据驱动(受控 value + 动态增删语义干净);请求纯手动触发,enabled 门控 + refetch,staleTime Infinity 切 tab 不重发,queryKey ['endpoint', path, skKey] 换 key 即新查询;endpoint-panel 的 sk-key computed 读写 store,切 tab 输入保留。
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
// vitest.config.ts 的 environment 是 node;这里需要 DOM 挂载 Vue 应用
|
||||
// @vitest-environment jsdom
|
||||
/**
|
||||
* 集成冒烟:手动触发语义的端到端验证(fetch 打桩)
|
||||
* 重点验证:sendRequest 前 data 为空、发送后拿到数据
|
||||
*/
|
||||
import { QueryClient, VueQueryPlugin } from '@tanstack/vue-query';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { defineComponent, h, ref } from 'vue';
|
||||
import { createApp } from 'vue';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
|
||||
import { useManualEndpointQuery } from './endpoint-queries';
|
||||
|
||||
describe('useManualEndpointQuery(集成冒烟)', () => {
|
||||
it('未发送时不发请求(enabled 门控),发送后拿到数据', async () => {
|
||||
setActivePinia(createPinia());
|
||||
let fetchCallCount = 0;
|
||||
globalThis.fetch = async () => {
|
||||
fetchCallCount += 1;
|
||||
return new Response('{"ok":true}', { status: 200 });
|
||||
};
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
|
||||
let queryResult: ReturnType<typeof useManualEndpointQuery> | null = null;
|
||||
const app = createApp(
|
||||
defineComponent({
|
||||
setup() {
|
||||
queryResult = useManualEndpointQuery(ref('/v1/models'), ref('sk-test'));
|
||||
return () => h('div');
|
||||
},
|
||||
}),
|
||||
);
|
||||
app.use(createPinia());
|
||||
app.use(VueQueryPlugin, { queryClient });
|
||||
app.mount(document.createElement('div'));
|
||||
|
||||
// 未发送:门控生效,不 fetch
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
expect(fetchCallCount).toBe(0);
|
||||
expect(queryResult!.hasSent.value).toBe(false);
|
||||
|
||||
// 发送:真实 fetch + 数据落地
|
||||
queryResult!.sendRequest();
|
||||
await new Promise((resolve) => setTimeout(resolve, 30));
|
||||
expect(fetchCallCount).toBe(1);
|
||||
expect(queryResult!.hasSent.value).toBe(true);
|
||||
expect(queryResult!.data.value?.json).toEqual({ ok: true });
|
||||
expect(queryResult!.data.value?.status).toBe(200);
|
||||
|
||||
app.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* TanStack Query 接入:手动触发的 endpoint 请求
|
||||
*
|
||||
* 语义(设计文档 §3.3 定案):
|
||||
* - queryKey = [endpointPath, skKey]——同 tab 换 key 是新查询,不同 tab(不同 key)缓存天然隔离
|
||||
* - staleTime: Infinity——调试工具的响应不该自己变,切走再切回不自动重发
|
||||
* - 手动触发:sendRequest() 置位 enabled,首次发送后才拉数据;再次点击强制 refetch
|
||||
*/
|
||||
|
||||
import { useQuery, type UseQueryReturnType } from '@tanstack/vue-query';
|
||||
import { readonly, ref, type Ref } from 'vue';
|
||||
|
||||
import { requestGatewayEndpoint, type GatewayResponse } from './gateway-client';
|
||||
|
||||
/** 手动请求的返回契约 */
|
||||
export interface ManualEndpointQuery {
|
||||
/** 响应数据(GatewayResponse,含 status/ms/bytes/text/json/networkError) */
|
||||
data: Readonly<Ref<GatewayResponse | undefined>>;
|
||||
/** 请求进行中 */
|
||||
isFetching: Readonly<Ref<boolean>>;
|
||||
/** 是否已发送过至少一次(区分「未发送」与「发送中」的空态) */
|
||||
hasSent: Readonly<Ref<boolean>>;
|
||||
/** 触发请求(key 为空时由调用方负责拦截,本层不校验) */
|
||||
sendRequest: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动触发的 endpoint 查询。
|
||||
* endpointPath 与 skKey 均为响应式 Ref:换接口 / 换 key 时 queryKey 随之变化(新缓存槽)。
|
||||
*/
|
||||
export function useManualEndpointQuery(
|
||||
endpointPath: Ref<string>,
|
||||
skKey: Ref<string>,
|
||||
): ManualEndpointQuery {
|
||||
const requestEnabled = ref(false);
|
||||
const hasSent = ref(false);
|
||||
|
||||
const query = useQuery<GatewayResponse>({
|
||||
queryKey: ['endpoint', endpointPath, skKey],
|
||||
queryFn: () => requestGatewayEndpoint(endpointPath.value, skKey.value),
|
||||
enabled: requestEnabled,
|
||||
staleTime: Infinity,
|
||||
gcTime: Infinity,
|
||||
retry: false, // 调试工具要看到真实的失败,不重试
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
});
|
||||
|
||||
function sendRequest(): void {
|
||||
hasSent.value = true;
|
||||
requestEnabled.value = true;
|
||||
// 已有缓存(如换回旧 key)时强制重新请求——「发送」按钮的语义就是真的发一次
|
||||
void query.refetch();
|
||||
}
|
||||
|
||||
return {
|
||||
data: query.data,
|
||||
isFetching: query.isFetching,
|
||||
hasSent: readonly(hasSent),
|
||||
sendRequest,
|
||||
};
|
||||
}
|
||||
|
||||
/** 具名导出便于类型引用(避免调用方直接依赖 tanstack 内部类型) */
|
||||
export type { UseQueryReturnType };
|
||||
Reference in New Issue
Block a user