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:
2026-09-08 00:49:37 +08:00
parent 3f95335651
commit 60ba5b6306
10 changed files with 746 additions and 0 deletions
+243
View File
@@ -0,0 +1,243 @@
/**
* 单接口完整面板:信息行 → 注意事项折叠 → sk-key 输入 + 发送按钮 → 响应区
*
* sk-key 存在 bench-store(每 tab 独立、无持久化、随刷新重置):
* 2026-09 起从组件局部 ref 提升——EndpointPanel 随 tab 切换卸载重建,
* 局部 ref 会在切回时丢输入;store 持有则切 tab 保留(数据仍由 Query 缓存兜底)
* 响应区:元信息行(状态/耗时/大小)+ 操作(复制/下载)+ 虚拟滚动 JSON 视图
*/
<script setup lang="ts">
import { useClipboard } from '@vueuse/core';
import { computed } from 'vue';
import { findEndpointByPath } from '@/api/endpoints';
import { useManualEndpointQuery } from '@/api/endpoint-queries';
import EndpointInfoLine from '@/components/endpoint-info-line.vue';
import JsonTextView from '@/components/json-text-view.vue';
import { useBenchStore } from '@/stores/bench-store';
const props = defineProps<{ endpointPath: string }>();
const endpoint = computed(() => {
const found = findEndpointByPath(props.endpointPath);
if (!found) throw new Error(`未知接口路径:${props.endpointPath}`);
return found;
});
/* ---------- sk-key(bench-store 持有:每 tab 独立、仅内存态、切 tab 保留) ---------- */
const benchStore = useBenchStore();
const skKeyInput = computed({
get: () => benchStore.getSkKey(props.endpointPath),
set: (value: string) => benchStore.setSkKey(props.endpointPath, value),
});
/* ---------- 手动请求(TanStack Query) ---------- */
const endpointPathRef = computed(() => props.endpointPath);
const { data: responseData, isFetching, hasSent, sendRequest } = useManualEndpointQuery(
endpointPathRef,
skKeyInput,
);
const keyInputStatus = computed(() =>
hasSent.value && !skKeyInput.value.trim() ? 'error' : 'default',
);
function handleSend(): void {
if (!skKeyInput.value.trim()) return; // 空 key 不发(输入框已标红提示)
sendRequest();
}
/* ---------- 响应区操作:复制 / 下载 ---------- */
const { copy: copyResponseToClipboard, copied, isSupported: clipboardSupported } = useClipboard({
source: () => responseData.value?.text ?? '',
legacy: true, // 非 https 环境降级 execCommand
});
const downloadFileName = computed(() => {
const pathSegment = props.endpointPath.replaceAll('/', '_').slice(1);
return `bench-${pathSegment}.json`;
});
function downloadResponseAsFile(): void {
const text = responseData.value?.text;
if (!text) return;
const blob = new Blob([text], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = downloadFileName.value;
anchor.click();
URL.revokeObjectURL(url);
}
</script>
<template>
<div class="endpoint-panel">
<!-- 信息行:method + 路径 + 鉴权徽标 -->
<section class="panel-section">
<EndpointInfoLine :endpoint="endpoint" />
</section>
<!-- 注意事项折叠(坑文档,文案来自 endpoints.ts) -->
<section class="panel-section">
<t-collapse :default-value="[]" expand-mutex>
<t-collapse-panel value="notes" header="注意事项(接口实证调研结论)">
<ul class="note-list">
<li v-for="note in endpoint.notes" :key="note">{{ note }}</li>
</ul>
</t-collapse-panel>
</t-collapse>
</section>
<!-- 调试行:sk-key + 发送 -->
<section class="panel-section">
<div class="request-row">
<t-input
v-model="skKeyInput"
class="sk-key-input"
type="password"
:clearable="true"
placeholder="sk-xxxxxxxxxxxx"
:status="keyInputStatus"
:tips="keyInputStatus === 'error' ? '请填写 sk-key' : ''"
@enter="handleSend"
/>
<t-button
theme="primary"
:loading="isFetching"
:disabled="!skKeyInput.trim()"
@click="handleSend"
>
发送请求
</t-button>
</div>
</section>
<!-- 响应区 -->
<section v-if="responseData" class="panel-section response-section">
<div class="response-meta">
<span
class="status"
:class="
responseData.status !== null && responseData.status >= 200 && responseData.status < 300
? 'ok'
: 'err'
"
>
{{ responseData.status !== null ? `HTTP ${responseData.status}` : '网络错误' }}
</span>
<span>{{ responseData.durationMs }}ms</span>
<span>{{ responseData.byteLength.toLocaleString('en-US') }} 字符</span>
<span v-if="responseData.networkError" class="network-error">
{{ responseData.networkError }}
</span>
<span class="meta-spacer"></span>
<t-button
v-if="clipboardSupported"
size="small"
variant="outline"
@click="copyResponseToClipboard()"
>
{{ copied ? '已复制' : '复制' }}
</t-button>
<t-button size="small" variant="outline" @click="downloadResponseAsFile">下载</t-button>
</div>
<JsonTextView :json="responseData.json" :raw-text="responseData.text" />
</section>
<section v-else-if="!hasSent" class="panel-section response-placeholder">
填写 sk-key 后点击「发送请求」
</section>
</div>
</template>
<style scoped>
.endpoint-panel {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px 20px 24px;
}
.panel-section {
flex-shrink: 0;
}
.request-row {
display: flex;
gap: 12px;
align-items: flex-start;
}
.sk-key-input {
flex: 1;
max-width: 480px;
}
.response-section {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.response-meta {
display: flex;
gap: 14px;
align-items: center;
margin-bottom: 10px;
font-size: 12.5px;
color: var(--td-text-color-secondary);
font-family: var(--td-font-family-mono, monospace);
}
.meta-spacer {
flex: 1;
}
.status {
font-weight: 700;
}
.status.ok {
color: var(--td-success-color);
}
.status.err {
color: var(--td-error-color);
}
.network-error {
color: var(--td-error-color);
}
.response-placeholder {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
color: var(--td-text-color-placeholder);
}
.note-list {
margin: 0;
padding-left: 18px;
font-size: 12.5px;
color: var(--td-text-color-secondary);
}
.note-list li {
margin: 4px 0;
line-height: 1.5;
}
.note-list li::marker {
color: var(--td-warning-color);
}
</style>