This repository has been archived on 2026-10-09. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
new-api-key-bench/src/components/endpoint-panel.vue
T
skycurtain d4fe766373 feat(layout): tabs 纯导航 + 面板外置,响应区固定分区滚动
参照 t-design-starter 与 Naive UI display-directive=show 语义:t-tabs 只当导航条(list 不传 panel),EndpointPanel 渲染在外置自有容器,v-show 常驻切换(面板不销毁,sk-key/滚动位置/局部状态全保留);删除两条 :deep 穿透,仅留一条收敛规则隐藏空 panel 壳;响应区 overflow:hidden + min-height 220px 固定分区,滚动收敛到 JsonTextView 自身;新增 request-tabs 4 用例锁死显隐/不销毁语义。
2026-09-08 10:48:22 +08:00

248 lines
6.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** * 单接口完整面板:信息行 → 注意事项折叠 → 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: 220px; /* 响应区保底高度:窄窗口下不被上部固定分区压缩到不可用 */
display: flex;
flex-direction: column;
overflow: hidden; /* 分区固定:滚动收敛到 JsonTextView 自身 */
}
.response-meta {
display: flex;
gap: 14px;
align-items: center;
margin-bottom: 10px;
flex-shrink: 0; /* 元信息行固定:不随响应区压缩 */
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>