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 a77cd71888 feat(view): JSON 折叠交互——chevron、行尾摘要、滚动补偿与折叠全部
- json-text-view:投影层接线(可视窗口/spacer/行号反查/指针换算/边缘滚动八接入点);折叠事务带 scrollTop 补偿(锚点=视口顶部 model 行号,折叠视口上方块无跳变);选区端点写入前钳制,syncDomSelection 修正坐标混用隐患;props.json 变化重置折叠态;chevron(hover 显/已折叠常显)与行尾摘要 {…} N 行;非 JSON 响应折叠 UI 整体关闭(hasFolds 门控)
- endpoint-panel:meta 行「折叠全部/展开全部」(收 depth=1 顶层块,经 defineExpose 通道)
- 新增 6 个视图层测试(折叠渲染/chevron 交互/选区隔离/补偿无跳变/状态重置/非 JSON 降级)
- 附技术博客 docs/JSON预览器代码折叠技术博客.md
- 全量 89/89 四绿(test/type-check/lint/build)
2026-09-09 14:17:42 +08:00

293 lines
8.2 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.
<script setup lang="ts">
/**
* 单接口完整面板:信息行 → 注意事项折叠 → sk-key 输入 + 发送按钮 → 响应区
*
* sk-key 存在 bench-store(每 tab 独立、无持久化、随刷新重置):
* 2026-09 起从组件局部 ref 提升——EndpointPanel 随 tab 切换卸载重建,
* 局部 ref 会在切回时丢输入;store 持有则切 tab 保留(数据仍由 Query 缓存兜底)
* 响应区:元信息行(状态/耗时/大小)+ 操作(复制/下载)+ 虚拟滚动 JSON 视图
*/
import { useClipboard } from '@vueuse/core';
import { computed, ref, watch } 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);
/** 无鉴权接口(如 /api/status)不需要 sk-key:隐藏输入行、按钮不因空 key 禁用 */
const requiresKey = computed(() => endpoint.value.authMode !== 'none');
const keyInputStatus = computed(() =>
hasSent.value && requiresKey.value && !skKeyInput.value.trim() ? 'error' : 'default',
);
const sendButtonDisabled = computed(() => requiresKey.value && !skKeyInput.value.trim());
function handleSend(): void {
if (sendButtonDisabled.value) return; // 空 key 不发(输入框已标红提示);无鉴权接口直接发
sendRequest();
}
/* ---------- 折叠全部 / 展开全部(JsonTextView defineExpose 通道) ---------- */
const jsonTextViewRef = ref<InstanceType<typeof JsonTextView> | null>(null);
const foldable = ref(false);
// hasFolds 是 JsonTextView 内部的 computed,经 watch 桥接为面板侧响应式标志
watch(
() => jsonTextViewRef.value?.hasFolds,
(value) => {
foldable.value = value === true;
},
{ immediate: true },
);
function collapseAllFolds(): void {
jsonTextViewRef.value?.collapseAll();
}
function expandAllFolds(): void {
jsonTextViewRef.value?.expandAll();
}
/* ---------- 响应区操作:复制 / 下载 ---------- */
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 + 发送(无鉴权接口隐藏 key 输入) -->
<section class="panel-section">
<div class="request-row">
<t-input
v-if="requiresKey"
v-model="skKeyInput"
class="sk-key-input"
type="password"
:clearable="true"
placeholder="sk-xxxxxxxxxxxx"
:status="keyInputStatus"
:tips="keyInputStatus === 'error' ? '请填写 sk-key' : ''"
@enter="handleSend"
/>
<span v-else class="no-key-hint">公开接口,无需 sk-key</span>
<t-button
theme="primary"
:loading="isFetching"
:disabled="sendButtonDisabled"
@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="foldable" size="small" variant="outline" @click="collapseAllFolds">
折叠全部
</t-button>
<t-button v-if="foldable" size="small" variant="outline" @click="expandAllFolds">
展开全部
</t-button>
<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 ref="jsonTextViewRef" :json="responseData.json" :raw-text="responseData.text" />
</section>
<section v-else-if="!hasSent" class="panel-section response-placeholder">
{{ requiresKey ? '填写 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;
}
.no-key-hint {
align-self: center;
font-size: 12.5px;
color: var(--td-text-color-placeholder);
}
.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>